Files
wpop/CLAUDE.md

4.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

@lewebsimple/wpop (wpop) — a Node CLI that deploys WordPress projects from a developer's working tree to a remote host over SSH/rsync. Authored ESM TypeScript, bundled with tsdown to dist/cli.mjs (the bin entry).

Commands

  • pnpm dev -- <args> — run the CLI from source via tsx (e.g. pnpm dev -- deploy --dry-run)
  • pnpm build — bundle to dist/ with tsdown (ESM, node24 target, emits .d.ts)
  • pnpm typechecktsc --noEmit against tsconfig.json
  • pnpm lint / pnpm lint:fix — oxlint
  • pnpm format / pnpm format:check — oxfmt
  • pnpm check — runs format:check + lint + typecheck (use this before declaring work done; there is no test suite)
  • pnpm releasechangelogen --release --push --noAuthors

There are no unit tests. Validate changes by running the CLI with --dry-run (every shell-out is gated by run() and prints [dry-run] <cmd> instead of executing).

Package manager is pnpm@10 (declared via packageManager); husky + lint-staged run oxfmt/oxlint on staged files at commit time.

Architecture

Single command today (deploy), but the layout assumes more will be added.

  • src/cli.ts — commander entry. Defines global flags (--cwd, --dry-run, --json, --yes, --verbose) on the root program and registers subcommands. Each subcommand action calls createContext(program.opts()) and passes the context as the first argument to its handler. New commands should follow this pattern: register in cli.ts, implement in src/commands/<name>.ts, take (context, options).
  • src/lib/context.tsWPopContext carries the resolved cwd and the global flags. Every side-effecting helper takes a context; nothing reads process.cwd() or the flags directly.
  • src/lib/run.tsrun(context, cmd, args, opts) is the single chokepoint for spawning processes via execa. In dry-run mode it logs and returns without executing. Any new shell-out must go through run (or use execa directly only when capturing stdout, and in that case branch on context.dryRun like sshOutput in deploy.ts).
  • src/lib/env.ts — Zod schema and resolution for deploy-time env vars (REMOTE_HOST, REMOTE_USER, REMOTE_PATH, REMOTE_PORT, SSH_PRIVATE_KEY, WPOP_CACHE_DIR, WP_VERSION, WP_LOCALE). Schema is parsed lazily inside the command, not at import. If any REMOTE_* value is missing, it can read Gitea Actions variables from the inferred repo/org using WEBSIMPLE_GITEA_API_TOKEN, WPOP_GITEA_TOKEN, or GITEA_TOKEN; repo inference comes from git remote get-url origin and can be overridden with WPOP_GITEA_REPO or WPOP_GITEA_OWNER + WPOP_GITEA_REPO_NAME.
  • src/lib/ssh.ts — builds PreparedSsh from env: optionally writes SSH_PRIVATE_KEY to a 0600 tempfile, runs ssh-keyscan to populate ~/.ssh/known_hosts, then verifies access with ssh -o BatchMode=yes. sshArgs/sshTarget/rsyncSshShell are used to construct ssh and rsync invocations consistently.
  • src/commands/deploy.ts — orchestrates the deploy. Order matters:
    1. Parse --include (default vendor,plugins,themes,mu-plugins; all adds core).
    2. Build local artifacts: composer install --no-dev (skippable), then per-theme pnpm/yarn/npm install + build under wp-content/themes/*/ (lockfile detection picks the package manager). node_modules is removed after each theme build.
    3. Drift check — for every content component being deployed, list remote top-level dirs under wp-content/<component> and abort if any are absent locally. This guards against rsync --delete wiping plugins/themes installed out-of-band on the server.
    4. Rsync, in this order: core (excludes wp-config.php, wp-content/, .htaccess, .user.ini, php.ini, robots.txt, .well-known/), vendor/ + composer.json/lock, then each content component. After vendor sync, asserts vendor/autoload.php is referenced from remote wp-config.php.
    5. Remote DB updates: wp core update-db, wp wc update if WooCommerce is present, wp acf json sync if ACF ≥ 6.8 is installed.
  • Caches (composer/, npm/, pnpm/, yarn/) live under WPOP_CACHE_DIR (default /tmp/wpop) and are wired into the child env so repeated runs reuse downloads.

Logging uses consola; structured output is opt-in via --json (currently only deploy emits a one-shot JSON header).

Conventions

  • ESM only ("type": "module"); imports use the node: prefix for built-ins.
  • JSON imports use the import attribute syntax (with { type: "json" }) — required by the node24 target.
  • Prefer adding to src/lib/ for shared helpers; commands should stay thin orchestrators that call into lib.
  • oxlint runs the correctness category as errors with typescript/unicorn/oxc plugins enabled — fix lint issues rather than disabling rules.