Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
18e2c06
Add spec
sirreal May 8, 2026
f49e45c
plan v2
sirreal May 8, 2026
b1cfbca
refine spec
sirreal May 8, 2026
787619e
spec: refine deps ownership
sirreal May 8, 2026
07e56fc
spec: apply review changes
sirreal May 8, 2026
1bf5b2d
spec: simplify and clenaup
sirreal May 8, 2026
5c8ff12
spec: engineering improvements
sirreal May 8, 2026
ad48964
spec: support Windows
sirreal May 8, 2026
df07079
spec: tighten
sirreal May 8, 2026
28d6051
plan: add envlite implementation plan
sirreal May 8, 2026
3d25340
feat(envlite): scaffold CLI dispatch and test harness
sirreal May 8, 2026
07dbd41
feat(envlite): add diagnostic logging helpers
sirreal May 8, 2026
c20963b
feat(envlite): add POSIX path utilities
sirreal May 8, 2026
d7db520
feat(envlite): add manifest read/write
sirreal May 8, 2026
a79ecbe
feat(envlite): add atomic file write helper
sirreal May 8, 2026
34b3f23
feat(envlite): add manifest-anchored ownership decisions
sirreal May 8, 2026
128738f
feat(envlite): add interactive prompt helper
sirreal May 8, 2026
5fa842c
feat(envlite): implement Phase 0 preflight
sirreal May 8, 2026
2ad62f0
feat(envlite): implement Phase 1 port discovery
sirreal May 8, 2026
b5cab27
feat(envlite): add subprocess helper and refactor Phase 0
sirreal May 8, 2026
68805e3
feat(envlite): implement Phases 2/3/4 (npm ci, build:dev, composer in…
sirreal May 8, 2026
6edeff8
feat(envlite): implement Phase 5 SQLite drop-in
sirreal May 8, 2026
7d085ae
feat(envlite): implement Phase 6 wp-tests-config.php
sirreal May 8, 2026
70599ed
feat(envlite): implement Phase 7 src/wp-config.php
sirreal May 8, 2026
2ba1ebc
feat(envlite): implement Phase 8 router.php
sirreal May 8, 2026
4a89271
feat(envlite): orchestrate init across all phases
sirreal May 8, 2026
71df4c4
feat(envlite): implement serve subcommand
sirreal May 8, 2026
a521f9b
feat(envlite): implement clean subcommand
sirreal May 8, 2026
7e5fc22
test(envlite): add end-to-end smoke covering phases 5-8 and clean
sirreal May 8, 2026
bcd9244
Use nowdoc for envlite help text
sirreal May 8, 2026
bd9f031
Ignore the .envlite dir
sirreal May 8, 2026
73b83c4
Add npm script
sirreal May 8, 2026
13e3178
Build/Test Tools: Ignore the `.envlite/` state directory.
lucatume May 8, 2026
66a909e
fix(envlite): address PR #35 review feedback
sirreal May 8, 2026
a93a186
Update spec to move router.php
sirreal May 8, 2026
b52fe13
refactor(envlite): ship router.php as a committed tool asset
sirreal May 8, 2026
92bed7e
feat(envlite): install WordPress on `init` (Phase 8)
sirreal May 8, 2026
32c0fa6
Document 127.0.0.1 site URL reasoning
sirreal May 8, 2026
032a376
feat(envlite): add `up` subcommand (init + foreground serve)
sirreal May 8, 2026
afdb51a
plan process swapping on serve work
sirreal May 8, 2026
9b6d6db
feat(envlite): require pcntl extension on Unix in Phase 0
sirreal May 8, 2026
195b063
feat(envlite): add envlite_run_dev_server helper with pcntl on Unix
sirreal May 8, 2026
84c6b9f
test(envlite): cover pcntl process replacement and proc_open fallback
sirreal May 9, 2026
d9b11cf
refactor(envlite): route serve through envlite_run_dev_server
sirreal May 9, 2026
18d29c2
refactor(envlite): route up through envlite_run_dev_server
sirreal May 9, 2026
c51fc83
docs(envlite): document pcntl dev-server launch and Windows fallback
sirreal May 9, 2026
cce15b4
docs(envlite): tighten bind-failure prose and pcntl phrasing
sirreal May 9, 2026
b7f2a80
refactor(envlite): fail loudly on Unix without pcntl; clarify test name
sirreal May 9, 2026
7ffbdd1
Allow arbitrary specified port
sirreal May 9, 2026
a40351e
Clarify gitignore spec, remove redundant gitignore entry
sirreal May 9, 2026
de7b5ab
Spec: Remove excessive detail for BOM, line-endings
sirreal May 9, 2026
9a00c6a
docs(envlite): design for phpunit test DB isolation
sirreal May 9, 2026
836eab7
docs(envlite): implementation plan for test DB isolation
sirreal May 9, 2026
fe9d7d6
feat(envlite): assert DB_FILE absent from wp-tests-config sample
sirreal May 9, 2026
4258d9b
feat(envlite): isolate phpunit DB by appending DB_FILE to wp-tests-co…
sirreal May 9, 2026
ce1f151
docs(envlite): record DB_FILE isolation in Phase 6 spec
sirreal May 9, 2026
4267889
docs(envlite): fix regex rendering in Phase 6 spec
sirreal May 9, 2026
72f56d6
REmove `--platform-php` from spec
sirreal May 9, 2026
50efdc9
Add README
sirreal May 9, 2026
349aaa9
Simplify README
sirreal May 9, 2026
3a686d9
More readme simplification
sirreal May 9, 2026
93bdf90
Iterate on README
sirreal May 9, 2026
8191a0e
Clean up html-api refs
sirreal May 9, 2026
adb2238
test(envlite): use truly invalid cached port in phase1 test
sirreal May 9, 2026
a5f2b24
fix(envlite): abort Phase 5 when ZipArchive extraction fails
sirreal May 9, 2026
36775d5
fix(envlite): treat fetched salts as literal in Phase 7 render
sirreal May 9, 2026
170fda1
fix(envlite): normalize CRLF in wp-config sample before render
sirreal May 9, 2026
a5180da
fix(envlite): block .ht* paths in router so SQLite DB is not download…
sirreal May 9, 2026
7167afa
fix(envlite): serve directory indexes so /wp-admin/ reaches wp-admin/…
sirreal May 9, 2026
0781adf
fix(envlite): re-pick port when cached port is no longer free
sirreal May 9, 2026
5d05c9b
docs(envlite): design spec for disabling WP-Cron by default in Phase 7
sirreal May 11, 2026
9242044
docs(envlite): implementation plan for disabling WP-Cron in Phase 7
sirreal May 11, 2026
021b5a6
Use multiple workers
sirreal May 12, 2026
28c6332
test(envlite): cover router DOCUMENT_ROOT resolution
sirreal May 13, 2026
07f3550
fix(envlite): resolve router paths via DOCUMENT_ROOT
sirreal May 13, 2026
271d7f6
test(envlite): normalize tmpdir via realpath for macOS portability
sirreal May 13, 2026
fc873b3
docs(envlite): plan for router DOCUMENT_ROOT fix
sirreal May 13, 2026
6548a52
docs(envlite): record macOS realpath() follow-up in plan
sirreal May 13, 2026
1d78461
docs(envlite): update spec for router DOCUMENT_ROOT resolution
sirreal May 13, 2026
2fb9661
feat(envlite): collapse to `up`-only with smart-skip and parallel ins…
sirreal May 14, 2026
95106f6
refactor(envlite): move state directory to .cache/envlite/
sirreal May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,293 changes: 2,293 additions & 0 deletions docs/superpowers/plans/2026-05-08-envlite.md

Large diffs are not rendered by default.

634 changes: 634 additions & 0 deletions docs/superpowers/plans/2026-05-08-pcntl-exec-dev-server.md

Large diffs are not rendered by default.

251 changes: 251 additions & 0 deletions docs/superpowers/plans/2026-05-13-fix-envlite-router-docroot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Fix envlite router path resolution Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make `tools/local-env/router.php` serve the repo where `php -S` was launched, not the repo that owns the router file.

**Architecture:** The PHP built-in server populates `$_SERVER['DOCUMENT_ROOT']` from its `-t` flag (resolved to an absolute path). `envlite_run_dev_server` already chdirs to the target repo and passes `-t src`, so `DOCUMENT_ROOT` always equals `<target-repo>/src`. Replace the router's two `dirname(__DIR__, 2) . '/src'` expressions with `$_SERVER['DOCUMENT_ROOT']` so the router stops assuming it lives inside the target repo.

**Tech Stack:** PHP 7.4+, PHP built-in server (`php -S`), envlite test harness (custom — `tools/local-env/tests/harness.php` + `run.php`).

---

## Bug recap (why this exists)

`router.php:11` and `router.php:25` use `dirname(__DIR__, 2) . '/src'` to locate the document root and front controller. `__DIR__` is the directory of the *router file itself*, so when envlite is invoked from a checkout other than the one that owns `router.php` (e.g. running `php /path/to/envlite-checkout/tools/local-env/envlite.php up` from a different worktree), the router loads the originating checkout's `src/index.php` — pulling in the *wrong* `wp-config.php`, which then triggers a WordPress canonical-URL 301 to whatever port that wp-config defines.

Reproduction observed: server bound at `127.0.0.1:8722` (target repo's port), but `GET /` returned `301 Location: http://127.0.0.1:8762/` (originating envlite checkout's wp-config port).

## File Structure

- **Modify:** `tools/local-env/router.php` — replace both `dirname(__DIR__, 2) . '/src'` expressions with `$_SERVER['DOCUMENT_ROOT']`. No structural change; same 25 lines.
- **Create:** `tools/local-env/tests/test_router.php` — integration test that boots `php -S` against a fixture site whose path is unrelated to the router file's location, then asserts the request was served from the fixture (not from envlite's own tree).

The router file is intentionally small and a single responsibility; no extraction is needed.

---

### Task 1: Add the failing regression test

**Files:**
- Create: `tools/local-env/tests/test_router.php`

This test boots a real `php -S` with the shipped `router.php`, pointing `-t` at a tmp fixture directory that contains a tiny `index.php` marker. It then `GET`s `/` and asserts the marker came back — proving the router served the fixture, not the envlite checkout's own `src/`.

Picking a free port: bind to `tcp://127.0.0.1:0`, read the assigned port from `stream_socket_get_name`, close the socket, then hand the port to `php -S`. There's a microscopic race window between close-and-rebind; if it bites in practice, retry once. Don't preemptively engineer for it.

- [ ] **Step 1: Write the failing test**

Create `tools/local-env/tests/test_router.php`:

```php
<?php
function envlite_test_router_pick_free_port(): int {
$sock = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
if ($sock === false) {
throw new \RuntimeException("could not bind to find free port: $errstr");
}
$name = stream_socket_get_name($sock, false);
$port = (int) substr($name, strrpos($name, ':') + 1);
fclose($sock);
return $port;
}

function envlite_test_router_wait_for_bind(int $port, float $timeout_seconds = 3.0): bool {
$deadline = microtime(true) + $timeout_seconds;
while (microtime(true) < $deadline) {
$check = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.1);
if ($check) {
fclose($check);
return true;
}
usleep(100_000);
}
return false;
}

function test_router_serves_from_document_root_not_router_directory() {
// Build a fixture "site" that does NOT share a parent with router.php.
// realpath() normalizes /tmp -> /private/tmp on macOS so the assert
// below matches __DIR__ from the fixture's index.php (which resolves
// symlinks). On Linux this is a no-op.
$site = realpath(envlite_test_tmpdir('router-docroot'));
envlite_assert($site !== false, 'tmp fixture directory must resolve via realpath');
file_put_contents("$site/index.php", "<?php echo 'FIXTURE_OK ' . __DIR__;");

// Use the real shipped router so we exercise its path resolution.
$router = realpath(__DIR__ . '/../router.php');
envlite_assert(is_file($router), 'router.php must exist at ' . __DIR__ . '/../router.php');

$port = envlite_test_router_pick_free_port();

// Spawn `php -S 127.0.0.1:<port> -t <site> <router>` with cwd = site.
// Matches envlite_run_dev_server: chdir into the target repo, then pass
// -t <docroot>. The router file lives outside $site on purpose — that is
// exactly the configuration that triggered the original bug.
$argv = [PHP_BINARY, '-S', "127.0.0.1:$port", '-t', $site, $router];
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open($argv, $descriptors, $pipes, $site);
envlite_assert(is_resource($proc), 'failed to start php -S');

try {
envlite_assert(
envlite_test_router_wait_for_bind($port),
"php -S did not bind on 127.0.0.1:$port within 3s"
);

$body = @file_get_contents("http://127.0.0.1:$port/");
envlite_assert($body !== false, "request to 127.0.0.1:$port failed");

envlite_assert(
strpos($body, 'FIXTURE_OK ' . $site) !== false,
'expected FIXTURE_OK marker from fixture index.php, got: ' . substr($body, 0, 400)
);
} finally {
foreach ($pipes as $p) { if (is_resource($p)) { @fclose($p); } }
$status = @proc_get_status($proc);
if ($status && $status['running']) {
@proc_terminate($proc, 15);
}
@proc_close($proc);
}
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php tools/local-env/tests/run.php`

Expected: `FAIL test_router_serves_from_document_root_not_router_directory` with a message about the FIXTURE_OK marker being absent. The body will either be empty (require failed — index.php from envlite's own `src/` does not exist in a fresh checkout running tests) or contain unrelated content from envlite's `src/index.php`. Either way the assert fires.

If instead the test passes here, STOP — the reproduction is wrong and the rest of the plan does nothing.

- [ ] **Step 3: Commit the failing test**

```bash
git add tools/local-env/tests/test_router.php
git commit -m "test(envlite): cover router DOCUMENT_ROOT resolution"
```

It is fine — and intentional — to land a failing regression test in its own commit. The next commit makes it pass.

---

### Task 2: Fix the router to use DOCUMENT_ROOT

**Files:**
- Modify: `tools/local-env/router.php:11` and `tools/local-env/router.php:25`

- [ ] **Step 1: Edit router.php**

Replace the current contents of `tools/local-env/router.php` with:

```php
<?php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// php -S does not honor Apache .ht* deny rules. Block any segment so the
// SQLite DB at wp-content/database/.ht.sqlite is not downloadable.
if (preg_match('#(^|/)\.ht#', $path)) {
http_response_code(403);
return true;
}

// DOCUMENT_ROOT is the absolute resolution of php -S's -t flag. Using it
// instead of a path computed from __DIR__ lets the router live outside the
// target repo (e.g. envlite invoked from a different checkout).
$docroot = $_SERVER['DOCUMENT_ROOT'];
$file = $docroot . $path;

if ($path !== '/' && file_exists($file)) {
if (!is_dir($file)) {
return false;
}
// Existing directory: let the built-in server serve its index.php
// (e.g. /wp-admin/ -> wp-admin/index.php). Without an index, fall
// through to the front controller to avoid directory listings.
if (file_exists(rtrim($file, '/') . '/index.php')) {
return false;
}
}

require $docroot . '/index.php';
```

- [ ] **Step 2: Run the new test to verify it passes**

Run: `php tools/local-env/tests/run.php`

Expected: `PASS test_router_serves_from_document_root_not_router_directory`, and the final tally line should show one more pass than before with zero failures.

- [ ] **Step 3: Run the full test suite to verify no regressions**

Run: `php tools/local-env/tests/run.php`

Expected: `0 failures` in the final summary line. In particular `test_dev_server_argv_targets_correct_port_root_router` must still pass — it asserts the argv shape, which this change does not touch.

- [ ] **Step 4: Manually verify the original reproduction is fixed**

This is a one-time smoke check, not a permanent test. Skip if you do not have two envlite-prepared worktrees handy.

Run:
```bash
# In one terminal, from a *different* envlite-prepared checkout B, start the server:
cd /path/to/checkout-B
php /path/to/checkout-A/tools/local-env/envlite.php up
```

```bash
# In another terminal:
curl -sI http://127.0.0.1:<B's port>/
```

Expected: a `200 OK` (or whatever the WordPress front page returns), NOT a `301` to checkout A's port.

- [ ] **Step 5: Commit the fix**

```bash
git add tools/local-env/router.php
git commit -m "fix(envlite): resolve router paths via DOCUMENT_ROOT

The router previously used dirname(__DIR__, 2) . '/src' to locate both
the static-file root and the front controller. That resolves relative
to the router file's own checkout, so invoking envlite from a different
worktree loaded the wrong wp-config.php and triggered a canonical-URL
301 to that wp-config's WP_HOME port.

Use \$_SERVER['DOCUMENT_ROOT'] instead — populated by php -S from its
-t flag, which envlite_run_dev_server already points at the target
repo's src/."
```

---

## Self-Review

**1. Spec coverage:**
- Root cause (router uses `__DIR__`-derived path): covered by Task 2 Step 1.
- Fix uses `$_SERVER['DOCUMENT_ROOT']`: covered by Task 2 Step 1.
- Regression test exists: covered by Task 1.
- Manual verification of original reproduction: covered by Task 2 Step 4.
- No structural changes to envlite.php: confirmed — `envlite_dev_server_argv` already passes `-t src`, no change needed.

**2. Placeholder scan:** No TBD/TODO/"fill in later". All code is concrete; all commands explicit.

**3. Type/identifier consistency:**
- Helper names `envlite_test_router_pick_free_port` and `envlite_test_router_wait_for_bind` are referenced in the test exactly as defined.
- `envlite_test_tmpdir` is defined in `tests/test_manifest.php:2` and reused here (matching the pattern in `test_smoke.php:3` and `test_atomic.php`).
- `envlite_assert` is defined in `tests/harness.php:2`.
- `proc_open` with `$descriptors` array form, then `proc_terminate`/`proc_close` — standard PHP API.

---

## Post-implementation notes

These deviated from the plan as originally written and are recorded here so future readers don't think the plan and the committed code drifted by accident.

- **macOS tmpdir symlink (commit `271d7f651d`).** Plan v1 wrote `$site = envlite_test_tmpdir('router-docroot')` directly. On macOS `sys_get_temp_dir()` returns `/var/folders/...` while PHP's `__DIR__` in the fixture's `index.php` resolves symlinks to `/private/var/folders/...`, so the `'FIXTURE_OK ' . $site` assertion fails even when the router is serving the fixture correctly. Fix: wrap the tmpdir in `realpath()` and assert it resolved, before the fixture write. The Task 1 code block above has been updated in-place to match what shipped; on Linux the change is a no-op. A more durable fix would be to push the `realpath()` into `envlite_test_tmpdir` itself so every test that compares tmp paths against `__DIR__`-resolved values is portable by default — deferred until a second caller needs it.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"lint:jsdoc": "wp-scripts lint-js",
"lint:jsdoc:fix": "wp-scripts lint-js --fix",
"typecheck:js": "tsc --build",
"envlite": "php ./tools/local-env/envlite.php",
"env:start": "node ./tools/local-env/scripts/start.js && node ./tools/local-env/scripts/docker.js run -T --rm php composer update -W",
"env:stop": "node ./tools/local-env/scripts/docker.js down",
"env:restart": "npm run env:stop && npm run env:start",
Expand Down
Loading
Loading