@automattic/vip 3.12.2 → 3.13.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.
@@ -35,6 +35,8 @@ services:
35
35
  image: <%= php %>
36
36
  command: run.sh
37
37
  working_dir: /wp
38
+ env_file:
39
+ - .env
38
40
  environment:
39
41
  XDEBUG: <%= xdebug ? 'enable' : 'disable' %>
40
42
  <% if ( xdebugConfig ) { %>
@@ -81,4 +81,4 @@ async function deleteEnvVarCommand(arg, opt) {
81
81
  envContext: true,
82
82
  requiredArgs: 1,
83
83
  usage: `${baseUsage} <VARIABLE_NAME>`
84
- }).examples(examples).option('skip-confirmation', 'Skip manual confirmation of input (USE WITH CAUTION)', false).argv(process.argv, deleteEnvVarCommand);
84
+ }).examples(examples).option('skip-confirmation', 'Skip the confirmation prompt (USE WITH CAUTION).', false).argv(process.argv, deleteEnvVarCommand);
@@ -100,4 +100,4 @@ async function setEnvVarCommand(arg, opt) {
100
100
  envContext: true,
101
101
  requiredArgs: 1,
102
102
  usage: `${baseUsage} <VARIABLE_NAME>`
103
- }).option('from-file', 'Read environment variable value from file (useful for multiline input)').option('skip-confirmation', 'Skip manual confirmation of input (USE WITH CAUTION)', false).examples(examples).argv(process.argv, setEnvVarCommand);
103
+ }).option('from-file', 'Read environment variable value from a UTF-8-encoded text file (useful for multiline input). Accepts a relative or absolute path.').option('skip-confirmation', 'Skip the confirmation prompt (USE WITH CAUTION).', false).examples(examples).argv(process.argv, setEnvVarCommand);
@@ -8,8 +8,8 @@ const exampleUsage = 'vip @example-app.develop config envvar';
8
8
 
9
9
  // Command examples
10
10
  const examples = [{
11
- usage: `${exampleUsage} delete MY_VARIABLE`,
12
- description: 'Delete the environment variable "MY_VARIABLE" from the environment.'
11
+ usage: `${exampleUsage} set MY_VARIABLE`,
12
+ description: 'Add or update the environment variable "MY_VARIABLE" and assign its value at the prompt.'
13
13
  }, {
14
14
  usage: `${exampleUsage} get MY_VARIABLE`,
15
15
  description: 'Retrieve the value of the environment variable "MY_VARIABLE".'
@@ -20,8 +20,8 @@ const examples = [{
20
20
  usage: `${exampleUsage} list`,
21
21
  description: 'List the names of all environment variables.'
22
22
  }, {
23
- usage: `${exampleUsage} set MY_VARIABLE`,
24
- description: 'Add or update the environment variable "MY_VARIABLE" and assign its value at the prompt.'
23
+ usage: `${exampleUsage} delete MY_VARIABLE`,
24
+ description: 'Delete the environment variable "MY_VARIABLE" from the environment.'
25
25
  }];
26
26
  (0, _command.default)({
27
27
  requiredArgs: 0,
@@ -69,10 +69,13 @@ cmd.argv(process.argv, async (arg, opt) => {
69
69
  let defaultOptions = {};
70
70
  /** @type {Record<string,import('../lib/dev-environment/types').IntegrationConfig>} */
71
71
  let integrationsConfig = {};
72
+ /** @type {Record<string,string>} */
73
+ let envVars = {};
72
74
  try {
73
75
  if (opt.app) {
74
76
  const appInfo = await (0, _devEnvironmentCore.getApplicationInformation)(opt.app, opt.env);
75
77
  integrationsConfig = appInfo.environment?.integrations ?? {};
78
+ envVars = appInfo.environment?.envVars ?? {};
76
79
  defaultOptions = (0, _devEnvironmentCli.getOptionsFromAppInfo)(appInfo);
77
80
  }
78
81
  } catch (error) {
@@ -90,7 +93,7 @@ cmd.argv(process.argv, async (arg, opt) => {
90
93
  const instanceData = await (0, _devEnvironmentCli.promptForArguments)(preselectedOptions, defaultOptions, suppressPrompts, true);
91
94
  instanceData.siteSlug = slug;
92
95
  try {
93
- await (0, _devEnvironmentCore.createEnvironment)(lando, instanceData, integrationsConfig);
96
+ await (0, _devEnvironmentCore.createEnvironment)(lando, instanceData, integrationsConfig, envVars);
94
97
  await (0, _devEnvironmentCore.printEnvironmentInfo)(lando, slug, {
95
98
  extended: false,
96
99
  suppressWarnings: true
@@ -21,17 +21,24 @@ const examples = [{
21
21
  usage: `${exampleUsage} --skip-rebuild --slug=example-site`,
22
22
  description: 'Start only the services of a local environment that are not currently in a running state.'
23
23
  }, {
24
- usage: `${exampleUsage} --vscode --slug=example-site`,
24
+ usage: `${exampleUsage} --editor=vscode --slug=example-site`,
25
25
  description: 'Start a local environment and generate a Workspace file for developing in Visual Studio Code.'
26
+ }, {
27
+ usage: `${exampleUsage} --editor=cursor --slug=example-site`,
28
+ description: 'Start a local environment and generate a Workspace file for developing in Cursor Editor.'
29
+ }, {
30
+ usage: `${exampleUsage} --editor=phpstorm --slug=example-site`,
31
+ description: 'Start a local environment and generate a Workspace file for developing in PhpStorm.'
26
32
  }];
27
33
  (0, _command.default)({
28
34
  usage
29
- }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('skip-rebuild', 'Only start services that are not in a running state.').option(['w', 'skip-wp-versions-check'], 'Skip the prompt to update WordPress; occurs if the last major release version is not configured.').option('vscode', 'Generate a Visual Studio Code Workspace file.').examples(examples).argv(process.argv, async (arg, opt) => {
35
+ }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('skip-rebuild', 'Only start services that are not in a running state.').option(['w', 'skip-wp-versions-check'], 'Skip the prompt to update WordPress; occurs if the last major release version is not configured.').option('vscode', 'Generate a Visual Studio Code Workspace file (deprecated, use --editor=vscode instead).').option('editor', 'Generate a workspace file for the specified editor (supports: vscode, cursor, windsurf, phpstorm).').examples(examples).argv(process.argv, async (arg, opt) => {
30
36
  const slug = await (0, _devEnvironmentCli.getEnvironmentName)(opt);
31
37
  const lando = await (0, _devEnvironmentLando.bootstrapLando)();
32
38
  (0, _devEnvironmentCli.validateDependencies)(lando);
33
39
  const startProcessing = new Date();
34
40
  const trackingInfo = (0, _devEnvironmentCli.getEnvTrackingInfo)(slug);
41
+ trackingInfo.editor = opt.editor || (opt.vscode ? 'vscode' : undefined);
35
42
  trackingInfo.vscode = Boolean(opt.vscode);
36
43
  trackingInfo.docker = lando.config.versions.engine;
37
44
  trackingInfo.docker_compose = lando.config.versions.compose;
@@ -55,6 +62,7 @@ const examples = [{
55
62
  process.exitCode = 1;
56
63
  }
57
64
  (0, _devEnvironmentCli.postStart)(slug, {
58
- openVSCode: Boolean(opt.vscode)
65
+ editor: opt.editor,
66
+ vscode: Boolean(opt.vscode)
59
67
  });
60
68
  });
@@ -3,6 +3,10 @@
3
3
 
4
4
  var _command = _interopRequireDefault(require("../lib/cli/command"));
5
5
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
+ if (process.getuid?.() === 0) {
7
+ console.error('This script should not be run as root. Exiting.');
8
+ process.exit(1);
9
+ }
6
10
  (0, _command.default)({
7
11
  requiredArgs: 0
8
12
  }).command('create', 'Create a new local environment.').command('update', 'Update the settings of a local environment.').command('start', 'Start a local environment.').command('stop', 'Stop a local environment.').command('destroy', 'Remove a local environment.').command('info', 'Retrieve information about a local environment.').command('list', 'Retrieve information about all local environments.').command('exec', 'Run a WP-CLI command against a local environment.').command('import', 'Import media or database files to a local environment.').command('shell', 'Create a shell and run commands against a local environment.').command('logs', 'Retrieve logs for a local environment.').command('sync', 'Sync the database of a VIP Platform environment to a local environment.').command('purge', 'Remove all local environments.').argv(process.argv);
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
+ var _singleLineLog = require("@wwa/single-line-log");
4
5
  var _chalk = _interopRequireDefault(require("chalk"));
5
6
  var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
6
- var _singleLineLog = require("single-line-log");
7
7
  var _api = _interopRequireDefault(require("../lib/api"));
8
8
  var _app2 = _interopRequireDefault(require("../lib/api/app"));
9
9
  var _command = _interopRequireDefault(require("../lib/cli/command"));
@@ -67,8 +67,9 @@ async function extractSiteUrls(sqlFile) {
67
67
  return new Promise((resolve, reject) => {
68
68
  const urls = new Set();
69
69
  readInterface.on('line', line => {
70
- const url = findSiteHomeUrl(line);
70
+ let url = findSiteHomeUrl(line);
71
71
  if (url) {
72
+ url = url.replace(/\/$/, '');
72
73
  urls.add(url);
73
74
  }
74
75
  });
@@ -178,7 +179,7 @@ class DevEnvSyncSQLCommand {
178
179
  if (!site?.blogId || site.blogId === 1) continue;
179
180
  const url = site?.homeUrl;
180
181
  if (!url) continue;
181
- const strippedUrl = stripProtocol(url);
182
+ const strippedUrl = stripProtocol(url).replace(/\/$/, '');
182
183
  if (!this.searchReplaceMap[strippedUrl]) continue;
183
184
  const domain = new URL(url).hostname;
184
185
  const newDomain = primaryDomain === domain ? this.landoDomain : `${this.slugifyDomain(domain)}.${this.landoDomain}`;
@@ -2,8 +2,8 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.StepStatus = exports.ProgressTracker = void 0;
5
+ var _singleLineLog = require("@wwa/single-line-log");
5
6
  var _nodeOs = require("node:os");
6
- var _singleLineLog = require("single-line-log");
7
7
  var _format = require("../../lib/cli/format");
8
8
  const PRINT_INTERVAL = process.env.DEBUG ? 5000 : 200; // How often the report is printed. Mainly affects the "spinner" animation.
9
9
  let StepStatus = exports.StepStatus = /*#__PURE__*/function (StepStatus) {
@@ -23,6 +23,7 @@ exports.promptForComponent = promptForComponent;
23
23
  exports.promptForMultisite = promptForMultisite;
24
24
  exports.promptForPhpVersion = promptForPhpVersion;
25
25
  exports.promptForText = promptForText;
26
+ exports.promptForURL = promptForURL;
26
27
  exports.promptForWordPress = promptForWordPress;
27
28
  exports.resolvePath = resolvePath;
28
29
  exports.resolvePhpVersion = resolvePhpVersion;
@@ -161,7 +162,7 @@ function processComponentOptionInput(passedParam, allowLocal) {
161
162
  }
162
163
  return {
163
164
  mode: 'image',
164
- tag: param === 'demo' ? undefined : param
165
+ tag: param === 'demo' || param === 'image' ? undefined : param
165
166
  };
166
167
  }
167
168
  function getOptionsFromAppInfo(appInfo) {
@@ -190,6 +191,13 @@ function getOptionsFromAppInfo(appInfo) {
190
191
  // eslint-disable-next-line complexity
191
192
  async function promptForArguments(preselectedOptions, defaultOptions, suppressPrompts, create) {
192
193
  debug('Provided preselected', preselectedOptions, 'and default', defaultOptions);
194
+ let isVIPUser;
195
+ try {
196
+ const currentUser = await (0, _user.getCurrentUserInfo)(true);
197
+ isVIPUser = currentUser?.isVIP ?? false;
198
+ } catch (err) {
199
+ isVIPUser = false;
200
+ }
193
201
  if (suppressPrompts) {
194
202
  preselectedOptions = {
195
203
  ...defaultOptions,
@@ -208,7 +216,7 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
208
216
  elasticsearch: false,
209
217
  php: preselectedOptions.php ? resolvePhpVersion(preselectedOptions.php) : await promptForPhpVersion(resolvePhpVersion(defaultOptions.php ?? _devEnvironment.DEV_ENVIRONMENT_DEFAULTS.phpVersion)),
210
218
  mariadb: preselectedOptions.mariadb ?? defaultOptions.mariadb,
211
- mediaRedirectDomain: preselectedOptions.mediaRedirectDomain ?? '',
219
+ mediaRedirectDomain: preselectedOptions.mediaRedirectDomain ?? defaultOptions.mediaRedirectDomain ?? '',
212
220
  wordpress: {
213
221
  mode: 'image',
214
222
  tag: ''
@@ -241,16 +249,19 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
241
249
  if (setMediaRedirectDomain) {
242
250
  instanceData.mediaRedirectDomain = defaultOptions.mediaRedirectDomain;
243
251
  }
244
- }
245
- instanceData.wordpress = await processWordPress((preselectedOptions.wordpress ?? '').toString(), (defaultOptions.wordpress ?? '').toString());
246
- for (const component of _devEnvironment.DEV_ENVIRONMENT_COMPONENTS) {
247
- const option = (preselectedOptions[component] ?? '').toString();
248
- const defaultValue = (defaultOptions[component] ?? '').toString();
249
-
250
- // eslint-disable-next-line no-await-in-loop
251
- const result = await processComponent(component, option, defaultValue, suppressPrompts);
252
- instanceData[component] = result;
253
- }
252
+ } else if (!create && defaultOptions.mediaRedirectDomain) {
253
+ const mediaRedirectPromptText = 'URL to redirect for missing media files ("n" to disable)?';
254
+ const mediaRedirectDomain = await promptForURL(mediaRedirectPromptText, defaultOptions.mediaRedirectDomain);
255
+ instanceData.mediaRedirectDomain = mediaRedirectDomain;
256
+ }
257
+ instanceData.wordpress = await processWordPress(preselectedOptions.wordpress ?? '', defaultOptions.wordpress ?? '');
258
+ instanceData.appCode = await processComponent('appCode', preselectedOptions.appCode ?? '', defaultOptions.appCode ?? '', suppressPrompts, validateAppCodeLocalPath);
259
+ let preselectedMU = preselectedOptions.muPlugins;
260
+ const defaultMU = defaultOptions.muPlugins;
261
+ if (!preselectedMU && !isVIPUser && (!defaultMU || ['demo', 'image'].includes(defaultMU))) {
262
+ preselectedMU = 'image';
263
+ }
264
+ instanceData.muPlugins = await processComponent('muPlugins', preselectedMU ?? '', defaultMU ?? '', suppressPrompts, validateMuPluginsLocalPath);
254
265
  debug(`Processing elasticsearch with preselected "%s"`, preselectedOptions.elasticsearch);
255
266
  if ('elasticsearch' in preselectedOptions) {
256
267
  instanceData.elasticsearch = Boolean(preselectedOptions.elasticsearch);
@@ -295,48 +306,41 @@ async function processWordPress(preselectedValue, defaultValue) {
295
306
  debug(result);
296
307
  return result;
297
308
  }
298
- async function processComponent(component, preselectedValue, defaultValue, suppressPrompts = false) {
299
- debug(`processing a component '${component}', with preselected/default - ${preselectedValue}/${defaultValue}`);
300
- let result;
301
- let allowLocal = true;
302
- if (component === 'muPlugins') {
303
- try {
304
- const currentUser = await (0, _user.getCurrentUserInfo)(true);
305
- allowLocal = currentUser?.isVIP ?? false;
306
- } catch (err) {
307
- allowLocal = false;
308
- }
309
+ async function processComponent(componentType, preselectedValue, defaultValue, suppressPrompts, localPathValidator) {
310
+ debug(`Processing the '${componentType}' component, with preselected = %s, default = %s`, preselectedValue, defaultValue);
311
+ const defaultObject = defaultValue ? processComponentOptionInput(defaultValue, true) : null;
312
+ if (preselectedValue && !suppressPrompts) {
313
+ console.log('%s Path to your local %s: %s', _chalk.default.green('✓'), componentDisplayNames[componentType], preselectedValue);
309
314
  }
310
- const defaultObject = defaultValue ? processComponentOptionInput(defaultValue, allowLocal) : null;
311
- if (preselectedValue) {
312
- result = processComponentOptionInput(preselectedValue, allowLocal);
313
- if (!suppressPrompts) {
314
- console.log(`${_chalk.default.green('✓')} Path to your local ${componentDisplayNames[component]}: ${preselectedValue}`);
315
- }
316
- } else {
317
- result = await promptForComponent(component, allowLocal, defaultObject);
315
+ let result = preselectedValue ? processComponentOptionInput(preselectedValue, true) : await promptForComponent(componentType, true, defaultObject);
316
+ debug(result);
317
+ if (result.mode === 'local') {
318
+ result = await validateLocalPath(result, localPathValidator, componentType, defaultObject);
318
319
  }
319
320
  debug(result);
320
- while ('local' === result.mode) {
321
- const resolvedPath = resolvePath(result.dir ?? '');
322
- result.dir = resolvedPath;
321
+ return result;
322
+ }
323
+ async function validateLocalPath(config, validator, componentName, defaultObject) {
324
+ while (config.mode === 'local') {
325
+ const resolvedPath = resolvePath(config.dir ?? '');
326
+ config.dir = resolvedPath;
323
327
  const {
324
328
  result: isPathValid,
325
329
  message
326
- } = validateLocalPath(component, resolvedPath);
330
+ } = validator(resolvedPath);
327
331
  if (isPathValid) {
328
332
  break;
329
333
  } else if (isStdinTTY) {
330
334
  console.log(_chalk.default.yellow('Warning:'), message);
331
335
  // eslint-disable-next-line no-await-in-loop
332
- result = await promptForComponent(component, allowLocal, defaultObject);
336
+ config = await promptForComponent(componentName, true, defaultObject);
333
337
  } else {
334
338
  throw new Error(message);
335
339
  }
336
340
  }
337
- return result;
341
+ return config;
338
342
  }
339
- function validateLocalPath(component, providedPath) {
343
+ function validateMuPluginsLocalPath(providedPath) {
340
344
  if (!isNonEmptyDirectory(providedPath)) {
341
345
  const message = `Provided path "${providedPath}" does not point to a valid or existing directory.`;
342
346
  return {
@@ -344,24 +348,35 @@ function validateLocalPath(component, providedPath) {
344
348
  message
345
349
  };
346
350
  }
347
- if (component === 'appCode') {
348
- const files = ['languages', 'plugins', 'themes', 'private', 'images', 'client-mu-plugins', 'vip-config'];
349
- const missingFiles = [];
350
- for (const file of files) {
351
- const filePath = _path.default.resolve(providedPath, file);
352
- if (!(0, _nodeFs.existsSync)(filePath)) {
353
- missingFiles.push(file);
354
- }
355
- }
356
- if (missingFiles.length > 0) {
357
- // eslint-disable-next-line max-len
358
- const message = `Provided path "${providedPath}" is missing following files/folders: ${missingFiles.join(', ')}. Learn more: https://docs.wpvip.com/wordpress-skeleton/`;
359
- return {
360
- result: false,
361
- message
362
- };
351
+ return {
352
+ result: true,
353
+ message: ''
354
+ };
355
+ }
356
+ function validateAppCodeLocalPath(providedPath) {
357
+ if (!isNonEmptyDirectory(providedPath)) {
358
+ const message = `Provided path "${providedPath}" does not point to a valid or existing directory.`;
359
+ return {
360
+ result: false,
361
+ message
362
+ };
363
+ }
364
+ const files = ['languages', 'plugins', 'themes', 'private', 'images', 'client-mu-plugins', 'vip-config'];
365
+ const missingFiles = [];
366
+ for (const file of files) {
367
+ const filePath = _path.default.resolve(providedPath, file);
368
+ if (!(0, _nodeFs.existsSync)(filePath)) {
369
+ missingFiles.push(file);
363
370
  }
364
371
  }
372
+ if (missingFiles.length > 0) {
373
+ // eslint-disable-next-line max-len
374
+ const message = `Provided path "${providedPath}" is missing following files/folders: ${missingFiles.join(', ')}. Learn more: https://docs.wpvip.com/wordpress-skeleton/`;
375
+ return {
376
+ result: false,
377
+ message
378
+ };
379
+ }
365
380
  return {
366
381
  result: true,
367
382
  message: ''
@@ -399,6 +414,36 @@ async function promptForText(message, initial) {
399
414
  }
400
415
  return (result.input || '').trim();
401
416
  }
417
+ async function promptForURL(message, initial) {
418
+ let result = {
419
+ input: initial
420
+ };
421
+ if (isStdinTTY) {
422
+ const URLValidator = value => {
423
+ if (!value.trim() || value === 'n') {
424
+ return true;
425
+ }
426
+ try {
427
+ const url = new URL(value);
428
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
429
+ return 'value must be a http:// or https:// URL';
430
+ }
431
+ return true;
432
+ } catch {
433
+ return 'value needs to be a valid URL or an empty string';
434
+ }
435
+ };
436
+ result = await (0, _enquirer.prompt)({
437
+ type: 'input',
438
+ name: 'input',
439
+ message,
440
+ initial,
441
+ validate: URLValidator
442
+ });
443
+ }
444
+ const retval = result.input.trim();
445
+ return retval === 'n' ? '' : retval;
446
+ }
402
447
  const multisiteOptions = ['subdomain', 'subdirectory'];
403
448
  async function promptForMultisite(message, initial) {
404
449
  // `undefined` is used here only because our tests need overhauling
@@ -676,30 +721,92 @@ function getEnvTrackingInfo(slug) {
676
721
  };
677
722
  }
678
723
  }
724
+ // Map of supported editors and their configurations
725
+ const SUPPORTED_EDITORS = {
726
+ vscode: {
727
+ displayName: 'VS Code',
728
+ candidates: ['code', 'code-insiders', 'codium'],
729
+ workspace: {
730
+ extension: 'code-workspace',
731
+ generator: _devEnvironmentCore.generateVSCodeWorkspace
732
+ }
733
+ },
734
+ cursor: {
735
+ displayName: 'Cursor',
736
+ candidates: ['cursor'],
737
+ workspace: {
738
+ extension: 'code-workspace',
739
+ generator: _devEnvironmentCore.generateVSCodeWorkspace
740
+ }
741
+ },
742
+ phpstorm: {
743
+ displayName: 'PHPStorm',
744
+ candidates: ['phpstorm', 'phpstorm64.exe'],
745
+ errors: {
746
+ notFound: `PHPStorm launcher was not detected in the expected path.\nPlease follow the setup instructions: https://www.jetbrains.com/help/phpstorm/working-with-the-ide-features-from-command-line.html#standalone`
747
+ },
748
+ workspace: {
749
+ extension: 'iml',
750
+ generator: _devEnvironmentCore.generatePHPStormWorkspace
751
+ }
752
+ },
753
+ windsurf: {
754
+ displayName: 'Windsurf',
755
+ candidates: ['windsurf'],
756
+ workspace: {
757
+ extension: 'code-workspace',
758
+ generator: _devEnvironmentCore.generateVSCodeWorkspace
759
+ }
760
+ }
761
+ };
762
+
763
+ // Helper to get workspace path for any editor
764
+ function getWorkspacePath(slug, editor) {
765
+ const {
766
+ workspace
767
+ } = SUPPORTED_EDITORS[editor];
768
+ if (editor === 'phpstorm') {
769
+ return _path.default.join((0, _devEnvironmentCore.getEnvironmentPath)(slug), '.idea', `${slug}.${workspace.extension}`);
770
+ }
771
+ return _path.default.join((0, _devEnvironmentCore.getEnvironmentPath)(slug), `${slug}.${workspace.extension}`);
772
+ }
679
773
  function postStart(slug, options) {
680
- if (options.openVSCode) {
681
- launchVSCode(slug);
774
+ let editorType;
775
+ if (options.editor) {
776
+ const editor = options.editor.toLowerCase();
777
+ if (!Object.keys(SUPPORTED_EDITORS).includes(editor)) {
778
+ throw new Error(`Invalid editor specified. Supported editors are: ${Object.keys(SUPPORTED_EDITORS).join(', ')}`);
779
+ }
780
+ editorType = editor;
781
+ } else if (options.vscode) {
782
+ editorType = 'vscode';
783
+ }
784
+ if (editorType) {
785
+ launchEditor(slug, editorType);
682
786
  }
683
787
  }
684
- const launchVSCode = slug => {
685
- const workspacePath = (0, _devEnvironmentCore.getVSCodeWorkspacePath)(slug);
788
+ const launchEditor = (slug, type) => {
789
+ const editorConfig = SUPPORTED_EDITORS[type];
790
+ const workspacePath = getWorkspacePath(slug, type);
686
791
  if ((0, _nodeFs.existsSync)(workspacePath)) {
687
- console.log('VS Code workspace already exists, skipping creation.');
792
+ console.log('Project already exists, skipping creation.');
688
793
  } else {
689
- (0, _devEnvironmentCore.generateVSCodeWorkspace)(slug);
690
- console.log('VS Code workspace generated');
691
- }
692
- const vsCodeExecutable = getVSCodeExecutable();
693
- if (vsCodeExecutable) {
694
- (0, _child_process.spawn)(vsCodeExecutable, [workspacePath], {
794
+ editorConfig.workspace.generator(slug);
795
+ console.log(`${editorConfig.displayName} project generated`);
796
+ }
797
+ console.log(`Project file location:\n${workspacePath}\n`);
798
+ const executable = findExecutable(editorConfig.candidates);
799
+ if (executable) {
800
+ // For PHPStorm, pass the environment home folder instead of the workspace path
801
+ const launchPath = type === 'phpstorm' ? (0, _devEnvironmentCore.getEnvironmentPath)(slug) : workspacePath;
802
+ (0, _child_process.spawn)(executable, [launchPath], {
695
803
  shell: process.platform === 'win32'
696
804
  });
697
805
  } else {
698
- console.log(`VS Code was not detected in the expected path. VS Code Workspace file location:\n${workspacePath}`);
806
+ console.log(editorConfig.errors?.notFound || `${editorConfig.displayName} was not detected in the expected path.`);
699
807
  }
700
808
  };
701
- const getVSCodeExecutable = () => {
702
- const candidates = ['code', 'code-insiders', 'codium'];
809
+ const findExecutable = candidates => {
703
810
  for (const candidate of candidates) {
704
811
  const result = (0, _shelljs.which)(candidate);
705
812
  if (result) {