# 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 Commands today: `deploy` (local → remote), `sync` (remote → local), `provision` (bootstrap a local site), and `destroy` (tear down a local site). 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/gitea.ts` — shared Gitea API primitives: `getGiteaToken`/`getGiteaBaseUrl` (env resolution, used by both `env.ts` and `provision`), plus `giteaRepoExists` (GET repo) and `generateGiteaRepo` (POST `/generate` from a template repo). - `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. - `src/commands/provision.ts` — bootstraps a minimal local site from a positional ``. Resolves env via `readProvisionEnv` (`WP_LOCAL_ROOT_PATH`, `WEBSIMPLE_STACK_DOMAIN`, `WEBSIMPLE_STACK_PROTOCOL`, `WP_VERSION`, `WP_LOCALE`, `WPOP_GITEA_OWNER`, `WPOP_GITEA_TEMPLATE`, plus a Gitea token) and derives path (`${WP_LOCAL_ROOT_PATH}/`), DB (`wp_`), and URL. Each step is idempotent ("check, then act"): ensure the Gitea `wp-sites/` repo (create from `templates/wp-boilerplate` if missing), clone it, report git freshness, `CREATE DATABASE IF NOT EXISTS`, `wp core download --skip-content`, `wp config create`, and inject a `vendor/autoload.php` guard into `wp-config.php`. It does **not** run `wp core install` or `composer install`. - `src/commands/destroy.ts` — the local inverse of `provision`. Resolves env via `readDestroyEnv` (only `WP_LOCAL_ROOT_PATH`), derives path (`${WP_LOCAL_ROOT_PATH}/`) and DB (`wp_`), confirms (yes/no, default No; `--yes` skips), guards that the path is a direct child of the root, then `rm -rf` the folder and `DROP DATABASE IF EXISTS`. Idempotent and **local only** — it never touches the Gitea repo or the remote. - 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` (`deploy`, `provision`, and `destroy` emit a one-shot JSON report). ## 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.