@refactco/refact-os 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/bin/refact-os.js +5 -0
- package/lib/refact-config.js +108 -6
- package/package.json +1 -1
- package/templates/base/README.md +16 -2
- package/templates/base/agent/skills/adopt/SKILL.md +1 -0
- package/templates/base/agent/skills/setup-project/SKILL.md +12 -6
- package/templates/overlays/code/agent/skills/add-codebase/SKILL.md +30 -3
- package/templates/overlays/wordpress/agent/skills/setup-kinsta-deploy/SKILL.md +20 -0
- package/templates/overlays/wordpress/agent/skills/setup-wpengine-deploy/SKILL.md +258 -0
- package/templates/overlays/wordpress/agent/skills/wp-env/SKILL.md +154 -22
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ Notable changes to the refact-os standard and scaffolder.
|
|
|
4
4
|
|
|
5
5
|
**Staying current:** a consumer repo updates with `npm run refact:update` (or `npx @refactco/refact-os init`). That refreshes the package-managed `agent/` payload, **prunes** skills this package has removed or renamed (safe — only skills the package previously shipped, never your own), regenerates `.cursor/` + `.claude/`, and flags `agent/AGENTS.md` drift for a manual merge. The version it last applied is recorded in `.refact-os.json` under `_scaffold.version`, and `init` prints the `old → new` transition so you can scan the entries below for anything needing manual action. Run `npm run refact:validate` afterward.
|
|
6
6
|
|
|
7
|
+
## 1.6.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`wp-env pull wp-config`** — new sub-command reads the staging `wp-config.php` via SSH and classifies each `define()` into the right local destination: scalar non-secrets → `.wp-env.json` `config`, secrets → `.wp-env.override.json` (gitignored), PHP arrays → a guarded local mu-plugin. Restart + verify included.
|
|
11
|
+
- **Host-aware rsync excludes for `pull mu-plugins`.** The `pull mu-plugins` flow now reads `stack.wordpress.hosting` from `.refact-os.json` and automatically excludes host-injected system mu-plugins (Kinsta: `kinsta-mu-plugins/`; WP Engine: `wpengine-common/`, `mu-plugin.php`, etc.) so they don't end up in git.
|
|
12
|
+
- **SSH access migration to `.refact-os.json`.** `init` now migrates SSH connection details from legacy `agent/AGENTS.md` blocks into the structured `stack.wordpress.environments.<env>.ssh` config, making `.refact-os.json` the single source of truth for all deploy/pull flows.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Split `.gitignore` strategy for `apps/wordpress/`.** The wp-env skill now maintains a separate `apps/wordpress/.gitignore` for wp-content paths (local-only mu-plugins, override files) instead of appending to the root `.gitignore`.
|
|
16
|
+
- **Large DB fallback.** `pull db` now uses a two-step dump-to-file approach (SSH export → `docker cp` → `wp db import`) instead of piping directly, preventing stalls on databases larger than ~500 MB.
|
|
17
|
+
- **Release flow is now local-only.** Removed dependency on CI `NPM_TOKEN`; publishing uses the maintainer's interactive `npm login` session with 2FA OTP (see `agent/skills/release/SKILL.md`).
|
|
18
|
+
|
|
7
19
|
## 1.5.2
|
|
8
20
|
|
|
9
21
|
### Added
|
package/bin/refact-os.js
CHANGED
|
@@ -72,6 +72,11 @@ async function runScaffold(args, { force }) {
|
|
|
72
72
|
if (result.missingFields && result.missingFields.length > 0) {
|
|
73
73
|
console.log(`Missing metadata (re-asked on next /refact run): ${result.missingFields.join(", ")}`);
|
|
74
74
|
}
|
|
75
|
+
if (result.agentsMigration && result.agentsMigration.sshLifted) {
|
|
76
|
+
const { env, fields } = result.agentsMigration.sshLifted;
|
|
77
|
+
console.log(`Lifted SSH routing from agent/AGENTS.md → .refact-os.json (stack.wordpress.environments.${env}: ${fields.join(", ")}).`);
|
|
78
|
+
console.log(" You can now delete the '## SSH access' section from agent/AGENTS.md — .refact-os.json is the single source of truth.");
|
|
79
|
+
}
|
|
75
80
|
if (result.contractChanged) {
|
|
76
81
|
console.log("");
|
|
77
82
|
console.log("⚠ agent/AGENTS.md template changed upstream. Your copy was preserved (skipIfExists).");
|
package/lib/refact-config.js
CHANGED
|
@@ -140,6 +140,63 @@ function _readIfExists(p) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Parse a legacy "## SSH access (staging)" block out of an AGENTS.md. Older
|
|
144
|
+
// scaffolds (pre-2026-05) wrote the staging SSH routing as a Markdown table:
|
|
145
|
+
//
|
|
146
|
+
// ## SSH access (staging)
|
|
147
|
+
// | Field | Value |
|
|
148
|
+
// |---|---|
|
|
149
|
+
// | Host | `stlmagstg.ssh.wpengine.net` |
|
|
150
|
+
// | User | `stlmagstg` |
|
|
151
|
+
// | Port | `22` |
|
|
152
|
+
// | Document root | `sites/stlmagstg/` |
|
|
153
|
+
// | Staging URL | `https://stlmagstg.wpenginepowered.com` |
|
|
154
|
+
//
|
|
155
|
+
// `.refact-os.json` is now the single source of truth (see the wp-env skill).
|
|
156
|
+
// This parser is the one-way migration lift: it pulls a filled block out of
|
|
157
|
+
// AGENTS.md so a consumer that hasn't touched the schema gets its `staging.ssh`
|
|
158
|
+
// populated automatically on the next `init`. Returns null if the block is
|
|
159
|
+
// absent, any required row is missing, or any value is still a `<placeholder>`.
|
|
160
|
+
function _parseAgentsSshBlock(agentsContent) {
|
|
161
|
+
if (typeof agentsContent !== "string" || agentsContent.length === 0) return null;
|
|
162
|
+
const headingMatch = agentsContent.match(/^##\s+SSH\s+access[^\n]*$/im);
|
|
163
|
+
if (!headingMatch) return null;
|
|
164
|
+
const startIdx = headingMatch.index + headingMatch[0].length;
|
|
165
|
+
const remainder = agentsContent.slice(startIdx);
|
|
166
|
+
const nextHeading = remainder.match(/^##\s+/m);
|
|
167
|
+
const block = nextHeading ? remainder.slice(0, nextHeading.index) : remainder;
|
|
168
|
+
|
|
169
|
+
const rowRe = /^\|\s*([^|]+?)\s*\|\s*`?([^`|\n]+?)`?\s*\|\s*$/gm;
|
|
170
|
+
const rows = {};
|
|
171
|
+
let m;
|
|
172
|
+
while ((m = rowRe.exec(block)) !== null) {
|
|
173
|
+
const label = m[1].toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
174
|
+
const value = m[2].trim();
|
|
175
|
+
if (!value) continue;
|
|
176
|
+
if (/^<.*>$/.test(value)) continue; // skip unfilled placeholders
|
|
177
|
+
if (/^[-:|\s]+$/.test(value)) continue; // skip the header separator row
|
|
178
|
+
rows[label] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const host = rows.host;
|
|
182
|
+
const user = rows.user;
|
|
183
|
+
const portRaw = rows.port;
|
|
184
|
+
const docRoot = rows["document root"] || rows["doc root"];
|
|
185
|
+
const stagingUrl = rows["staging url"] || rows["url"];
|
|
186
|
+
if (!host || !user || !portRaw || !docRoot) return null;
|
|
187
|
+
const port = Number(portRaw);
|
|
188
|
+
if (!Number.isFinite(port) || port <= 0) return null;
|
|
189
|
+
|
|
190
|
+
const result = {
|
|
191
|
+
user,
|
|
192
|
+
host,
|
|
193
|
+
port,
|
|
194
|
+
path: docRoot.replace(/\/+$/, ""), // strip trailing slash to match new schema
|
|
195
|
+
};
|
|
196
|
+
if (stagingUrl) result.url = stagingUrl;
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
143
200
|
function _stripScaffoldMetadataSection(agentsContent) {
|
|
144
201
|
const lines = agentsContent.split("\n");
|
|
145
202
|
const startIdx = lines.findIndex((line) => /^##\s+Scaffold Metadata\s*$/.test(line));
|
|
@@ -271,15 +328,60 @@ function migrateFromAgents(targetDir, config) {
|
|
|
271
328
|
}
|
|
272
329
|
}
|
|
273
330
|
|
|
331
|
+
// Lift a filled "## SSH access (staging)" block from AGENTS.md into
|
|
332
|
+
// .refact-os.json › stack.wordpress.environments.<staging-like>.ssh. One-way
|
|
333
|
+
// and idempotent: if the JSON already has the ssh routing for the staging-like
|
|
334
|
+
// env, do nothing. (The new wp-env skill reads SSH only from .refact-os.json.)
|
|
335
|
+
let sshLifted = null;
|
|
336
|
+
const wpEntry = config.stack && config.stack.wordpress;
|
|
337
|
+
if (wpEntry && proseSource) {
|
|
338
|
+
if (!wpEntry.environments || typeof wpEntry.environments !== "object" || Array.isArray(wpEntry.environments)) {
|
|
339
|
+
wpEntry.environments = {};
|
|
340
|
+
}
|
|
341
|
+
// Prefer an existing staging-like env to avoid creating a duplicate next to
|
|
342
|
+
// a project's `stage` (or similar) key. Fall back to creating `staging` only
|
|
343
|
+
// if nothing staging-shaped exists yet.
|
|
344
|
+
let stagingKey = null;
|
|
345
|
+
for (const candidate of ["staging", "stage"]) {
|
|
346
|
+
if (wpEntry.environments[candidate]) {
|
|
347
|
+
stagingKey = candidate;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const existingEnv = stagingKey ? wpEntry.environments[stagingKey] : null;
|
|
352
|
+
const sshAbsent =
|
|
353
|
+
!existingEnv ||
|
|
354
|
+
!existingEnv.ssh ||
|
|
355
|
+
typeof existingEnv.ssh !== "object" ||
|
|
356
|
+
!existingEnv.ssh.host ||
|
|
357
|
+
!existingEnv.ssh.user ||
|
|
358
|
+
!existingEnv.ssh.port ||
|
|
359
|
+
!existingEnv.ssh.path;
|
|
360
|
+
if (sshAbsent) {
|
|
361
|
+
const parsed = _parseAgentsSshBlock(proseSource);
|
|
362
|
+
if (parsed) {
|
|
363
|
+
const targetKey = stagingKey || "staging";
|
|
364
|
+
const env =
|
|
365
|
+
wpEntry.environments[targetKey] ||
|
|
366
|
+
(wpEntry.environments[targetKey] = { branch: null, url: null });
|
|
367
|
+
env.ssh = { user: parsed.user, host: parsed.host, port: parsed.port, path: parsed.path };
|
|
368
|
+
if (parsed.url && !env.url) env.url = parsed.url;
|
|
369
|
+
sshLifted = { env: targetKey, fields: ["user", "host", "port", "path", ...(parsed.url && !existingEnv?.url ? ["url"] : [])] };
|
|
370
|
+
migrated = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
274
375
|
// Strip the legacy "## Scaffold Metadata" section from the root AGENTS.md.
|
|
376
|
+
let stripped = false;
|
|
275
377
|
if (rootText != null) {
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
fs.writeFileSync(rootPath,
|
|
279
|
-
|
|
378
|
+
const strippedText = _stripScaffoldMetadataSection(rootText);
|
|
379
|
+
if (strippedText !== rootText) {
|
|
380
|
+
fs.writeFileSync(rootPath, strippedText, "utf8");
|
|
381
|
+
stripped = true;
|
|
280
382
|
}
|
|
281
383
|
}
|
|
282
|
-
return { migrated, stripped
|
|
384
|
+
return { migrated, stripped, sshLifted };
|
|
283
385
|
}
|
|
284
386
|
|
|
285
387
|
function setAsanaProjectId(config, raw) {
|
|
@@ -320,5 +422,5 @@ module.exports = {
|
|
|
320
422
|
ensureStack,
|
|
321
423
|
setAsanaProjectId,
|
|
322
424
|
askAsanaProjectId,
|
|
323
|
-
_internal: { _get, _set, _has, _parseProjectTypeFromAgents, _parseAgentsField, _stripScaffoldMetadataSection },
|
|
425
|
+
_internal: { _get, _set, _has, _parseProjectTypeFromAgents, _parseAgentsField, _parseAgentsSshBlock, _stripScaffoldMetadataSection },
|
|
324
426
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refactco/refact-os",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Installable scaffolder for the agent-first repo standard: minimum-seed substrate + canonical agent/ skill catalog with generated tool adapters",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
package/templates/base/README.md
CHANGED
|
@@ -7,10 +7,24 @@ A Refact engagement scaffolded by [`refact-os`](https://github.com/refactco/refa
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install
|
|
11
|
-
cp .env.example .env
|
|
10
|
+
npm install # installs the @refactco/refact-os devDependency + others
|
|
11
|
+
cp .env.example .env # fill in per-user tokens
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## WordPress quickstart
|
|
15
|
+
|
|
16
|
+
If this is a WordPress engagement (`.refact-os.json` › `stack.wordpress` present), open the repo in Cursor / Claude Code and run, in order:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
/refact init # idempotent checklist: stack, hosting, per-env (url/branch/install/ssh), .env, git
|
|
20
|
+
/refact wp-env setup # one-shot: containers up + plugins/mu-plugins/DB from staging + local domain
|
|
21
|
+
/refact setup wpengine deploy # or: /refact setup kinsta deploy — generates .github/workflows/wordpress-deploy-*.yml
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each command is **idempotent**: re-running on a fully-configured project is a no-op, so a teammate joining later just runs the same sequence and gets prompted for nothing the owner already filled in. Stack/hosting/SSH routing lives in `.refact-os.json`; per-user secrets (tokens, etc.) stay in `.env`. SSH **private keys** for auto-deploy live in GitHub Actions secrets, never in the repo.
|
|
25
|
+
|
|
26
|
+
Delete this section if this isn't a WordPress project.
|
|
27
|
+
|
|
14
28
|
## Where things live
|
|
15
29
|
|
|
16
30
|
| Path | What's in it |
|
|
@@ -35,6 +35,7 @@ Produce a **plan** for bringing an existing repo to the agent-first standard. Th
|
|
|
35
35
|
- Duplicate canonical map (`INDEX.md` vs `docs/index.md`) → which is canonical; merge the other.
|
|
36
36
|
- Duplicate contract (root `AGENTS.md` vs `agent/AGENTS.md`) → consolidate into `agent/AGENTS.md`, leave a thin root pointer.
|
|
37
37
|
- Variant dirs (`docs/tasks` vs `docs/task`) → consolidate onto the standard name.
|
|
38
|
+
- **Existing root `.gitignore` (when a codebase is moving into `apps/<slot>/`)** → propose moving the existing file wholesale to `apps/<slot>/.gitignore` (merge if one already exists there), and refresh the root `.gitignore` from the refact-os shipped template. The existing rules were written for code at the repo root; once the code lives at `apps/<slot>/`, the rules belong with the code. The refreshed root `.gitignore` is project-level only (refact-os hygiene: `.env`, OS junk, the generated `.claude/settings.local.json`, etc.). The user can pluck specific rules back to root afterward if they realize one is project-scoped (e.g. an org-wide editor pattern). Surface this as a single phase, not three — the move is one paired operation.
|
|
38
39
|
- Forbidden `agent/workflows/` / `agent/evals/` → fold each into `agent/skills/<verb-object>/SKILL.md` (orchestrator/review pattern); propose names.
|
|
39
40
|
- **Folders to delete** where a folder is genuinely unneeded (e.g. an empty `docs/deliverables/` on a repo that ships nothing) — list them explicitly so the user can approve.
|
|
40
41
|
- Content not in a clear role → propose a role + destination.
|
|
@@ -57,7 +57,8 @@ Run each check in order. After handling any unmet item, re-check it before movin
|
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| `url` | `"https://www.example.com/"` | Full URL including `https://`. |
|
|
59
59
|
| `branch` | `"main"`, `"stage"`, `"develop"` | Git branch that auto-deploys to this environment. |
|
|
60
|
-
| `
|
|
60
|
+
| `install` | `"stlmagdev"` | **WP Engine only.** The WP Engine install name used in the deploy URL `git@git.wpengine.com:<install>.git`. Required when `hosting === "wpengine"`; omit otherwise. |
|
|
61
|
+
| `ssh` | `{ "user": "example", "host": "1.2.3.4", "port": 12345, "path": "/www/<env>/public" }` | **SSH hosts only** (Kinsta, WP Engine). Used by the `wp-env` skill to pull plugins/mu-plugins/DB from staging. Omit entirely for git-integration hosts (Vercel, Netlify). Never store the private **key** here — that's a CI secret. |
|
|
61
62
|
|
|
62
63
|
**Handle (if unmet):** Ask the user, one focused question at a time, for each type's `hosting` + `runtime` and at least its `production` (usually also `staging`) environment. Accept "skip" — leave the field `null` (or omit `ssh`) so it stays visibly outstanding without blocking init. Write directly into `.refact-os.json` and show the proposed change first. Collect **non-secret routing only** — SSH keys, tokens, and passwords belong in `.env` or the CI secret store, never here.
|
|
63
64
|
|
|
@@ -93,13 +94,18 @@ Run each check in order. After handling any unmet item, re-check it before movin
|
|
|
93
94
|
|
|
94
95
|
**Handle (if unmet):** Ask the user whether they want to install the curated WP skills now — it's optional and they may prefer to defer. If yes, delegate to `agent/skills/install-wp-skills/SKILL.md`. If no, mark the checkbox as user-deferred and move on; do not re-prompt on the next init run unless the user re-asks.
|
|
95
96
|
|
|
96
|
-
### 7.
|
|
97
|
+
### 7. Auto-deploy workflows (WordPress + SSH host only)
|
|
97
98
|
|
|
98
|
-
**Skip this check** if `.refact-os.json` › `stack` has no `wordpress` entry, **or** if `stack.wordpress.hosting` is
|
|
99
|
+
**Skip this check** if `.refact-os.json` › `stack` has no `wordpress` entry, **or** if `stack.wordpress.hosting` is neither `kinsta` nor `wpengine`.
|
|
99
100
|
|
|
100
|
-
**Check:**
|
|
101
|
+
**Check:** A `.github/workflows/wordpress-deploy-*.yml` file exists for each `stack.wordpress.environments.<env>` whose `branch` is set. (Concretely: a `kinsta` project usually has `wordpress-deploy-stage.yml` + `wordpress-deploy-main.yml`; a `wpengine` project usually adds `wordpress-deploy-develop.yml` as well.)
|
|
101
102
|
|
|
102
|
-
**Handle (if unmet):** Ask the user whether they want to create the
|
|
103
|
+
**Handle (if unmet):** Ask the user whether they want to create the auto-deploy workflows now — it requires host-side SSH keys and GitHub Actions secrets, so they may want to defer. If yes:
|
|
104
|
+
|
|
105
|
+
- `stack.wordpress.hosting === "kinsta"` → delegate to `agent/skills/setup-kinsta-deploy/SKILL.md`.
|
|
106
|
+
- `stack.wordpress.hosting === "wpengine"` → delegate to `agent/skills/setup-wpengine-deploy/SKILL.md`.
|
|
107
|
+
|
|
108
|
+
If no, mark as user-deferred and move on.
|
|
103
109
|
|
|
104
110
|
### 8. agent/AGENTS.md drift warning (post-update only)
|
|
105
111
|
|
|
@@ -119,7 +125,7 @@ init checklist:
|
|
|
119
125
|
[x] git + GitHub remote
|
|
120
126
|
[x] apps/wordpress/ (wordpress only)
|
|
121
127
|
[-] WordPress agent skills (deferred by user)
|
|
122
|
-
[x]
|
|
128
|
+
[x] Auto-deploy workflows (wordpress + kinsta/wpengine only)
|
|
123
129
|
[x] agent/AGENTS.md drift (no pending warning)
|
|
124
130
|
```
|
|
125
131
|
|
|
@@ -181,7 +181,34 @@ touch apps/wordpress/wp-content/themes/.gitkeep
|
|
|
181
181
|
touch apps/wordpress/wp-content/mu-plugins/.gitkeep
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
### B3. Write
|
|
184
|
+
### B3. Write `apps/wordpress/.gitignore`
|
|
185
|
+
|
|
186
|
+
This file is load-bearing: it is the deploy filter for WP Engine and Kinsta workflows (only git-tracked files under `apps/wordpress/wp-content/` reach the server). WordPress-scoped ignore rules live here using relative paths; repo-level ignores (`.env`, `node_modules`, `.wp-env.override.json`) stay in the root `.gitignore`.
|
|
187
|
+
|
|
188
|
+
```gitignore
|
|
189
|
+
# Ignore everything in the WordPress root except wp-content.
|
|
190
|
+
/*
|
|
191
|
+
!.gitignore
|
|
192
|
+
!wp-content/
|
|
193
|
+
|
|
194
|
+
# Inside wp-content, only track themes, plugins, and mu-plugins.
|
|
195
|
+
wp-content/*
|
|
196
|
+
!wp-content/themes/
|
|
197
|
+
!wp-content/plugins/
|
|
198
|
+
!wp-content/mu-plugins/
|
|
199
|
+
|
|
200
|
+
# Inside each, ignore everything by default — add explicit ! exceptions
|
|
201
|
+
# for tracked themes/plugins/mu-plugins as they are added to the project.
|
|
202
|
+
wp-content/themes/*
|
|
203
|
+
wp-content/plugins/*
|
|
204
|
+
wp-content/mu-plugins/*
|
|
205
|
+
|
|
206
|
+
# Local-only wp-env mu-plugins (never deploy)
|
|
207
|
+
wp-content/mu-plugins/00-wp-env-local-url.php
|
|
208
|
+
wp-content/mu-plugins/01-wp-env-local-config.php
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### B4. Write a stub README
|
|
185
212
|
|
|
186
213
|
Write `apps/wordpress/REFACT-SOURCE.md`:
|
|
187
214
|
|
|
@@ -193,9 +220,9 @@ Created fresh via `/refact add codebase wordpress` on <DATE>.
|
|
|
193
220
|
This slot is an empty WordPress codebase shell. Drop the client's WordPress source here — plugins under `wp-content/plugins/`, themes under `wp-content/themes/`, mu-plugins under `wp-content/mu-plugins/`.
|
|
194
221
|
```
|
|
195
222
|
|
|
196
|
-
###
|
|
223
|
+
### B5. Report
|
|
197
224
|
|
|
198
|
-
Tell the user the slot exists with empty `wp-content/{plugins,themes,mu-plugins}/` ready for content.
|
|
225
|
+
Tell the user the slot exists with empty `wp-content/{plugins,themes,mu-plugins}/` ready for content, and that `apps/wordpress/.gitignore` was created as the deploy filter (they should add `!` exceptions as they add tracked themes/plugins/mu-plugins).
|
|
199
226
|
|
|
200
227
|
---
|
|
201
228
|
|
|
@@ -184,6 +184,25 @@ Round-trip test that doesn't touch theme/plugin code:
|
|
|
184
184
|
5. Visit the staging URL — bottom-right badge means the tree reached Kinsta and was checked out.
|
|
185
185
|
6. Revert via a **new commit** (don't amend) — delete the file, remove the gitignore line, push.
|
|
186
186
|
|
|
187
|
+
## Vendor policy
|
|
188
|
+
|
|
189
|
+
**The default for refact-os WordPress projects is: `vendor/` directories are tracked in git.** Each plugin (or mu-plugin) commits its `composer install` output alongside the source.
|
|
190
|
+
|
|
191
|
+
Consequences for this deploy flow:
|
|
192
|
+
|
|
193
|
+
- The deploy workflow **must not** run `composer install`. It commits exactly what's tracked under `apps/wordpress/` and force-pushes that tree to Kinsta.
|
|
194
|
+
- The bytes deployed to Kinsta equal the bytes in the merge commit — deploys are deterministic and there's no build step that can drift between environments.
|
|
195
|
+
- New plugin dependencies require a commit that includes the updated `vendor/` tree alongside the `composer.json` change.
|
|
196
|
+
|
|
197
|
+
Override path: a project that wants a build-in-CI flow must (a) add a `composer install --no-dev --optimize-autoloader` step in each plugin dir before the `git add .`, (b) add the matching `vendor/` ignore rules to the repo's `.gitignore`, and (c) document the deviation in `docs/decisions.md` with a responsible person.
|
|
198
|
+
|
|
199
|
+
## `.gitignore` hard rules
|
|
200
|
+
|
|
201
|
+
Kinsta receives **only** what reaches the workflow's fresh-init tree, which is **only** what's tracked by git under `apps/wordpress/`. That means the `.gitignore` files in the repo are the deploy filter, not just a local hygiene tool.
|
|
202
|
+
|
|
203
|
+
- **Never use a root-whitelist `.gitignore` pattern (`/*` followed by `!apps/`, `!docs/`, …)** at the repo root. Whitelists are fragile across branch switches: a branch that doesn't carve out one of the allowed paths can quietly drop tracking on files the deploy depends on. Use blocklist semantics (ignore specific things; track everything else).
|
|
204
|
+
- **Scoped allow-lists belong in `apps/wordpress/.gitignore`**, not at the repo root.
|
|
205
|
+
|
|
187
206
|
## Hard rules
|
|
188
207
|
|
|
189
208
|
1. **Force-push (`--force`) is required for the Kinsta endpoint and only the Kinsta endpoint.** Kinsta's bare repo can't fast-forward against a fresh-init tree. Never `--force` to GitHub's `main` or `stage`.
|
|
@@ -191,6 +210,7 @@ Round-trip test that doesn't touch theme/plugin code:
|
|
|
191
210
|
3. **Never place workflows under `apps/wordpress/.github/`.** GitHub doesn't run nested workflows. If any exist there, delete them.
|
|
192
211
|
4. **Never push to `main` directly.** Stage gets validated first, then `stage → main` via PR.
|
|
193
212
|
5. **Don't bypass the path filter** (e.g. removing `paths: 'apps/wordpress/**'`). Doing so means every README edit redeploys to Kinsta.
|
|
213
|
+
6. **Never run `composer install` (or any other build step) in the deploy workflow** without first changing the vendor policy as described above.
|
|
194
214
|
|
|
195
215
|
## When to stop and ask the user
|
|
196
216
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-wpengine-deploy
|
|
3
|
+
description: Create the WP Engine auto-deploy GitHub workflows for apps/wordpress. WordPress-on-WP-Engine only.
|
|
4
|
+
pattern: procedure
|
|
5
|
+
requires_approval: true
|
|
6
|
+
when_to_use: /refact setup wpengine auto-deploy | add wpengine deploy | create wpengine workflows.
|
|
7
|
+
when_not_to_use: Non-WP-Engine hosting.
|
|
8
|
+
next_skills: []
|
|
9
|
+
sub_agents: []
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# WP Engine Auto-Deploy Reference
|
|
13
|
+
|
|
14
|
+
Use this reference when the user invokes any of:
|
|
15
|
+
|
|
16
|
+
- `/refact setup wpengine auto-deploy`
|
|
17
|
+
- `/refact add wpengine deploy`
|
|
18
|
+
- `/refact enable wpengine auto-deploy`
|
|
19
|
+
- `/refact create wpengine workflows`
|
|
20
|
+
|
|
21
|
+
This reference both **documents** how the WP Engine auto-deploy works for projects scaffolded by this package, and **drives the agent** to create the GitHub Actions workflow files on demand. The workflow YAMLs are *not* pre-vendored into the project — they are generated by following Step 2 below, because not every engagement deploys to WP Engine.
|
|
22
|
+
|
|
23
|
+
## TL;DR
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
push to <env.branch> ──▶ .github/workflows/wordpress-deploy-<env>.yml ──▶ WP Engine <install>
|
|
27
|
+
|
|
28
|
+
(path-filtered on apps/wordpress/**; each workflow copies apps/wordpress/wp-content
|
|
29
|
+
into a fresh /tmp/deploy repo and force-pushes it to git@git.wpengine.com:<install>.git)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each workflow:
|
|
33
|
+
|
|
34
|
+
1. Checks out the monorepo.
|
|
35
|
+
2. Loads the env's SSH private key and trusts `git.wpengine.com`.
|
|
36
|
+
3. `cp -r apps/wordpress/wp-content /tmp/deploy/wp-content` — only the wp-content tree leaves the monorepo.
|
|
37
|
+
4. Inside `/tmp/deploy`: `git init -b master`, single commit, force-push to `git@git.wpengine.com:<install>.git HEAD:master`.
|
|
38
|
+
5. WP Engine's git endpoint deploys the pushed tree to the install.
|
|
39
|
+
|
|
40
|
+
## Step 1 — Preflight
|
|
41
|
+
|
|
42
|
+
### 1a. Confirm this is a WordPress-on-WP-Engine project
|
|
43
|
+
|
|
44
|
+
Check `.refact-os.json`:
|
|
45
|
+
|
|
46
|
+
- `stack.wordpress` exists.
|
|
47
|
+
- `stack.wordpress.hosting === "wpengine"`.
|
|
48
|
+
- `apps/wordpress/wp-content/` exists.
|
|
49
|
+
|
|
50
|
+
If `stack.wordpress` is missing → stop, this flow is WordPress-specific. If `apps/wordpress/` is missing → delegate to `/refact add codebase wordpress` first. If `hosting` is `kinsta` → use `setup-kinsta-deploy.md` instead.
|
|
51
|
+
|
|
52
|
+
### 1b. Confirm each environment has the fields we need
|
|
53
|
+
|
|
54
|
+
For every `stack.wordpress.environments.<env>` entry, the workflow needs:
|
|
55
|
+
|
|
56
|
+
| Field | Purpose | Source |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `branch` | Which git branch's pushes trigger this env's workflow. | `.refact-os.json` |
|
|
59
|
+
| `install` | WP Engine install name; used in `git@git.wpengine.com:<install>.git`. | `.refact-os.json` |
|
|
60
|
+
| `url` | Public URL (informational; used in the report at Step 3). | `.refact-os.json` |
|
|
61
|
+
| `ssh.user` / `ssh.host` / `ssh.port` / `ssh.path` | Used by the `wp-env` SKILL for staging pulls, not by deploy. Not required here. | `.refact-os.json` |
|
|
62
|
+
|
|
63
|
+
If `branch` or `install` is missing for any env you're about to generate, stop and ask the user to run `/refact init` to fill them in — do not invent defaults.
|
|
64
|
+
|
|
65
|
+
The **non-secret** deploy routing lives in `.refact-os.json`. The SSH **private keys** are GitHub Actions secrets and live only there — never in `.refact-os.json` and never in `agent/AGENTS.md`.
|
|
66
|
+
|
|
67
|
+
### 1c. Confirm with the user
|
|
68
|
+
|
|
69
|
+
Tell the user which workflow files are about to be created — one per env in the stack, e.g.:
|
|
70
|
+
|
|
71
|
+
- `.github/workflows/wordpress-deploy-develop.yml`
|
|
72
|
+
- `.github/workflows/wordpress-deploy-stage.yml`
|
|
73
|
+
- `.github/workflows/wordpress-deploy-main.yml`
|
|
74
|
+
|
|
75
|
+
And list the GitHub Actions secrets they will need to add **before** the workflows can succeed. One private key per env:
|
|
76
|
+
|
|
77
|
+
| Env | Secret name |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `develop` (or whatever maps to the develop install) | `SSH_PRIV_KEY_DEV` |
|
|
80
|
+
| `stage` | `SSH_PRIV_KEY_STG` |
|
|
81
|
+
| `production` (the `main` branch env) | `SSH_PRIV_KEY_PROD` |
|
|
82
|
+
|
|
83
|
+
The matching public keys go in WP Engine's SSH allow-list for each install (User Portal → Sites → `<install>` → SSH gateway).
|
|
84
|
+
|
|
85
|
+
Confirm before writing files.
|
|
86
|
+
|
|
87
|
+
## Step 2 — Create the workflow files
|
|
88
|
+
|
|
89
|
+
GitHub Actions only reads workflows from the **repo root** `/.github/workflows/`. Never place these under `apps/wordpress/.github/` — GitHub will not run them, and they would pollute WP Engine's checkout.
|
|
90
|
+
|
|
91
|
+
Generate one file per env present in `stack.wordpress.environments`. Substitute `<env-key>`, `<env.branch>`, `<env.install>`, and `<SECRET_NAME>` for each.
|
|
92
|
+
|
|
93
|
+
### Template (one file per env)
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
name: <Env-Title> auto-deploy
|
|
97
|
+
on:
|
|
98
|
+
push:
|
|
99
|
+
branches: [<env.branch>]
|
|
100
|
+
paths:
|
|
101
|
+
- 'apps/wordpress/**'
|
|
102
|
+
workflow_dispatch:
|
|
103
|
+
|
|
104
|
+
jobs:
|
|
105
|
+
deploy:
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/checkout@v4
|
|
109
|
+
|
|
110
|
+
- name: Push wp-content to WP Engine (<env-key>)
|
|
111
|
+
run: |
|
|
112
|
+
mkdir -p ~/.ssh
|
|
113
|
+
chmod 700 ~/.ssh
|
|
114
|
+
eval $(ssh-agent -s)
|
|
115
|
+
echo "${{ secrets.<SECRET_NAME> }}" | tr -d '\r' | ssh-add -
|
|
116
|
+
ssh-keyscan git.wpengine.com >> ~/.ssh/known_hosts
|
|
117
|
+
chmod 644 ~/.ssh/known_hosts
|
|
118
|
+
|
|
119
|
+
mkdir -p /tmp/deploy
|
|
120
|
+
cp -r apps/wordpress/wp-content /tmp/deploy/wp-content
|
|
121
|
+
cd /tmp/deploy
|
|
122
|
+
git init -b master
|
|
123
|
+
git config user.email "deploy@github.actions"
|
|
124
|
+
git config user.name "GitHub Actions"
|
|
125
|
+
git add .
|
|
126
|
+
git commit -m "Deploy ${{ github.sha }}"
|
|
127
|
+
git push git@git.wpengine.com:<env.install>.git HEAD:master --force
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Concrete example, for an env keyed `production` with `branch: "main"` and `install: "stlouismagazin"`:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
name: Production auto-deploy
|
|
134
|
+
on:
|
|
135
|
+
push:
|
|
136
|
+
branches: [main]
|
|
137
|
+
paths:
|
|
138
|
+
- 'apps/wordpress/**'
|
|
139
|
+
workflow_dispatch:
|
|
140
|
+
|
|
141
|
+
jobs:
|
|
142
|
+
deploy:
|
|
143
|
+
runs-on: ubuntu-latest
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@v4
|
|
146
|
+
|
|
147
|
+
- name: Push wp-content to WP Engine (production)
|
|
148
|
+
run: |
|
|
149
|
+
mkdir -p ~/.ssh
|
|
150
|
+
chmod 700 ~/.ssh
|
|
151
|
+
eval $(ssh-agent -s)
|
|
152
|
+
echo "${{ secrets.SSH_PRIV_KEY_PROD }}" | tr -d '\r' | ssh-add -
|
|
153
|
+
ssh-keyscan git.wpengine.com >> ~/.ssh/known_hosts
|
|
154
|
+
chmod 644 ~/.ssh/known_hosts
|
|
155
|
+
|
|
156
|
+
mkdir -p /tmp/deploy
|
|
157
|
+
cp -r apps/wordpress/wp-content /tmp/deploy/wp-content
|
|
158
|
+
cd /tmp/deploy
|
|
159
|
+
git init -b master
|
|
160
|
+
git config user.email "deploy@github.actions"
|
|
161
|
+
git config user.name "GitHub Actions"
|
|
162
|
+
git add .
|
|
163
|
+
git commit -m "Deploy ${{ github.sha }}"
|
|
164
|
+
git push git@git.wpengine.com:stlouismagazin.git HEAD:master --force
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Secret name mapping
|
|
168
|
+
|
|
169
|
+
Use the env key (not the branch) to pick the secret:
|
|
170
|
+
|
|
171
|
+
| Env key | Secret name |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `production` | `SSH_PRIV_KEY_PROD` |
|
|
174
|
+
| `stage` / `staging` | `SSH_PRIV_KEY_STG` |
|
|
175
|
+
| `develop` / `development` / `dev` | `SSH_PRIV_KEY_DEV` |
|
|
176
|
+
| Anything else | `SSH_PRIV_KEY_<UPPER_ENV>` (e.g. `preview` → `SSH_PRIV_KEY_PREVIEW`) |
|
|
177
|
+
|
|
178
|
+
WP Engine accepts the same public key across installs, but a separate secret per env is the safer default — rotating one install's key doesn't disturb the others.
|
|
179
|
+
|
|
180
|
+
## Step 3 — Report and hand off
|
|
181
|
+
|
|
182
|
+
Tell the user:
|
|
183
|
+
|
|
184
|
+
1. Which files were created (one per env).
|
|
185
|
+
2. The list of GitHub Actions secrets they must add at **Settings → Secrets and variables → Actions** before the first push. If a secret is missing, the workflow fails at the `ssh-add` step.
|
|
186
|
+
3. The corresponding public keys need to be added to each WP Engine install's SSH gateway (User Portal → site → SSH keys).
|
|
187
|
+
4. The next step: push to the `develop` branch first, watch the run in the Actions tab, then promote `develop → stage → main` via PRs.
|
|
188
|
+
5. That the workflows are **path-filtered** on `apps/wordpress/**`, so edits elsewhere in the monorepo will not trigger a deploy or burn Actions minutes.
|
|
189
|
+
|
|
190
|
+
## Vendor policy
|
|
191
|
+
|
|
192
|
+
**The default for refact-os WordPress projects is: `vendor/` directories are tracked in git.** Each plugin (or mu-plugin) commits its `composer install` output alongside the source.
|
|
193
|
+
|
|
194
|
+
Consequences for this deploy flow:
|
|
195
|
+
|
|
196
|
+
- The deploy workflow **must not** run `composer install`. It `cp -r`s exactly what was committed.
|
|
197
|
+
- The bytes deployed to WP Engine equal the bytes in the merge commit — deploys are deterministic and there's no build step that can drift between environments.
|
|
198
|
+
- New plugin dependencies require a commit that includes the updated `vendor/` tree alongside the `composer.json` change.
|
|
199
|
+
|
|
200
|
+
Override path: a project that wants a build-in-CI flow must (a) add a `composer install --no-dev --optimize-autoloader` step before the `cp -r` here, (b) add the matching `vendor/` ignore rules to the repo's `.gitignore`, and (c) document the deviation in `docs/decisions.md` with a responsible person.
|
|
201
|
+
|
|
202
|
+
## `.gitignore` hard rules
|
|
203
|
+
|
|
204
|
+
WP Engine receives **only** what reaches `/tmp/deploy/wp-content`, which is **only** what's tracked by git under `apps/wordpress/wp-content/`. That means the `.gitignore` files in the repo are the deploy filter, not just a local hygiene tool.
|
|
205
|
+
|
|
206
|
+
- **Never use a root-whitelist `.gitignore` pattern (`/*` followed by `!apps/`, `!docs/`, …)** at the repo root. Whitelists are fragile across branch switches: a branch that doesn't carve out one of the allowed paths can quietly drop tracking on files the deploy depends on. Use blocklist semantics (ignore specific things; track everything else).
|
|
207
|
+
- **Scoped allow-lists belong in `apps/wordpress/.gitignore`**, not at the repo root. That's where the classic "ignore WP core; allow `wp-content/`; inside `wp-content/`, only track our theme + mu-plugin" pattern lives.
|
|
208
|
+
- **Never delete or aggressively rewrite `apps/wordpress/.gitignore`** — it's load-bearing. Without it, the `cp -r` could pick up untracked WordPress core, dump files, or local dev artifacts.
|
|
209
|
+
|
|
210
|
+
## Adding new tracked files under `apps/wordpress/`
|
|
211
|
+
|
|
212
|
+
The nested `apps/wordpress/.gitignore` is the allow-list. New files in `wp-content/mu-plugins/`, `wp-content/plugins/`, or `wp-content/themes/` need an explicit `!` exception or git won't see them — and if git doesn't see them, neither does WP Engine.
|
|
213
|
+
|
|
214
|
+
Verify it works before committing:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git check-ignore -v apps/wordpress/wp-content/mu-plugins/your-new-plugin.php
|
|
218
|
+
# Should print the `!...` rule line, NOT the wildcard rule.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`git status` showing the file as untracked-but-eligible is the green-light.
|
|
222
|
+
|
|
223
|
+
## Smoke-testing the auto-deploy
|
|
224
|
+
|
|
225
|
+
Round-trip test that doesn't touch theme/plugin code:
|
|
226
|
+
|
|
227
|
+
1. On `develop`, create `apps/wordpress/wp-content/mu-plugins/deploy-test.php`:
|
|
228
|
+
|
|
229
|
+
```php
|
|
230
|
+
<?php
|
|
231
|
+
/** Plugin Name: Deploy Test */
|
|
232
|
+
defined('ABSPATH') || exit;
|
|
233
|
+
add_action('wp_footer', function () {
|
|
234
|
+
echo '<div style="position:fixed;bottom:8px;right:8px;background:#222;color:#fff;padding:6px 10px;font:12px monospace;z-index:99999;">deploy ok · ' . esc_html(gmdate('c')) . '</div>';
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
2. Add `!wp-content/mu-plugins/deploy-test.php` to `apps/wordpress/.gitignore`.
|
|
239
|
+
3. Commit and `git push origin develop`.
|
|
240
|
+
4. Watch `https://github.com/<org>/<repo>/actions` → "Development auto-deploy" run.
|
|
241
|
+
5. Visit the dev install URL — bottom-right badge means the tree reached WP Engine.
|
|
242
|
+
6. Revert via a **new commit** (don't amend) — delete the file, remove the gitignore line, push.
|
|
243
|
+
|
|
244
|
+
## Hard rules
|
|
245
|
+
|
|
246
|
+
1. **Force-push (`--force`) is required for the WP Engine endpoint and only the WP Engine endpoint.** WP Engine's git endpoint expects a fresh-init tree and cannot fast-forward against one. Never `--force` to GitHub's `main` (or any other GitHub branch).
|
|
247
|
+
2. **Never delete or aggressively rewrite `apps/wordpress/.gitignore`.** It is the deploy filter (see above).
|
|
248
|
+
3. **Never place workflows under `apps/wordpress/.github/`.** GitHub doesn't run nested workflows. If any exist there, delete them.
|
|
249
|
+
4. **Never push to `main` directly.** Develop and stage get validated first, then `stage → main` via PR.
|
|
250
|
+
5. **Don't bypass the path filter** (e.g. removing `paths: 'apps/wordpress/**'`). Doing so means every README edit redeploys.
|
|
251
|
+
6. **Never run `composer install` (or any other build step) in the deploy workflow** without first changing the vendor policy as described above.
|
|
252
|
+
|
|
253
|
+
## When to stop and ask the user
|
|
254
|
+
|
|
255
|
+
- About to add a path under `apps/wordpress/` and unsure if the nested allow-list covers it → run `git check-ignore -v` first; if it points at the wildcard rule, you need a new `!` line.
|
|
256
|
+
- A workflow run fails → diagnose, don't retry blindly. Common causes: missing secret, public key not in WP Engine's SSH allow-list for that install, wrong `install` name (`.git` path mismatch).
|
|
257
|
+
- Stage deploy works but production doesn't pick up → confirm `branch:` matches the actual production branch, and `<install>` matches the production install name in WP Engine.
|
|
258
|
+
- About to change a workflow to push something other than `apps/wordpress/wp-content/` → that's an architecture change; surface and confirm scope.
|
|
@@ -12,18 +12,19 @@ sub_agents: []
|
|
|
12
12
|
|
|
13
13
|
Use this reference when the user invokes any of:
|
|
14
14
|
|
|
15
|
-
- `/refact wp-env setup` — bring up a fresh local WordPress stack
|
|
15
|
+
- `/refact wp-env setup` — bring up a fresh local WordPress stack and, in the same flow, optionally pull plugins/mu-plugins + DB from staging and set a local domain. Idempotent: each sub-step is verified independently and skipped silently if already met, so re-running on a fully-configured project is a no-op.
|
|
16
16
|
- `/refact wp-env pull` — alias for **pull plugins + mu-plugins + db** (staging → local).
|
|
17
17
|
- `/refact wp-env pull plugins`
|
|
18
18
|
- `/refact wp-env pull mu-plugins`
|
|
19
19
|
- `/refact wp-env pull db`
|
|
20
|
+
- `/refact wp-env pull wp-config` — read staging `wp-config.php` and extract application constants into local config files.
|
|
20
21
|
- `/refact wp-env reset` — destroy containers + volumes and rebuild from scratch.
|
|
21
22
|
- `/refact wp-env domain set <hostname>` — front the local env with a `.local` hostname over HTTPS via Caddy.
|
|
22
23
|
- `/refact wp-env domain clear` — remove the hostname mapping and revert to `http://localhost:8888`.
|
|
23
24
|
|
|
24
25
|
## What this does
|
|
25
26
|
|
|
26
|
-
Manages the local WordPress development stack for engagements scaffolded with `/refact add codebase wordpress`. Local code lives in `apps/wordpress/wp-content/`, which is the same tree the Kinsta deploy workflows push (see [`kinsta-
|
|
27
|
+
Manages the local WordPress development stack for engagements scaffolded with `/refact add codebase wordpress`. Local code lives in `apps/wordpress/wp-content/`, which is the same tree the Kinsta and WP Engine deploy workflows push (see [`setup-kinsta-deploy.md`](../setup-kinsta-deploy/SKILL.md) / [`setup-wpengine-deploy.md`](../setup-wpengine-deploy/SKILL.md)). The local env mirrors staging by pulling plugins, mu-plugins, and the DB over SSH using the routing in `.refact-os.json` › `stack.wordpress.environments.staging`.
|
|
27
28
|
|
|
28
29
|
## Canonical layout
|
|
29
30
|
|
|
@@ -55,7 +56,7 @@ The hostname is stored at `.refact-os.json` › `wpEnv.localDomain` so every tea
|
|
|
55
56
|
3. Docker is reachable: `docker info` exits 0. If not, stop and tell the user to start Docker Desktop / Colima.
|
|
56
57
|
4. Node is **18+**: `node --version`. wp-env requires it.
|
|
57
58
|
|
|
58
|
-
For `pull` flows only — also verify
|
|
59
|
+
For `pull` flows only — also verify `.refact-os.json` › `stack.wordpress.environments.staging` has its `ssh` block (user/host/port/path) and `url` filled. If the `ssh` block is missing or any of its fields are absent, stop and tell the user to run `/refact init` first — the deploy/SSH source of truth is `.refact-os.json`, not `agent/AGENTS.md`.
|
|
59
60
|
|
|
60
61
|
---
|
|
61
62
|
|
|
@@ -149,23 +150,60 @@ Print to the user:
|
|
|
149
150
|
|
|
150
151
|
If a domain is configured but Caddy isn't running yet, also remind the user to run `/refact wp-env domain set <hostname>` (or, if already set, `caddy start --config ~/.refact/Caddyfile` to bring the proxy up).
|
|
151
152
|
|
|
153
|
+
### 1g. One-shot post-start checklist
|
|
154
|
+
|
|
155
|
+
The container is up. Walk this **idempotent checklist** so the user gets to a usable, populated local site in one command. Each item is independent: verify the condition, skip silently if already met, otherwise ask the user a single yes/no and delegate to the documented sub-flow. Re-running `/refact wp-env setup` on a fully-configured project does nothing.
|
|
156
|
+
|
|
157
|
+
The values prompted for here persist into `.refact-os.json`, so the next teammate who clones the repo and runs `/refact wp-env setup` is asked **none** of them.
|
|
158
|
+
|
|
159
|
+
#### Checklist
|
|
160
|
+
|
|
161
|
+
1. **Pull plugins + mu-plugins from staging.**
|
|
162
|
+
- Skip silently if `apps/wordpress/wp-content/plugins/` contains any directory other than `.gitkeep` (i.e. there's already at least one plugin checked in or pulled).
|
|
163
|
+
- Otherwise ask: *"Pull plugins and mu-plugins from staging now? [Y/n]"*. On yes, run Step 2b (`pull plugins`) followed by Step 2c (`pull mu-plugins`). On no, mark as user-deferred and continue.
|
|
164
|
+
- Preflight is shared: if Step 2a's SSH check fails, surface and stop the checklist here.
|
|
165
|
+
|
|
166
|
+
2. **Pull staging DB.**
|
|
167
|
+
- Skip silently if `npm run wp:cli -- option get siteurl` returns anything other than wp-env's default (`http://localhost:8888` or `https://localhost:8888`). Any non-default value means the DB has already been imported.
|
|
168
|
+
- Otherwise ask: *"Pull the staging database now? This rewrites local URLs from `<STAGING_URL>` → `<LOCAL_URL>` and resets the admin password. [Y/n]"*. On yes, run Step 2d (`pull db`). On no, mark as user-deferred.
|
|
169
|
+
|
|
170
|
+
3. **Set a local domain.**
|
|
171
|
+
- Skip silently if `.refact-os.json` › `wpEnv.localDomain` is already set. (If it is set but Caddy isn't running, the message printed in 1f covers that.)
|
|
172
|
+
- Otherwise ask: *"Front the local stack with a `.local` hostname over HTTPS (recommended)? Leave blank to keep `http://localhost:8888`."*. If the user provides a hostname, run Step 3b (`domain set <hostname>`). If they leave it blank or decline, persist nothing (so the next teammate gets the same prompt — they may want a domain even if this user doesn't).
|
|
173
|
+
|
|
174
|
+
#### Output
|
|
175
|
+
|
|
176
|
+
After the checklist, print a single summary, in the same shape `setup-project` uses:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
wp-env setup checklist:
|
|
180
|
+
[x] containers up (wp-env start)
|
|
181
|
+
[x] plugins + mu-plugins pulled (or [-] deferred / [-] already populated)
|
|
182
|
+
[x] staging DB imported (or [-] deferred / [-] already populated)
|
|
183
|
+
[x] local domain set (or [-] deferred / [-] not requested)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use `[x]` for met, `[-]` for user-deferred or already-populated (don't re-prompt next run unless the user re-asks), and `[ ]` only for items that failed mid-step (with the suggested next action).
|
|
187
|
+
|
|
152
188
|
---
|
|
153
189
|
|
|
154
190
|
## Step 2 — `pull`
|
|
155
191
|
|
|
156
192
|
`/refact wp-env pull` runs **2b → 2c → 2d** in order. The single-target variants (`pull plugins` / `pull mu-plugins` / `pull db`) run just that step. All variants share the preflight in 2a.
|
|
157
193
|
|
|
158
|
-
### 2a. Resolve SSH target from
|
|
194
|
+
### 2a. Resolve SSH target from `.refact-os.json`
|
|
159
195
|
|
|
160
|
-
|
|
196
|
+
Read `.refact-os.json` › `stack.wordpress.environments.staging`. (For projects whose staging env is keyed differently — e.g. `stage` — fall back to that key; never default to `production`.) Map the fields:
|
|
161
197
|
|
|
162
|
-
|
|
|
198
|
+
| `.refact-os.json` path | Variable |
|
|
163
199
|
|---|---|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
200
|
+
| `…environments.staging.ssh.user` | `SSH_USER` |
|
|
201
|
+
| `…environments.staging.ssh.host` | `SSH_HOST` |
|
|
202
|
+
| `…environments.staging.ssh.port` | `SSH_PORT` |
|
|
203
|
+
| `…environments.staging.ssh.path` | `DOC_ROOT` (the WordPress install root, relative to the SSH user's home; e.g. `sites/<install>` on WP Engine, `/www/<dir>/public` on Kinsta) |
|
|
204
|
+
| `…environments.staging.url` | `STAGING_URL` |
|
|
205
|
+
|
|
206
|
+
If `stack.wordpress.environments.staging.ssh` is absent (or any of `user`/`host`/`port`/`path` is missing), stop and tell the user to run `/refact init` to fill it. Do **not** fall back to parsing `agent/AGENTS.md` — `.refact-os.json` is the single source of truth.
|
|
169
207
|
|
|
170
208
|
Build the SSH connection string:
|
|
171
209
|
|
|
@@ -198,24 +236,49 @@ Notes:
|
|
|
198
236
|
|
|
199
237
|
- `--delete` mirrors staging exactly. Warn the user once: "this removes any local-only plugins under `apps/wordpress/wp-content/plugins/`. Confirm?" If they decline, drop `--delete`.
|
|
200
238
|
- Some hosts inject plugins that don't belong in the repo (e.g. `kinsta-mu-plugins` lives in `mu-plugins/`, not here, so it shouldn't appear; but check). Don't auto-exclude anything beyond `index.php` without asking.
|
|
201
|
-
- After the rsync, remind the user to inspect `git status` for new tracked paths in `apps/wordpress/wp-content/plugins/`. If something new should reach
|
|
239
|
+
- After the rsync, remind the user to inspect `git status` for new tracked paths in `apps/wordpress/wp-content/plugins/`. If something new should reach the host on deploy, the nested `apps/wordpress/.gitignore` needs an explicit `!` exception — see the project's deploy skill (`setup-kinsta-deploy` or `setup-wpengine-deploy`) § "Adding new tracked files under `apps/wordpress/`".
|
|
202
240
|
|
|
203
241
|
### 2c. Pull `mu-plugins`
|
|
204
242
|
|
|
243
|
+
Build the rsync exclude list from two sources: local-only files that must never be deleted by `--delete`, and host-injected system mu-plugins that aren't useful locally. Read `hosting` from `.refact-os.json` › `stack.wordpress.hosting` to select the right set.
|
|
244
|
+
|
|
245
|
+
**Always exclude (local-only wp-env helpers):**
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
--exclude='index.php'
|
|
249
|
+
--exclude='00-wp-env-local-url.php'
|
|
250
|
+
--exclude='01-wp-env-local-config.php'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Host-specific excludes:**
|
|
254
|
+
|
|
255
|
+
| Hosting | Exclude patterns |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `kinsta` | `kinsta-mu-plugins/`, `kinsta-mu-plugins.php` |
|
|
258
|
+
| `wpengine` | `mu-plugin.php`, `force-strong-passwords/`, `slt-force-strong-passwords.php`, `wpe-cache-plugin*`, `wpe-update-source-selector*`, `wpe-wp-sign-on-plugin*`, `wpengine-common/`, `wpengine-security-auditor.php` |
|
|
259
|
+
| Other | Ask the user if unrecognized system mu-plugins are detected |
|
|
260
|
+
|
|
261
|
+
Example for a WP Engine project:
|
|
262
|
+
|
|
205
263
|
```bash
|
|
206
264
|
rsync -avz --delete \
|
|
207
265
|
-e "ssh ${SSH_OPTS}" \
|
|
208
266
|
--exclude='index.php' \
|
|
209
267
|
--exclude='00-wp-env-local-url.php' \
|
|
210
|
-
--exclude='
|
|
211
|
-
--exclude='
|
|
268
|
+
--exclude='01-wp-env-local-config.php' \
|
|
269
|
+
--exclude='mu-plugin.php' \
|
|
270
|
+
--exclude='force-strong-passwords/' \
|
|
271
|
+
--exclude='slt-force-strong-passwords.php' \
|
|
272
|
+
--exclude='wpe-cache-plugin*' \
|
|
273
|
+
--exclude='wpe-update-source-selector*' \
|
|
274
|
+
--exclude='wpe-wp-sign-on-plugin*' \
|
|
275
|
+
--exclude='wpengine-common/' \
|
|
276
|
+
--exclude='wpengine-security-auditor.php' \
|
|
212
277
|
"${SSH_TARGET}:${DOC_ROOT}/wp-content/mu-plugins/" \
|
|
213
278
|
apps/wordpress/wp-content/mu-plugins/
|
|
214
279
|
```
|
|
215
280
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`00-wp-env-local-url.php` is a local-only helper created by `domain set`; keep excluding it so `pull mu-plugins --delete` doesn't remove the local Caddy URL fix.
|
|
281
|
+
These host-injected mu-plugins are managed server-side; they aren't useful locally and shouldn't end up in git. If you spot an unrecognized system mu-plugin (owned by `root` or `nobody`, or matching a known hosting vendor pattern), ask the user before adding it to the exclude list.
|
|
219
282
|
|
|
220
283
|
### 2d. Pull `db`
|
|
221
284
|
|
|
@@ -223,21 +286,32 @@ The local stack **must be running** for this step. If `npx wp-env run cli wp cor
|
|
|
223
286
|
|
|
224
287
|
Resolve `LOCAL_URL` the same way Step 1b does: `https://<wpEnv.localDomain>` if it's set in `.refact-os.json`, otherwise `http://localhost:8888`.
|
|
225
288
|
|
|
289
|
+
Use a two-step dump-to-file approach. Piping SSH export directly into `wp-env run cli wp db import -` can stall on databases larger than ~500 MB because Docker's stdin buffering saturates before the import catches up.
|
|
290
|
+
|
|
226
291
|
```bash
|
|
227
|
-
# 1. Dump staging DB
|
|
292
|
+
# 1. Dump staging DB to a local file.
|
|
293
|
+
mkdir -p .wp-env-dumps
|
|
228
294
|
ssh ${SSH_OPTS} "${SSH_TARGET}" \
|
|
229
295
|
"cd '${DOC_ROOT}' && wp db export --single-transaction -" \
|
|
230
|
-
|
|
296
|
+
> .wp-env-dumps/staging.sql
|
|
297
|
+
|
|
298
|
+
# 2. Copy the dump into the wp-env container and import.
|
|
299
|
+
CONTAINER=$(docker ps --filter "name=cli" --format '{{.Names}}' | grep wp-env | head -1)
|
|
300
|
+
docker cp .wp-env-dumps/staging.sql "${CONTAINER}:/tmp/staging.sql"
|
|
301
|
+
npx wp-env run cli wp db import /tmp/staging.sql
|
|
231
302
|
|
|
232
|
-
#
|
|
303
|
+
# 3. Rewrite URLs from staging → local.
|
|
233
304
|
npx wp-env run cli wp search-replace "${STAGING_URL}" "${LOCAL_URL}" \
|
|
234
305
|
--skip-columns=guid --all-tables
|
|
235
306
|
|
|
236
307
|
# If the staging URL in wp_options has no trailing slash, make sure home/siteurl land exactly.
|
|
237
308
|
npx wp-env run cli wp db query "UPDATE wp_options SET option_value='${LOCAL_URL}' WHERE option_name IN ('home','siteurl')"
|
|
238
309
|
|
|
239
|
-
#
|
|
310
|
+
# 4. Flush caches.
|
|
240
311
|
npx wp-env run cli wp cache flush
|
|
312
|
+
|
|
313
|
+
# 5. Clean up the dump (gitignored path, but no reason to keep ~1 GB on disk).
|
|
314
|
+
rm -f .wp-env-dumps/staging.sql
|
|
241
315
|
```
|
|
242
316
|
|
|
243
317
|
Print `STAGING_URL → LOCAL_URL` before running `search-replace` and ask the user to confirm. A bad replace can rewrite half the DB to the wrong host and is painful to undo.
|
|
@@ -257,6 +331,64 @@ After import:
|
|
|
257
331
|
|
|
258
332
|
If the staging host has no `wp-cli` on `$PATH`, **stop** rather than silently falling back — surface the error and the user can decide whether to install wp-cli on the server or pull a manual `mysqldump`. Don't invent a fallback in this flow.
|
|
259
333
|
|
|
334
|
+
### 2e. Pull `wp-config`
|
|
335
|
+
|
|
336
|
+
Reads the staging `wp-config.php` via SSH and extracts application-level constants into the local environment. Run this after `pull db` so the local stack has the same constants the staging code expects.
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
ssh ${SSH_OPTS} "${SSH_TARGET}" "cat '${DOC_ROOT}/wp-config.php'"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Classify each `define()` in the file into one of four buckets:
|
|
343
|
+
|
|
344
|
+
**Skip (infrastructure — handled by wp-env or the host):**
|
|
345
|
+
|
|
346
|
+
- Database: `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_HOST_SLAVE`, `DB_CHARSET`, `DB_COLLATE`, `$table_prefix`
|
|
347
|
+
- Salts/keys: `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT`
|
|
348
|
+
- WordPress core paths: `ABSPATH`, `WP_CACHE`, `WPLANG`, `WP_AUTO_UPDATE_CORE`
|
|
349
|
+
- Host-injected (read `hosting` from `.refact-os.json` › `stack.wordpress.hosting`):
|
|
350
|
+
- **WP Engine**: `WPE_*`, `PWP_NAME`, `FS_METHOD`, `FS_CHMOD_*`, `WPE_SFTP_*`, `WPE_CDN_*`, `DISALLOW_FILE_*`, `DISABLE_WP_CRON`, `FORCE_SSL_LOGIN`, `WPE_FORCE_SSL_LOGIN`, `WP_POST_REVISIONS`, `WP_TURN_OFF_ADMIN_BAR`, `WPE_BETA_TESTER`, `WPE_WHITELABEL`, `WPE_EXTERNAL_URL`, `$wpe_*`, `$memcached_servers`
|
|
351
|
+
- **Kinsta**: `KINSTA_*`, `WP_CACHE_KEY_SALT`
|
|
352
|
+
- Debug flags: `WP_DEBUG`, `WP_DEBUG_LOG`, `WP_DEBUG_DISPLAY` (already set in `.wp-env.json`)
|
|
353
|
+
|
|
354
|
+
**Scalar, non-secret → `.wp-env.json` `config` (committed):**
|
|
355
|
+
|
|
356
|
+
Application constants with no secret value: version strings, import/feature IDs, memory limits, email addresses. Examples: `SLM_CORE_VERSION`, `CONTENT_IMPORT_ID`, `WP_MEMORY_LIMIT`, `WP_MAX_MEMORY_LIMIT`.
|
|
357
|
+
|
|
358
|
+
Add these to the existing `config` object in `.wp-env.json`. Don't overwrite values already present.
|
|
359
|
+
|
|
360
|
+
**Scalar, secret → `.wp-env.override.json` `config` (gitignored):**
|
|
361
|
+
|
|
362
|
+
API keys, encryption keys, tokens. Examples: `SLM_AKISMET_API_KEY`, `WP2FA_ENCRYPT_KEY`.
|
|
363
|
+
|
|
364
|
+
Create `.wp-env.override.json` if it doesn't exist (`{ "config": { … } }`). This file must be in the root `.gitignore`.
|
|
365
|
+
|
|
366
|
+
**Complex (PHP arrays) → local mu-plugin (gitignored):**
|
|
367
|
+
|
|
368
|
+
wp-env `config` only supports scalar values. PHP arrays (e.g. service-account credential blobs) go into `apps/wordpress/wp-content/mu-plugins/01-wp-env-local-config.php`, guarded by:
|
|
369
|
+
|
|
370
|
+
```php
|
|
371
|
+
if ( defined( 'WP_ENVIRONMENT_TYPE' ) && 'local' === WP_ENVIRONMENT_TYPE ) {
|
|
372
|
+
if ( ! defined( 'SOME_CONSTANT' ) ) {
|
|
373
|
+
define( 'SOME_CONSTANT', array( … ) );
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
This file must be in `apps/wordpress/.gitignore`.
|
|
379
|
+
|
|
380
|
+
**After writing all files:**
|
|
381
|
+
|
|
382
|
+
1. Restart wp-env: `npx wp-env start` (picks up `.wp-env.json` and override changes).
|
|
383
|
+
2. Verify: `npx wp-env run cli wp eval "echo defined('CONSTANT_NAME') ? 'yes' : 'no';"` for a sample of the extracted constants.
|
|
384
|
+
3. If a previously-crashed plugin depended on a missing constant (common: GA4/analytics credentials), re-activate it: `npx wp-env run cli wp plugin activate <plugin-slug>`.
|
|
385
|
+
|
|
386
|
+
**Guardrails:**
|
|
387
|
+
|
|
388
|
+
- Never commit secrets. `.wp-env.override.json` and `01-wp-env-local-config.php` must always be gitignored.
|
|
389
|
+
- Never copy the staging DB credentials, salts, or host-specific constants into local files — they're meaningless locally and risk leaking if committed.
|
|
390
|
+
- Show the user the proposed classification (which constants go where) before writing, so they can override the bucket for edge cases.
|
|
391
|
+
|
|
260
392
|
---
|
|
261
393
|
|
|
262
394
|
## Step 3 — `domain set <hostname>` / `domain clear`
|
|
@@ -464,7 +596,7 @@ After reset, the user typically wants `/refact wp-env pull db` to restore stagin
|
|
|
464
596
|
- **Never run `search-replace` without confirming the URLs.** A bad replace can rewrite half the DB to the wrong host and is painful to undo. Always print the source and target URL once before executing.
|
|
465
597
|
- **Never `wp-env destroy` without confirmation.** It nukes the local DB. `reset` confirms; ad-hoc destroy elsewhere should too.
|
|
466
598
|
- **Never invent a fallback** when staging's wp-cli or rsync isn't available. Surface the error, let the user decide.
|
|
467
|
-
- **Never edit `agent/AGENTS.md` to fill missing SSH fields from this flow.** Send the user to `/refact init` so
|
|
599
|
+
- **Never edit `agent/AGENTS.md` (or any other file) to fill missing SSH fields from this flow.** Send the user to `/refact init` so `.refact-os.json` › `stack.wordpress.environments.<env>.ssh` is filled — that's the single source of truth.
|
|
468
600
|
- **Never commit pulled DB dumps.** If you write a `.sql` file at any point during this flow, place it in a gitignored path (e.g. `./.wp-env-dumps/`) and delete it after import.
|
|
469
601
|
- **Never write Caddyfiles into the project tree.** Per-project site blocks live under `~/.refact/caddy/`. Project-local files would either bind-collide with another project's Caddy instance or end up committed by accident.
|
|
470
602
|
- **Never accept a public-TLD hostname** for `domain set` (`.com`, `.io`, etc.) without explicit user confirmation — Caddy would try Let's Encrypt and either fail or, worse, attempt ACME against a domain the user doesn't control.
|