# 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 -- ` — 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 typecheck` — `tsc --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 release` — `changelogen --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] ` 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/.ts`, take `(context, options)`. - `src/lib/context.ts` — `WPopContext` 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.ts` — `run(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/` 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.