@wordpress/env 10.39.0 → 11.0.1-next.v.20260206T143.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.
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+ /**
3
+ * External dependencies
4
+ */
5
+ const path = require( 'path' );
6
+ const { existsSync } = require( 'fs' );
7
+ const chalk = require( 'chalk' );
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ const { loadConfig } = require( '../config' );
13
+ const { getRuntime, detectRuntime } = require( '../runtime' );
14
+
15
+ /**
16
+ * Check if an environment has been initialized by looking for runtime-specific files.
17
+ *
18
+ * @param {Object} config The wp-env configuration object.
19
+ * @return {boolean} True if the environment has been initialized.
20
+ */
21
+ function isEnvironmentInitialized( config ) {
22
+ // Check for Docker's docker-compose.yml
23
+ if ( existsSync( config.dockerComposeConfigPath ) ) {
24
+ return true;
25
+ }
26
+
27
+ // Check for Playground's blueprint file
28
+ const playgroundBlueprintPath = path.join(
29
+ config.workDirectoryPath,
30
+ 'playground-blueprint.json'
31
+ );
32
+ if ( existsSync( playgroundBlueprintPath ) ) {
33
+ return true;
34
+ }
35
+
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * Outputs the status of the wp-env environment.
41
+ *
42
+ * @param {Object} options
43
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
44
+ * @param {boolean} options.debug True if debug mode is enabled.
45
+ * @param {boolean} options.json True to output as JSON.
46
+ */
47
+ module.exports = async function status( { spinner, debug, json } ) {
48
+ spinner.text = 'Getting environment status.';
49
+
50
+ const config = await loadConfig( path.resolve( '.' ) );
51
+
52
+ // Check if environment is initialized by looking for runtime-specific files.
53
+ // We check for these files specifically because the work directory may exist
54
+ // just from caching the WordPress version, but these files are only created
55
+ // when `wp-env start` is actually run.
56
+ if ( ! isEnvironmentInitialized( config ) ) {
57
+ spinner.stop();
58
+ if ( json ) {
59
+ console.log(
60
+ JSON.stringify( {
61
+ status: 'uninitialized',
62
+ installPath: config.workDirectoryPath,
63
+ configPath: config.configDirectoryPath,
64
+ } )
65
+ );
66
+ } else {
67
+ console.log( formatNotInitialized( config ) );
68
+ }
69
+ return;
70
+ }
71
+
72
+ // Detect and get runtime.
73
+ const runtimeName = detectRuntime( config.workDirectoryPath );
74
+ const runtime = getRuntime( runtimeName );
75
+
76
+ // Get status from runtime.
77
+ const statusData = await runtime.getStatus( config, { spinner, debug } );
78
+
79
+ spinner.stop();
80
+ if ( json ) {
81
+ console.log( JSON.stringify( statusData ) );
82
+ } else {
83
+ console.log( formatStatus( statusData ) );
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Format status for human-readable output when not initialized.
89
+ *
90
+ * @param {Object} config The config object.
91
+ * @return {string} Formatted output.
92
+ */
93
+ function formatNotInitialized( config ) {
94
+ const indent = ' - ';
95
+ return `
96
+ ${ chalk.bold( 'status' ) }: ${ chalk.red( 'uninitialized' ) }
97
+ ${ indent }install path: ${ chalk.dim( config.workDirectoryPath ) }
98
+ ${ indent }config: ${ chalk.dim( config.configDirectoryPath ) }
99
+
100
+ ${ chalk.dim( 'Run `wp-env start` to initialize the environment.' ) }
101
+ `;
102
+ }
103
+
104
+ /**
105
+ * Format status data for human-readable output.
106
+ *
107
+ * @param {Object} status The status object from runtime.
108
+ * @return {string} Formatted output.
109
+ */
110
+ function formatStatus( status ) {
111
+ const statusColor = status.status === 'running' ? chalk.green : chalk.red;
112
+ const indent = ' - ';
113
+
114
+ let output = `
115
+ ${ chalk.bold( 'status' ) }: ${ statusColor( status.status ) }
116
+ ${ indent }runtime: ${ chalk.dim( status.runtime ) }
117
+ ${ indent }install path: ${ chalk.dim( status.installPath ) }
118
+ ${ indent }config: ${ chalk.dim( status.configPath ) }
119
+ `;
120
+
121
+ // Environment section.
122
+ output += `\n${ chalk.bold( 'environment' ) }:\n`;
123
+ if ( status.urls?.development ) {
124
+ output += `${ indent }url: ${ chalk.dim( status.urls.development ) }\n`;
125
+ }
126
+ output += `${ indent }multisite: ${ chalk.dim(
127
+ status.config?.multisite ? 'yes' : 'no'
128
+ ) }\n`;
129
+ output += `${ indent }xdebug: ${ chalk.dim(
130
+ status.config?.xdebug || 'off'
131
+ ) }\n`;
132
+ if ( status.ports?.development ) {
133
+ output += `${ indent }http port: ${ chalk.dim(
134
+ status.ports.development
135
+ ) }\n`;
136
+ }
137
+ if ( status.urls?.phpmyadmin ) {
138
+ output += `${ indent }phpmyadmin url: ${ chalk.dim(
139
+ status.urls.phpmyadmin
140
+ ) }\n`;
141
+ }
142
+ if ( status.ports?.mysql ) {
143
+ output += `${ indent }mysql port: ${ chalk.dim(
144
+ status.ports.mysql
145
+ ) }\n`;
146
+ }
147
+ if ( status.ports?.tests ) {
148
+ output += `${ indent }test http port: ${ chalk.dim(
149
+ status.ports.tests
150
+ ) }\n`;
151
+ }
152
+
153
+ return output;
154
+ }
@@ -19,6 +19,8 @@ const { getRuntime, detectRuntime } = require( '../runtime' );
19
19
  */
20
20
  module.exports = async function stop( { spinner, debug } ) {
21
21
  const config = await loadConfig( path.resolve( '.' ) );
22
- const runtime = getRuntime( detectRuntime( config.workDirectoryPath ) );
22
+ const runtime = getRuntime(
23
+ await detectRuntime( config.workDirectoryPath )
24
+ );
23
25
  await runtime.stop( config, { spinner, debug } );
24
26
  };
@@ -23,7 +23,6 @@ const { checkPort, checkVersion, checkString } = require( './validate-config' );
23
23
  * @property {?number} phpmyadminPort An override for the development environment's phpMyAdmin port.
24
24
  * @property {?WPSource} coreSource An override for all environment's coreSource.
25
25
  * @property {?string} phpVersion An override for all environment's PHP version.
26
- * @property {?boolean} multisite An override for if environmen should be multisite.
27
26
  * @property {?Object.<string, string>} lifecycleScripts An override for various lifecycle scripts.
28
27
  */
29
28
 
@@ -69,10 +68,6 @@ module.exports = function getConfigFromEnvironmentVars( cacheDirectoryPath ) {
69
68
  environmentConfig.phpVersion = process.env.WP_ENV_PHP_VERSION;
70
69
  }
71
70
 
72
- if ( process.env.WP_ENV_MULTISITE ) {
73
- environmentConfig.multisite = !! process.env.WP_ENV_MULTISITE;
74
- }
75
-
76
71
  return environmentConfig;
77
72
  };
78
73
 
@@ -108,6 +103,8 @@ function getLifecycleScriptOverrides() {
108
103
  const lifecycleEnvironmentVars = {
109
104
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_START: 'afterStart',
110
105
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_CLEAN: 'afterClean',
106
+ WP_ENV_LIFECYCLE_SCRIPT_AFTER_RESET: 'afterReset',
107
+ WP_ENV_LIFECYCLE_SCRIPT_AFTER_CLEANUP: 'afterCleanup',
111
108
  WP_ENV_LIFECYCLE_SCRIPT_AFTER_DESTROY: 'afterDestroy',
112
109
  };
113
110
  for ( const envVar in lifecycleEnvironmentVars ) {
@@ -235,6 +235,8 @@ async function getDefaultConfig(
235
235
  lifecycleScripts: {
236
236
  afterStart: null,
237
237
  afterClean: null,
238
+ afterReset: null,
239
+ afterCleanup: null,
238
240
  afterDestroy: null,
239
241
  },
240
242
  env: {
@@ -312,6 +314,12 @@ function getEnvironmentVarOverrides( cacheDirectoryPath ) {
312
314
  overrideConfig.env.tests.phpVersion = overrides.phpVersion;
313
315
  }
314
316
 
317
+ if ( overrides.multisite ) {
318
+ overrideConfig.multisite = overrides.multisite;
319
+ overrideConfig.env.development.multisite = overrides.multisite;
320
+ overrideConfig.env.tests.multisite = overrides.multisite;
321
+ }
322
+
315
323
  return overrideConfig;
316
324
  }
317
325
 
@@ -71,7 +71,9 @@ exports[`Config Integration should load local and override configuration files 1
71
71
  },
72
72
  "lifecycleScripts": {
73
73
  "afterClean": null,
74
+ "afterCleanup": null,
74
75
  "afterDestroy": "test",
76
+ "afterReset": null,
75
77
  "afterStart": null,
76
78
  },
77
79
  "name": "gutenberg",
@@ -150,7 +152,9 @@ exports[`Config Integration should load local configuration file 1`] = `
150
152
  },
151
153
  "lifecycleScripts": {
152
154
  "afterClean": null,
155
+ "afterCleanup": null,
153
156
  "afterDestroy": null,
157
+ "afterReset": null,
154
158
  "afterStart": "test",
155
159
  },
156
160
  "name": "gutenberg",
@@ -229,7 +233,9 @@ exports[`Config Integration should use default configuration 1`] = `
229
233
  },
230
234
  "lifecycleScripts": {
231
235
  "afterClean": null,
236
+ "afterCleanup": null,
232
237
  "afterDestroy": null,
238
+ "afterReset": null,
233
239
  "afterStart": null,
234
240
  },
235
241
  "name": "gutenberg",
@@ -310,7 +316,9 @@ exports[`Config Integration should use environment variables over local and over
310
316
  },
311
317
  "lifecycleScripts": {
312
318
  "afterClean": null,
319
+ "afterCleanup": null,
313
320
  "afterDestroy": null,
321
+ "afterReset": null,
314
322
  "afterStart": "test",
315
323
  },
316
324
  "name": "gutenberg",
@@ -52,6 +52,8 @@ const DEFAULT_CONFIG = {
52
52
  lifecycleScripts: {
53
53
  afterStart: null,
54
54
  afterClean: null,
55
+ afterReset: null,
56
+ afterCleanup: null,
55
57
  afterDestroy: null,
56
58
  },
57
59
  env: {
@@ -181,6 +181,19 @@ module.exports = function buildDockerComposeConfig( config ) {
181
181
  config.env.tests.phpmyadminPort ?? ''
182
182
  }}:80`;
183
183
 
184
+ // MySQL healthcheck using MariaDB's official healthcheck.sh script.
185
+ // --connect: verifies TCP connection and that entrypoint has finished
186
+ // --innodb_initialized: ensures InnoDB storage engine is fully initialized
187
+ // MARIADB_AUTO_UPGRADE env var ensures healthcheck user exists for existing installations.
188
+ // Timing is generous to support slow CI environments.
189
+ const mysqlHealthcheck = {
190
+ test: [ 'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized' ],
191
+ interval: '5s',
192
+ timeout: '10s',
193
+ retries: 12,
194
+ start_period: '60s',
195
+ };
196
+
184
197
  return {
185
198
  services: {
186
199
  mysql: {
@@ -191,8 +204,11 @@ module.exports = function buildDockerComposeConfig( config ) {
191
204
  MYSQL_ROOT_PASSWORD:
192
205
  dbEnv.credentials.WORDPRESS_DB_PASSWORD,
193
206
  MYSQL_DATABASE: dbEnv.development.WORDPRESS_DB_NAME,
207
+ // Ensures healthcheck user is created for existing installations.
208
+ MARIADB_AUTO_UPGRADE: '1',
194
209
  },
195
210
  volumes: [ 'mysql:/var/lib/mysql' ],
211
+ healthcheck: mysqlHealthcheck,
196
212
  },
197
213
  'tests-mysql': {
198
214
  image: 'mariadb:lts',
@@ -202,11 +218,18 @@ module.exports = function buildDockerComposeConfig( config ) {
202
218
  MYSQL_ROOT_PASSWORD:
203
219
  dbEnv.credentials.WORDPRESS_DB_PASSWORD,
204
220
  MYSQL_DATABASE: dbEnv.tests.WORDPRESS_DB_NAME,
221
+ // Ensures healthcheck user is created for existing installations.
222
+ MARIADB_AUTO_UPGRADE: '1',
205
223
  },
206
224
  volumes: [ 'mysql-test:/var/lib/mysql' ],
225
+ healthcheck: mysqlHealthcheck,
207
226
  },
208
227
  wordpress: {
209
- depends_on: [ 'mysql' ],
228
+ depends_on: {
229
+ mysql: {
230
+ condition: 'service_healthy',
231
+ },
232
+ },
210
233
  build: {
211
234
  context: '.',
212
235
  dockerfile: 'WordPress.Dockerfile',
@@ -224,7 +247,11 @@ module.exports = function buildDockerComposeConfig( config ) {
224
247
  extra_hosts: [ 'host.docker.internal:host-gateway' ],
225
248
  },
226
249
  'tests-wordpress': {
227
- depends_on: [ 'tests-mysql' ],
250
+ depends_on: {
251
+ 'tests-mysql': {
252
+ condition: 'service_healthy',
253
+ },
254
+ },
228
255
  build: {
229
256
  context: '.',
230
257
  dockerfile: 'Tests-WordPress.Dockerfile',
@@ -11,7 +11,6 @@ const { rimraf } = require( 'rimraf' );
11
11
  /**
12
12
  * Promisified dependencies
13
13
  */
14
- const sleep = util.promisify( setTimeout );
15
14
  const exec = util.promisify( require( 'child_process' ).exec );
16
15
 
17
16
  /**
@@ -26,7 +25,6 @@ const {
26
25
  validateRunContainer,
27
26
  } = require( './validate-run-container' );
28
27
  const {
29
- checkDatabaseConnection,
30
28
  configureWordPress,
31
29
  resetDatabase,
32
30
  setupWordPressDirectories,
@@ -174,7 +172,7 @@ class DockerRuntime {
174
172
  }
175
173
 
176
174
  await Promise.all( [
177
- dockerCompose.upOne( 'mysql', {
175
+ dockerCompose.upMany( [ 'mysql', 'tests-mysql' ], {
178
176
  ...dockerComposeConfig,
179
177
  commandOptions: shouldConfigureWp
180
178
  ? [ '--build', '--force-recreate' ]
@@ -250,19 +248,6 @@ class DockerRuntime {
250
248
  if ( shouldConfigureWp ) {
251
249
  spinner.text = 'Configuring WordPress.';
252
250
 
253
- try {
254
- await checkDatabaseConnection( fullConfig );
255
- } catch ( error ) {
256
- // Wait 30 seconds for MySQL to accept connections.
257
- await retry( () => checkDatabaseConnection( fullConfig ), {
258
- times: 30,
259
- delay: 1000,
260
- } );
261
-
262
- // It takes 3-4 seconds for MySQL to be ready after it starts accepting connections.
263
- await sleep( 4000 );
264
- }
265
-
266
251
  // Retry WordPress installation in case MySQL *still* wasn't ready.
267
252
  await Promise.all( [
268
253
  retry(
@@ -369,6 +354,15 @@ class DockerRuntime {
369
354
  return 'WARNING! This will remove Docker containers, volumes, networks, and images associated with the WordPress instance.';
370
355
  }
371
356
 
357
+ /**
358
+ * Get the warning message for cleanup confirmation.
359
+ *
360
+ * @return {string} Warning message.
361
+ */
362
+ getCleanupWarningMessage() {
363
+ return 'WARNING! This will remove Docker containers, volumes, networks, and local files associated with the WordPress instance. Docker images will be preserved.';
364
+ }
365
+
372
366
  /**
373
367
  * Stop the Docker containers.
374
368
  *
@@ -413,7 +407,8 @@ class DockerRuntime {
413
407
  spinner.text = 'Removing local files.';
414
408
  // Note: there is a race condition where docker compose actually hasn't finished
415
409
  // by this point, which causes rimraf to fail. We need to wait at least 2.5-5s,
416
- // but using 10s in case it's dependant on the machine.
410
+ // but using 10s in case it's dependent on the machine. Removing images takes
411
+ // longer so we use a longer wait time here.
417
412
  await new Promise( ( resolve ) => setTimeout( resolve, 10000 ) );
418
413
  await rimraf( config.workDirectoryPath );
419
414
 
@@ -421,11 +416,38 @@ class DockerRuntime {
421
416
  }
422
417
 
423
418
  /**
424
- * Clean/reset the WordPress database.
419
+ * Cleanup the Docker containers and remove local files, but preserve images.
420
+ *
421
+ * @param {WPConfig} config The wp-env config object.
422
+ * @param {Object} options Cleanup options.
423
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
424
+ * @param {boolean} options.debug True if debug mode is enabled.
425
+ */
426
+ async cleanup( config, { spinner, debug } ) {
427
+ spinner.text = 'Removing docker containers, volumes, and networks.';
428
+
429
+ await dockerCompose.down( {
430
+ config: config.dockerComposeConfigPath,
431
+ commandOptions: [ '--volumes', '--remove-orphans' ],
432
+ log: debug,
433
+ } );
434
+
435
+ spinner.text = 'Removing local files.';
436
+ // Note: there is a race condition where docker compose actually hasn't finished
437
+ // by this point, which causes rimraf to fail. We need to wait at least 2.5-5s,
438
+ // but since we're not removing images, the wait can be shorter.
439
+ await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) );
440
+ await rimraf( config.workDirectoryPath );
441
+
442
+ spinner.text = 'Cleaned up WordPress environment.';
443
+ }
444
+
445
+ /**
446
+ * Reset the WordPress database.
425
447
  *
426
448
  * @param {WPConfig} config The wp-env config object.
427
- * @param {Object} options Clean options.
428
- * @param {string} options.environment The environment to clean.
449
+ * @param {Object} options Reset options.
450
+ * @param {string} options.environment The environment to reset.
429
451
  * @param {Object} options.spinner A CLI spinner which indicates progress.
430
452
  * @param {boolean} options.debug True if debug mode is enabled.
431
453
  */
@@ -435,13 +457,23 @@ class DockerRuntime {
435
457
  const description = `${ environment } environment${
436
458
  environment === 'all' ? 's' : ''
437
459
  }`;
438
- spinner.text = `Cleaning ${ description }.`;
460
+ spinner.text = `Resetting ${ description }.`;
439
461
 
440
462
  const tasks = [];
441
463
 
442
- // Start the database first to avoid race conditions where all tasks create
443
- // different docker networks with the same name.
444
- await dockerCompose.upOne( 'mysql', {
464
+ // Start the appropriate MySQL service(s) first to avoid race conditions
465
+ // where parallel tasks try to create docker networks with the same name.
466
+ // The dependency chain (cli -> wordpress -> mysql with service_healthy)
467
+ // ensures MySQL is ready before database operations run.
468
+ const mysqlServices = [];
469
+ if ( environment === 'all' || environment === 'development' ) {
470
+ mysqlServices.push( 'mysql' );
471
+ }
472
+ if ( environment === 'all' || environment === 'tests' ) {
473
+ mysqlServices.push( 'tests-mysql' );
474
+ }
475
+
476
+ await dockerCompose.upMany( mysqlServices, {
445
477
  config: fullConfig.dockerComposeConfigPath,
446
478
  log: fullConfig.debug,
447
479
  } );
@@ -466,7 +498,7 @@ class DockerRuntime {
466
498
 
467
499
  await Promise.all( tasks );
468
500
 
469
- spinner.text = `Cleaned ${ description }.`;
501
+ spinner.text = `Reset ${ description }.`;
470
502
  }
471
503
 
472
504
  /**
@@ -577,6 +609,74 @@ class DockerRuntime {
577
609
  spinner.text = 'Finished showing logs.';
578
610
  }
579
611
 
612
+ /**
613
+ * Get the status of the Docker environment.
614
+ *
615
+ * @param {WPConfig} config The wp-env config object.
616
+ * @param {Object} options Status options.
617
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
618
+ * @param {boolean} options.debug True if debug mode is enabled.
619
+ * @return {Promise<Object>} Status object with environment information.
620
+ */
621
+ async getStatus( config, { spinner, debug } ) {
622
+ spinner.text = 'Getting environment status.';
623
+
624
+ const fullConfig = await initConfig( { spinner, debug } );
625
+ const dockerComposeConfig = {
626
+ config: fullConfig.dockerComposeConfigPath,
627
+ log: debug,
628
+ };
629
+
630
+ // Check if containers are running by trying to get a port.
631
+ let isRunning = false;
632
+ let mySQLPort = null;
633
+ let phpmyadminPort = null;
634
+
635
+ try {
636
+ mySQLPort = await this._getPublicDockerPort(
637
+ 'mysql',
638
+ 3306,
639
+ dockerComposeConfig
640
+ );
641
+ isRunning = true;
642
+
643
+ if ( fullConfig.env.development.phpmyadminPort ) {
644
+ phpmyadminPort = await this._getPublicDockerPort(
645
+ 'phpmyadmin',
646
+ 80,
647
+ dockerComposeConfig
648
+ );
649
+ }
650
+ } catch {
651
+ // Containers are not running.
652
+ }
653
+
654
+ const siteUrl = fullConfig.env.development.config.WP_SITEURL;
655
+
656
+ return {
657
+ status: isRunning ? 'running' : 'stopped',
658
+ runtime: 'docker',
659
+ urls: {
660
+ development: isRunning ? siteUrl : null,
661
+ phpmyadmin:
662
+ isRunning && phpmyadminPort
663
+ ? `http://localhost:${ phpmyadminPort }`
664
+ : null,
665
+ },
666
+ ports: {
667
+ development: fullConfig.env.development.port,
668
+ tests: fullConfig.env.tests.port,
669
+ mysql: mySQLPort,
670
+ },
671
+ config: {
672
+ multisite: fullConfig.env.development.multisite,
673
+ xdebug: fullConfig.xdebug || 'off',
674
+ },
675
+ configPath: fullConfig.configDirectoryPath,
676
+ installPath: fullConfig.workDirectoryPath,
677
+ };
678
+ }
679
+
580
680
  /**
581
681
  * Runs an arbitrary command on the given Docker container.
582
682
  *
@@ -47,20 +47,6 @@ function isWPMajorMinorVersionLower( version, compareVersion ) {
47
47
  return versionNumber < compareVersionNumber;
48
48
  }
49
49
 
50
- /**
51
- * Checks a WordPress database connection. An error is thrown if the test is
52
- * unsuccessful.
53
- *
54
- * @param {WPConfig} config The wp-env config object.
55
- */
56
- async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) {
57
- await dockerCompose.run( 'cli', 'wp db check', {
58
- config: dockerComposeConfigPath,
59
- commandOptions: [ '--rm' ],
60
- log: debug,
61
- } );
62
- }
63
-
64
50
  /**
65
51
  * Configures WordPress for the given environment by installing WordPress,
66
52
  * activating all plugins, and activating the first theme. These steps are
@@ -316,7 +302,6 @@ async function copyCoreFiles( fromPath, toPath ) {
316
302
  }
317
303
 
318
304
  module.exports = {
319
- checkDatabaseConnection,
320
305
  configureWordPress,
321
306
  resetDatabase,
322
307
  setupWordPressDirectories,
@@ -12,6 +12,17 @@ class UnsupportedCommandError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ /**
16
+ * Error thrown when the environment has not been initialized.
17
+ */
18
+ class EnvironmentNotInitializedError extends Error {
19
+ constructor() {
20
+ super( 'Environment not initialized. Run `wp-env start` first.' );
21
+ this.name = 'EnvironmentNotInitializedError';
22
+ }
23
+ }
24
+
15
25
  module.exports = {
16
26
  UnsupportedCommandError,
27
+ EnvironmentNotInitializedError,
17
28
  };
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * External dependencies
5
- */
6
- const { existsSync } = require( 'fs' );
7
- const path = require( 'path' );
8
-
9
3
  /**
10
4
  * Internal dependencies
11
5
  */
12
6
  const DockerRuntime = require( './docker' );
13
7
  const PlaygroundRuntime = require( './playground' );
14
- const { UnsupportedCommandError } = require( './errors' );
8
+ const {
9
+ UnsupportedCommandError,
10
+ EnvironmentNotInitializedError,
11
+ } = require( './errors' );
12
+ const { setCache, getCache } = require( '../cache' );
13
+
14
+ const RUNTIME_CACHE_KEY = 'runtime';
15
15
 
16
16
  const runtimes = {
17
17
  docker: DockerRuntime,
@@ -42,28 +42,50 @@ function getAvailableRuntimes() {
42
42
  }
43
43
 
44
44
  /**
45
- * Detect which runtime was used based on files in the work directory.
46
- * Returns 'playground' if playground-blueprint.json exists, otherwise 'docker'.
45
+ * Save the runtime type to the cache file.
46
+ * Called when start command initializes the environment.
47
+ *
48
+ * @param {string} runtimeName The runtime name ('docker' or 'playground').
49
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
50
+ */
51
+ async function saveRuntime( runtimeName, workDirectoryPath ) {
52
+ await setCache( RUNTIME_CACHE_KEY, runtimeName, { workDirectoryPath } );
53
+ }
54
+
55
+ /**
56
+ * Get the saved runtime type from cache.
57
+ *
58
+ * @param {string} workDirectoryPath Path to the wp-env work directory.
59
+ * @return {Promise<string|undefined>} The saved runtime name, or undefined if not set.
60
+ */
61
+ async function getSavedRuntime( workDirectoryPath ) {
62
+ return await getCache( RUNTIME_CACHE_KEY, { workDirectoryPath } );
63
+ }
64
+
65
+ /**
66
+ * Detect which runtime was used by reading from the cache.
67
+ * Throws EnvironmentNotInitializedError if no runtime has been saved.
47
68
  *
48
69
  * @param {string} workDirectoryPath Path to the wp-env work directory.
49
- * @return {string} Runtime name ('docker' or 'playground').
70
+ * @return {Promise<string>} Runtime name ('docker' or 'playground').
71
+ * @throws {EnvironmentNotInitializedError} If environment not initialized.
50
72
  */
51
- function detectRuntime( workDirectoryPath ) {
52
- const playgroundBlueprintFile = path.join(
53
- workDirectoryPath,
54
- 'playground-blueprint.json'
55
- );
56
- if ( existsSync( playgroundBlueprintFile ) ) {
57
- return 'playground';
73
+ async function detectRuntime( workDirectoryPath ) {
74
+ const savedRuntime = await getSavedRuntime( workDirectoryPath );
75
+ if ( ! savedRuntime ) {
76
+ throw new EnvironmentNotInitializedError();
58
77
  }
59
- return 'docker';
78
+ return savedRuntime;
60
79
  }
61
80
 
62
81
  module.exports = {
63
82
  getRuntime,
64
83
  getAvailableRuntimes,
65
84
  detectRuntime,
85
+ saveRuntime,
86
+ getSavedRuntime,
66
87
  DockerRuntime,
67
88
  PlaygroundRuntime,
68
89
  UnsupportedCommandError,
90
+ EnvironmentNotInitializedError,
69
91
  };