@wp-operations/wp-app 0.3.0 → 0.4.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.
Files changed (4) hide show
  1. package/README.md +20 -33
  2. package/dist/index.js +150 -71
  3. package/dist/main.js +699 -282
  4. package/package.json +5 -6
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @wp-operations/wp-app
2
2
 
3
- **The `wp-now` revival.** A zero-config local WordPress dev server on `@php-wasm/node`: no Docker, no MySQL, no XAMPP. Point it at a theme, a plugin, or a full core checkout and it starts.
4
-
5
- When `wp-now` was deprecated in favor of the blueprint-centric Playground CLI, the simple `wp-now start` workflow went with it. `wp-app` brings it back — same one-command experience, running on the **current** php-wasm engine (PHP 7.4 – 8.5).
3
+ A zero-config local WordPress dev server on `@php-wasm/node`: no Docker, no MySQL, no XAMPP. Point it at a theme, a plugin, a full core checkout, or a wp-app project, and it starts.
6
4
 
7
5
  ## Install
8
6
 
@@ -15,48 +13,37 @@ Requires Node.js >= 20.10.
15
13
  ## Usage
16
14
 
17
15
  ```bash
18
- wp-app start # auto-detects project type from cwd
19
- wp-app start --path=my-theme # explicit path
20
- wp-app start --php=8.3 --wp=6.8 # pin PHP / WordPress versions
21
- wp-app start --wp=classicpress # run ClassicPress instead (lightweight, no Gutenberg)
22
- wp-app start --port=8881 --reset # fresh site (wipes the managed WordPress + database)
23
- wp-app dev # start + restart on file changes
24
- wp-app build # composer install + npm build script, when present
25
- wp-app php -- -r 'echo PHP_VERSION;' # PHP CLI via php-wasm
26
- wp-app php -- script.php # run a PHP file
16
+ wp-app start
17
+ wp-app start --path=my-app
18
+ wp-app start --php=8.3 --wp=6.8
19
+ wp-app start --wp=classicpress
20
+ wp-app start --port=8881 --reset
21
+ wp-app dev
22
+ wp-app build
23
+ wp-app install
24
+ wp-app update
25
+ wp-app create my-app
26
+ wp-app php -- -r 'echo PHP_VERSION;'
27
+ wp-app php -- script.php
27
28
  ```
28
29
 
29
30
  Login: `admin` / `password`.
30
31
 
31
- ## How data is stored
32
-
33
- - Core downloads are cached once in `~/.wp-app/cache/`
34
- - Each project gets a full persistent WordPress site in `~/.wp-app/sites/<project>-<hash>/` — files and SQLite database survive restarts; `--reset` deletes exactly this directory and never your project
35
- - Your project directory is mounted *into* that site (as a theme, plugin, or wp-content overlay) — edits are live without restarts
36
- - Set the `WP_APP_HOME` environment variable to relocate all of the above (CI, portable drives, or the `.local` repo convention via `@wp-operations/wp-ide`)
37
-
38
32
  ## Modes
39
33
 
40
- Auto-detected from the project directory, or forced with `--mode`:
34
+ Auto-detected from the project directory, or forced with `--mode`. Checked in this order — first match wins:
41
35
 
42
36
  | Mode | Detected when | What runs |
43
37
  |------|--------------|-----------|
44
- | `theme` | `style.css` with a Theme Name header | Managed WP + your directory mounted as a theme, auto-activated |
45
- | `plugin` | PHP file with a Plugin Name header | Managed WP + your directory mounted as a plugin, auto-activated |
38
+ | `wp-app` | `composer.json` with `"type": "wp-app"` | `wp-app` |
39
+ | `wordpress` | Full WordPress core checkout | Your core served directly, SQLite-backed |
40
+ | `classicpress` | Full ClassicPress core checkout | Your core served directly, SQLite-backed |
46
41
  | `wp-content` | `plugins/` + `themes/` dirs | Managed WP + your entries overlaid on `wp-content` |
47
- | `wordpress` | Full core checkout | Your core served directly, SQLite-backed |
48
- | `wordpress-develop` | Core dev checkout | The `build/` directory |
42
+ | `plugin` | PHP file with a Plugin Name header | Managed WP + your directory mounted as a plugin, auto-activated |
43
+ | `theme` | `style.css` with a Theme Name header | Managed WP + your directory mounted as a theme, auto-activated |
49
44
  | `index` | An `index.php` | Plain PHP server, no WordPress |
50
- | `playground` | None of the above (e.g. empty dir) | Fresh managed install |
51
-
52
- ## ClassicPress
53
-
54
- `--wp=classicpress` fetches the latest ClassicPress release; `--wp=classicpress-2.4.1` pins a version. Everything else (modes, SQLite, mounts) works identically.
55
-
56
- ## Part of wp-operations
57
45
 
58
- `wp-app` is the foundation of [wp-operations](https://github.com/wordpress-operations/wordpress-operations) — an open-source operations toolchain for classic WordPress development: PSR-4 vendor packages as plugins, an AI skills library with security guidelines, monorepo tooling (`@wp-operations/wp-ide`), and a unified operations CLI (`@wp-operations/wp-ops`).
59
46
 
60
- ## Credits & License
47
+ ## License
61
48
 
62
49
  GPL-2.0-or-later. Continues the workflow of [`@wp-now/wp-now`](https://github.com/WordPress/playground-tools) by the WordPress contributors, rebuilt on `@php-wasm/*`.
package/dist/index.js CHANGED
@@ -10,8 +10,8 @@ function getCodeSpaceURL(port) {
10
10
  }
11
11
 
12
12
  // src/engine.ts
13
- import fs10 from "fs";
14
- import path9 from "path";
13
+ import fs11 from "fs";
14
+ import path10 from "path";
15
15
  import { createNodeFsMountHandler } from "@php-wasm/node";
16
16
 
17
17
  // src/boot.ts
@@ -37,7 +37,18 @@ async function createPhp(phpVersion) {
37
37
  return php;
38
38
  }
39
39
  function createRequestHandler(php, documentRoot, absoluteUrl) {
40
- return new PHPRequestHandler({ php, documentRoot, absoluteUrl });
40
+ return new PHPRequestHandler({
41
+ php,
42
+ documentRoot,
43
+ absoluteUrl,
44
+ // Route unmatched URLs (permalinks, admin-ajax) to index.php, not 404.
45
+ getFileNotFoundAction: () => ({
46
+ type: "internal-redirect",
47
+ uri: "/index.php"
48
+ }),
49
+ // Let Set-Cookie pass through to the real HTTP server (express).
50
+ cookieStore: false
51
+ });
41
52
  }
42
53
  async function runPhp(php, code) {
43
54
  const response = await php.run({ code: `<?php ${code}` });
@@ -140,18 +151,22 @@ function defineSiteConstants(php, siteUrl, databaseDir) {
140
151
  php.defineConstant("DB_DIR", databaseDir);
141
152
  php.defineConstant("DB_FILE", ".ht.sqlite");
142
153
  php.defineConstant("WP_SQLITE_AST_DRIVER", true);
154
+ php.defineConstant("DISABLE_WP_CRON", true);
143
155
  }
144
156
  async function runInstaller(requestHandler) {
157
+ const adminUser = process.env.WP_ADMIN_USER || "admin";
158
+ const adminPassword = process.env.WP_ADMIN_PASSWORD || "password";
159
+ const adminEmail = process.env.WP_ADMIN_EMAIL || "admin@localhost.com";
145
160
  const fields = new URLSearchParams({
146
161
  language: "en",
147
162
  prefix: "wp_",
148
163
  weblog_title: "My WordPress Website",
149
- user_name: "admin",
150
- admin_password: "password",
151
- admin_password2: "password",
164
+ user_name: adminUser,
165
+ admin_password: adminPassword,
166
+ admin_password2: adminPassword,
152
167
  Submit: "Install WordPress",
153
168
  pw_weak: "1",
154
- admin_email: "admin@localhost.com"
169
+ admin_email: adminEmail
155
170
  });
156
171
  return requestHandler.request({
157
172
  url: "/wp-admin/install.php?step=2",
@@ -175,8 +190,8 @@ var DEFAULT_WORDPRESS_VERSION = "latest";
175
190
  var DOCROOT = "/wordpress";
176
191
 
177
192
  // src/download.ts
178
- import fs9 from "fs";
179
- import path8 from "path";
193
+ import fs10 from "fs";
194
+ import path9 from "path";
180
195
 
181
196
  // src/detect/has-index-file.ts
182
197
  import fs from "fs";
@@ -188,7 +203,7 @@ function hasIndexFile(projectPath) {
188
203
  // src/detect/is-valid-wordpress-version.ts
189
204
  function isValidWordPressVersion(version) {
190
205
  const versionPattern = /^latest$|^(?:(\d+)\.(\d+)(?:\.(\d+))?)((?:-beta(?:\d+)?)|(?:-RC(?:\d+)?))?$/;
191
- const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?)?$/;
206
+ const classicPressPattern = /^classicpress(?:-(\d+)\.(\d+)(?:\.(\d+))?(?:-rc(?:\d+)?)?)?$/;
192
207
  return versionPattern.test(version) || classicPressPattern.test(version);
193
208
  }
194
209
  function isClassicPressVersion(version) {
@@ -253,13 +268,29 @@ function isThemeDirectory(projectPath) {
253
268
  return themeNameRegex.test(styleCSS);
254
269
  }
255
270
 
256
- // src/detect/is-wp-content-directory.ts
271
+ // src/detect/is-wp-app-project.ts
257
272
  import fs5 from "fs";
258
273
  import path4 from "path";
274
+ function isWpAppProject(projectPath) {
275
+ const composerJsonPath = path4.join(projectPath, "composer.json");
276
+ if (!fs5.existsSync(composerJsonPath)) {
277
+ return false;
278
+ }
279
+ try {
280
+ const composerJson = JSON.parse(fs5.readFileSync(composerJsonPath, "utf8"));
281
+ return composerJson.type === "wp-app";
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ // src/detect/is-wp-content-directory.ts
288
+ import fs6 from "fs";
289
+ import path5 from "path";
259
290
  function isWpContentDirectory(projectPath) {
260
- const muPluginsExists = fs5.existsSync(path4.join(projectPath, "mu-plugins"));
261
- const pluginsExists = fs5.existsSync(path4.join(projectPath, "plugins"));
262
- const themesExists = fs5.existsSync(path4.join(projectPath, "themes"));
291
+ const muPluginsExists = fs6.existsSync(path5.join(projectPath, "mu-plugins"));
292
+ const pluginsExists = fs6.existsSync(path5.join(projectPath, "plugins"));
293
+ const themesExists = fs6.existsSync(path5.join(projectPath, "themes"));
263
294
  if (muPluginsExists || pluginsExists || themesExists) {
264
295
  return true;
265
296
  }
@@ -267,29 +298,24 @@ function isWpContentDirectory(projectPath) {
267
298
  }
268
299
 
269
300
  // src/detect/is-wordpress-directory.ts
270
- import fs6 from "fs";
271
- import path5 from "path";
301
+ import fs7 from "fs";
302
+ import path6 from "path";
272
303
  function isWordPressDirectory(projectPath) {
273
- return fs6.existsSync(path5.join(projectPath, "wp-content")) && fs6.existsSync(path5.join(projectPath, "wp-includes")) && fs6.existsSync(path5.join(projectPath, "wp-load.php"));
304
+ return fs7.existsSync(path6.join(projectPath, "wp-content")) && fs7.existsSync(path6.join(projectPath, "wp-includes")) && fs7.existsSync(path6.join(projectPath, "wp-load.php"));
274
305
  }
275
306
 
276
- // src/detect/is-wordpress-develop-directory.ts
277
- import fs7 from "fs";
278
- import path6 from "path";
279
- function isWordPressDevelopDirectory(projectPath) {
280
- const requiredFiles = [
281
- "src",
282
- "src/wp-content",
283
- "src/wp-includes",
284
- "src/wp-load.php",
285
- "build",
286
- "build/wp-content",
287
- "build/wp-includes",
288
- "build/wp-load.php"
289
- ];
290
- return requiredFiles.every(
291
- (file) => fs7.existsSync(path6.join(projectPath, file))
292
- );
307
+ // src/detect/is-classicpress-directory.ts
308
+ import fs8 from "fs";
309
+ import path7 from "path";
310
+ function isClassicPressDirectory(projectPath) {
311
+ if (!isWordPressDirectory(projectPath)) {
312
+ return false;
313
+ }
314
+ const versionFile = path7.join(projectPath, "wp-includes", "version.php");
315
+ if (!fs8.existsSync(versionFile)) {
316
+ return false;
317
+ }
318
+ return fs8.readFileSync(versionFile, "utf8").includes("classicpress_version");
293
319
  }
294
320
 
295
321
  // src/output.ts
@@ -300,24 +326,54 @@ var output = shouldOutput() ? console : null;
300
326
 
301
327
  // src/paths.ts
302
328
  import crypto from "crypto";
303
- import fs8 from "fs";
329
+ import fs9 from "fs";
304
330
  import os from "os";
305
- import path7 from "path";
331
+ import path8 from "path";
306
332
  function getWpAppHome() {
307
- return process.env[WP_APP_HOME_ENV] || path7.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
333
+ const home = process.env[WP_APP_HOME_ENV] || path8.join(os.homedir(), WP_APP_HIDDEN_FOLDER);
334
+ cleanupLegacyHome(home);
335
+ return home;
308
336
  }
309
337
  function getCachePath() {
310
- return ensureDir(path7.join(getWpAppHome(), "cache"));
338
+ return ensureDir(path8.join(getWpAppHome(), "cache"));
339
+ }
340
+ function getCorePath(wordPressVersion) {
341
+ const key = wordPressVersion.replace(/[^a-zA-Z0-9.-]/g, "_");
342
+ return path8.join(getWpAppHome(), "core", key);
311
343
  }
312
344
  function getSitePath(projectPath) {
313
- const projectName = path7.basename(path7.resolve(projectPath));
314
- const hash = crypto.createHash("sha1").update(path7.resolve(projectPath)).digest("hex");
315
- return path7.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
345
+ const resolved = path8.resolve(projectPath);
346
+ const identity = process.platform === "win32" ? resolved.toLowerCase() : resolved;
347
+ const projectName = path8.basename(resolved);
348
+ const hash = crypto.createHash("sha1").update(identity).digest("hex");
349
+ return path8.join(getWpAppHome(), "sites", `${projectName}-${hash}`);
350
+ }
351
+ function getProjectLocalPath(projectPath) {
352
+ return path8.join(path8.resolve(projectPath), ".local", "wp-app");
316
353
  }
317
354
  function ensureDir(dir) {
318
- fs8.mkdirSync(dir, { recursive: true });
355
+ fs9.mkdirSync(dir, { recursive: true });
319
356
  return dir;
320
357
  }
358
+ var LEGACY_TOP_LEVEL_DIRS = [
359
+ "wordpress-versions",
360
+ "wp-content",
361
+ "mu-plugins",
362
+ "sqlite-database-integration-main"
363
+ ];
364
+ var legacyCleanupDone = false;
365
+ function cleanupLegacyHome(home) {
366
+ if (legacyCleanupDone) {
367
+ return;
368
+ }
369
+ legacyCleanupDone = true;
370
+ for (const name of LEGACY_TOP_LEVEL_DIRS) {
371
+ const dir = path8.join(home, name);
372
+ if (fs9.existsSync(dir)) {
373
+ fs9.rmSync(dir, { recursive: true, force: true });
374
+ }
375
+ }
376
+ }
321
377
 
322
378
  // src/download.ts
323
379
  function getWordPressVersionUrl(version) {
@@ -336,8 +392,8 @@ function getClassicPressVersionUrl(version) {
336
392
  return `https://github.com/ClassicPress/ClassicPress-release/archive/refs/tags/${tag}.zip`;
337
393
  }
338
394
  async function cachedZip(url, cacheKey) {
339
- const cacheFile = path8.join(getCachePath(), cacheKey);
340
- if (!fs9.existsSync(cacheFile)) {
395
+ const cacheFile = path9.join(getCachePath(), cacheKey);
396
+ if (!fs10.existsSync(cacheFile)) {
341
397
  output?.log(`Downloading ${cacheKey}...`);
342
398
  const response = await fetch(url, { redirect: "follow" });
343
399
  if (!response.ok) {
@@ -346,11 +402,11 @@ async function cachedZip(url, cacheKey) {
346
402
  );
347
403
  }
348
404
  const bytes = Buffer.from(await response.arrayBuffer());
349
- fs9.writeFileSync(`${cacheFile}.partial`, bytes);
350
- fs9.renameSync(`${cacheFile}.partial`, cacheFile);
405
+ fs10.writeFileSync(`${cacheFile}.partial`, bytes);
406
+ fs10.renameSync(`${cacheFile}.partial`, cacheFile);
351
407
  output?.log(`Cached ${cacheKey} (${bytes.length} bytes).`);
352
408
  }
353
- return new File([fs9.readFileSync(cacheFile)], cacheKey);
409
+ return new File([fs10.readFileSync(cacheFile)], cacheKey);
354
410
  }
355
411
  async function getCoreZip(version) {
356
412
  if (isClassicPressVersion(version)) {
@@ -364,17 +420,22 @@ async function getSqliteIntegrationZip() {
364
420
 
365
421
  // src/engine.ts
366
422
  async function startWPApp(options) {
367
- const projectPath = path9.resolve(options.projectPath);
423
+ const projectPath = path10.resolve(options.projectPath);
368
424
  const mode = options.mode;
369
425
  output?.log(`directory: ${options.projectPath}`);
370
426
  output?.log(`mode: ${mode}`);
371
427
  output?.log(`php: ${options.phpVersion}`);
372
- const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ ? projectPath : mode === "wordpress-develop" /* WORDPRESS_DEVELOP */ ? path9.join(projectPath, "build") : ensureDir(getSitePath(projectPath));
428
+ const sharesCore = mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */ || mode === "plugin" /* PLUGIN */ || mode === "wp-content" /* WP_CONTENT */;
429
+ const hostDocroot = mode === "index" /* INDEX */ || mode === "wordpress" /* WORDPRESS */ || mode === "classicpress" /* CLASSICPRESS */ ? projectPath : ensureDir(getCorePath(options.wordPressVersion));
430
+ const projectDataRoot = sharesCore ? ensureDir(
431
+ mode === "wp-app" /* WP_APP */ ? getProjectLocalPath(projectPath) : getSitePath(projectPath)
432
+ ) : null;
373
433
  const isWordPressBacked = mode !== "index" /* INDEX */;
374
- const freshSite = isWordPressBacked && !fs10.existsSync(path9.join(hostDocroot, "wp-load.php"));
434
+ const freshCore = isWordPressBacked && !fs11.existsSync(path10.join(hostDocroot, "wp-load.php"));
435
+ const freshSite = sharesCore ? !fs11.existsSync(path10.join(projectDataRoot, "database", ".ht.sqlite")) : freshCore;
375
436
  if (isWordPressBacked) {
376
437
  output?.log(`wp: ${options.wordPressVersion}`);
377
- output?.log(`site data: ${hostDocroot}`);
438
+ output?.log(`site data: ${projectDataRoot ?? hostDocroot}`);
378
439
  }
379
440
  const php = await createPhp(options.phpVersion);
380
441
  php.mkdir(DOCROOT);
@@ -384,7 +445,7 @@ async function startWPApp(options) {
384
445
  DOCROOT,
385
446
  options.absoluteUrl
386
447
  );
387
- if (freshSite) {
448
+ if (freshCore) {
388
449
  await installCore(
389
450
  php,
390
451
  await getCoreZip(options.wordPressVersion),
@@ -399,6 +460,22 @@ async function startWPApp(options) {
399
460
  DOCROOT
400
461
  );
401
462
  }
463
+ }
464
+ if (projectDataRoot) {
465
+ const databaseDir = ensureDir(path10.join(projectDataRoot, "database"));
466
+ const uploadsDir = ensureDir(path10.join(projectDataRoot, "uploads"));
467
+ php.mkdir(`${DOCROOT}/wp-content/database`);
468
+ await php.mount(
469
+ `${DOCROOT}/wp-content/database`,
470
+ createNodeFsMountHandler(databaseDir)
471
+ );
472
+ php.mkdir(`${DOCROOT}/wp-content/uploads`);
473
+ await php.mount(
474
+ `${DOCROOT}/wp-content/uploads`,
475
+ createNodeFsMountHandler(uploadsDir)
476
+ );
477
+ }
478
+ if (isWordPressBacked) {
402
479
  defineSiteConstants(
403
480
  php,
404
481
  options.absoluteUrl,
@@ -415,8 +492,9 @@ async function startWPApp(options) {
415
492
  return { requestHandler, php, options, freshSite };
416
493
  }
417
494
  async function applyProjectMounts(php, mode, projectPath) {
418
- const projectName = path9.basename(projectPath);
495
+ const projectName = path10.basename(projectPath);
419
496
  switch (mode) {
497
+ case "wp-app" /* WP_APP */:
420
498
  case "theme" /* THEME */:
421
499
  await mountAt(
422
500
  php,
@@ -432,13 +510,13 @@ async function applyProjectMounts(php, mode, projectPath) {
432
510
  );
433
511
  break;
434
512
  case "wp-content" /* WP_CONTENT */:
435
- for (const entry of fs10.readdirSync(projectPath)) {
513
+ for (const entry of fs11.readdirSync(projectPath)) {
436
514
  if (entry === "index.php") {
437
515
  continue;
438
516
  }
439
517
  await mountAt(
440
518
  php,
441
- path9.join(projectPath, entry),
519
+ path10.join(projectPath, entry),
442
520
  `${DOCROOT}/wp-content/${entry}`
443
521
  );
444
522
  }
@@ -448,14 +526,14 @@ async function applyProjectMounts(php, mode, projectPath) {
448
526
  }
449
527
  }
450
528
  async function mountAt(php, hostPath, vfsPath) {
451
- if (fs10.statSync(hostPath).isDirectory()) {
529
+ if (fs11.statSync(hostPath).isDirectory()) {
452
530
  php.mkdir(vfsPath);
453
531
  }
454
532
  await php.mount(vfsPath, createNodeFsMountHandler(hostPath));
455
533
  }
456
534
  async function activateProject(php, mode, projectPath) {
457
- const projectName = path9.basename(projectPath);
458
- if (mode === "theme" /* THEME */) {
535
+ const projectName = path10.basename(projectPath);
536
+ if (mode === "wp-app" /* WP_APP */ || mode === "theme" /* THEME */) {
459
537
  await runWordPressCode(
460
538
  php,
461
539
  `switch_theme(${phpString(projectName)});`
@@ -500,11 +578,11 @@ function phpString(value) {
500
578
  }
501
579
  function findPluginFile(projectPath) {
502
580
  const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
503
- for (const file of fs10.readdirSync(projectPath)) {
581
+ for (const file of fs11.readdirSync(projectPath)) {
504
582
  if (!file.endsWith(".php")) {
505
583
  continue;
506
584
  }
507
- const content = fs10.readFileSync(path9.join(projectPath, file), "utf8");
585
+ const content = fs11.readFileSync(path10.join(projectPath, file), "utf8");
508
586
  if (pluginNameRegex.test(content)) {
509
587
  return file;
510
588
  }
@@ -512,8 +590,10 @@ function findPluginFile(projectPath) {
512
590
  return null;
513
591
  }
514
592
  function inferMode(projectPath) {
515
- if (isWordPressDevelopDirectory(projectPath)) {
516
- return "wordpress-develop" /* WORDPRESS_DEVELOP */;
593
+ if (isWpAppProject(projectPath)) {
594
+ return "wp-app" /* WP_APP */;
595
+ } else if (isClassicPressDirectory(projectPath)) {
596
+ return "classicpress" /* CLASSICPRESS */;
517
597
  } else if (isWordPressDirectory(projectPath)) {
518
598
  return "wordpress" /* WORDPRESS */;
519
599
  } else if (isWpContentDirectory(projectPath)) {
@@ -525,7 +605,7 @@ function inferMode(projectPath) {
525
605
  } else if (hasIndexFile(projectPath)) {
526
606
  return "index" /* INDEX */;
527
607
  }
528
- return "playground" /* PLAYGROUND */;
608
+ return null;
529
609
  }
530
610
 
531
611
  // src/port-finder.ts
@@ -556,11 +636,6 @@ var PortFinder = class _PortFinder {
556
636
  });
557
637
  });
558
638
  }
559
- /**
560
- * Returns the first available open port, caching and reusing it for subsequent calls.
561
- *
562
- * @returns {Promise<number>} A promise that resolves to the open port number.
563
- */
564
639
  async getOpenPort() {
565
640
  if (this.#openPort) {
566
641
  return this.#openPort;
@@ -612,7 +687,11 @@ async function getWpAppConfig(args) {
612
687
  }
613
688
  });
614
689
  if (!options.mode || options.mode === "auto") {
615
- options.mode = inferMode(options.projectPath);
690
+ const inferred = inferMode(options.projectPath);
691
+ if (!inferred) {
692
+ process.exit(1);
693
+ }
694
+ options.mode = inferred;
616
695
  }
617
696
  if (!options.absoluteUrl) {
618
697
  options.absoluteUrl = await getAbsoluteURL();
@@ -631,12 +710,12 @@ async function getWpAppConfig(args) {
631
710
  }
632
711
 
633
712
  // src/start-server.ts
634
- import fs11 from "fs";
713
+ import fs12 from "fs";
635
714
  import { Readable } from "stream";
636
715
  import { pipeline } from "stream/promises";
637
716
  import express from "express";
638
717
  async function startServer(options = {}) {
639
- if (!fs11.existsSync(options.projectPath)) {
718
+ if (!fs12.existsSync(options.projectPath)) {
640
719
  throw new Error(
641
720
  `The given path "${options.projectPath}" does not exist.`
642
721
  );