@automattic/vip 2.27.0-dev2 → 2.27.0-dev4

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.
@@ -37,6 +37,7 @@ var _devEnvironment = require("../constants/dev-environment");
37
37
  var _devEnvironmentCore = require("./dev-environment-core");
38
38
  var _devEnvironmentLando = require("./dev-environment-lando");
39
39
  var _userError = _interopRequireDefault(require("../user-error"));
40
+ var _devEnvironmentConfigurationFile = require("./dev-environment-configuration-file");
40
41
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
41
42
  /**
42
43
  *
@@ -128,13 +129,13 @@ const verifyDNSResolution = async slug => {
128
129
  };
129
130
  const VALIDATION_STEPS = [{
130
131
  id: 'docker',
131
- name: 'Check for docker installation'
132
+ name: 'Check for Docker installation'
132
133
  }, {
133
134
  id: 'compose',
134
135
  name: 'Check for docker-compose installation'
135
136
  }, {
136
137
  id: 'access',
137
- name: 'Check access to docker for current user'
138
+ name: 'Check Docker connectivity'
138
139
  }, {
139
140
  id: 'dns',
140
141
  name: 'Check DNS resolution'
@@ -181,7 +182,7 @@ const validateDependencies = async (lando, slug, quiet) => {
181
182
  debug('Validation checks completed in %d ms', duration);
182
183
  };
183
184
  exports.validateDependencies = validateDependencies;
184
- function getEnvironmentName(options) {
185
+ async function getEnvironmentName(options) {
185
186
  if (options.slug) {
186
187
  return options.slug;
187
188
  }
@@ -194,6 +195,12 @@ function getEnvironmentName(options) {
194
195
  const message = `This command does not support @app.env notation. Use '--slug=${appName}' to target the local environment.`;
195
196
  throw new _userError.default(message);
196
197
  }
198
+ const configurationFileOptions = await (0, _devEnvironmentConfigurationFile.getConfigurationFileOptions)();
199
+ if (configurationFileOptions.slug) {
200
+ const slug = configurationFileOptions.slug;
201
+ console.log(`Using environment ${_chalk.default.blue.bold(slug)} from ${_chalk.default.gray(_devEnvironmentConfigurationFile.CONFIGURATION_FILE_NAME)}\n`);
202
+ return slug;
203
+ }
197
204
  const envs = (0, _devEnvironmentCore.getAllEnvironmentNames)();
198
205
  if (envs.length === 1) {
199
206
  return envs[0];
@@ -205,8 +212,9 @@ function getEnvironmentName(options) {
205
212
  return DEFAULT_SLUG; // Fall back to the default slug if we don't have any, e.g. during the env creation purpose
206
213
  }
207
214
 
208
- function getEnvironmentStartCommand(slug) {
209
- if (!slug) {
215
+ function getEnvironmentStartCommand(slug, configurationFileOptions) {
216
+ const isUsingConfigurationFileSlug = Object.keys(configurationFileOptions).length > 0 && configurationFileOptions.slug === slug;
217
+ if (!slug || isUsingConfigurationFileSlug) {
210
218
  return `${_devEnvironment.DEV_ENVIRONMENT_FULL_COMMAND} start`;
211
219
  }
212
220
  return `${_devEnvironment.DEV_ENVIRONMENT_FULL_COMMAND} start --slug ${slug}`;
@@ -272,7 +280,7 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
272
280
  }
273
281
  const instanceData = {
274
282
  wpTitle: preselectedOptions.title || (await promptForText('WordPress site title', defaultOptions.title || _devEnvironment.DEV_ENVIRONMENT_DEFAULTS.title)),
275
- multisite: 'multisite' in preselectedOptions ? preselectedOptions.multisite : await promptForBoolean(multisiteText, !!multisiteDefault),
283
+ multisite: preselectedOptions.multisite !== undefined ? preselectedOptions.multisite : await promptForBoolean(multisiteText, !!multisiteDefault),
276
284
  elasticsearch: false,
277
285
  php: preselectedOptions.php ? resolvePhpVersion(preselectedOptions.php) : await promptForPhpVersion(resolvePhpVersion(defaultOptions.php || _devEnvironment.DEV_ENVIRONMENT_DEFAULTS.phpVersion)),
278
286
  mariadb: preselectedOptions.mariadb || defaultOptions.mariadb,
@@ -312,7 +320,7 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
312
320
  const defaultValue = ((_defaultOptions$compo = defaultOptions[component]) !== null && _defaultOptions$compo !== void 0 ? _defaultOptions$compo : '').toString();
313
321
 
314
322
  // eslint-disable-next-line no-await-in-loop
315
- const result = await processComponent(component, option, defaultValue);
323
+ const result = await processComponent(component, option, defaultValue, suppressPrompts);
316
324
  if (null === result) {
317
325
  throw new Error('processComponent() returned null');
318
326
  }
@@ -337,14 +345,14 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
337
345
  debug('Instance data after prompts', instanceData);
338
346
  return instanceData;
339
347
  }
340
- async function processComponent(component, preselectedValue, defaultValue) {
348
+ async function processComponent(component, preselectedValue, defaultValue, suppressPrompts = false) {
341
349
  debug(`processing a component '${component}', with preselected/default - ${preselectedValue}/${defaultValue}`);
342
350
  let result = null;
343
351
  const allowLocal = component !== 'wordpress';
344
352
  const defaultObject = defaultValue ? processComponentOptionInput(defaultValue, allowLocal) : null;
345
353
  if (preselectedValue) {
346
354
  result = processComponentOptionInput(preselectedValue, allowLocal);
347
- if (allowLocal) {
355
+ if (allowLocal && suppressPrompts === false) {
348
356
  console.log(`${_chalk.default.green('✓')} Path to your local ${componentDisplayNames[component]}: ${preselectedValue}`);
349
357
  }
350
358
  } else {
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CONFIGURATION_FILE_NAME = void 0;
7
+ exports.getConfigurationFileOptions = getConfigurationFileOptions;
8
+ exports.mergeConfigurationFileOptions = mergeConfigurationFileOptions;
9
+ exports.printConfigurationFile = printConfigurationFile;
10
+ var _debug = _interopRequireDefault(require("debug"));
11
+ var _fs = _interopRequireDefault(require("fs"));
12
+ var _path = _interopRequireDefault(require("path"));
13
+ var _chalk = _interopRequireDefault(require("chalk"));
14
+ var _jsYaml = _interopRequireWildcard(require("js-yaml"));
15
+ var exit = _interopRequireWildcard(require("../cli/exit"));
16
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
17
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
18
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19
+ /**
20
+ *
21
+ * @format
22
+ */
23
+
24
+ /**
25
+ * External dependencies
26
+ */
27
+
28
+ /**
29
+ * Internal dependencies
30
+ */
31
+
32
+ const debug = (0, _debug.default)('@automattic/vip:bin:dev-environment');
33
+ const CONFIGURATION_FILE_NAME = '.vip-dev-env.yml';
34
+ exports.CONFIGURATION_FILE_NAME = CONFIGURATION_FILE_NAME;
35
+ async function getConfigurationFileOptions() {
36
+ const configurationFilePath = _path.default.join(process.cwd(), CONFIGURATION_FILE_NAME);
37
+ let configurationFileContents = '';
38
+ const fileExists = await _fs.default.promises.access(configurationFilePath, _fs.default.R_OK).then(() => true).catch(() => false);
39
+ if (fileExists) {
40
+ debug('Reading configuration file from:', configurationFilePath);
41
+ configurationFileContents = await _fs.default.promises.readFile(configurationFilePath, 'utf8');
42
+ } else {
43
+ return {};
44
+ }
45
+ let configurationFromFile = {};
46
+ try {
47
+ configurationFromFile = _jsYaml.default.load(configurationFileContents, {
48
+ // Only allow strings, arrays, and objects to be parsed from configuration file
49
+ // This causes number-looking values like `php: 8.1` to be parsed directly into strings
50
+ schema: _jsYaml.FAILSAFE_SCHEMA
51
+ });
52
+ } catch (err) {
53
+ const messageToShow = `Configuration file ${_chalk.default.grey(CONFIGURATION_FILE_NAME)} could not be loaded:\n` + err.toString();
54
+ exit.withError(messageToShow);
55
+ }
56
+ const configuration = await sanitizeConfiguration(configurationFromFile).catch(async ({
57
+ message
58
+ }) => {
59
+ exit.withError(message);
60
+ return {};
61
+ });
62
+ debug('Sanitized configuration from file:', configuration);
63
+ return configuration;
64
+ }
65
+ async function sanitizeConfiguration(configuration) {
66
+ const genericConfigurationError = `Configuration file ${_chalk.default.grey(CONFIGURATION_FILE_NAME)} is available but ` + `couldn't be loaded. Ensure there is a ${_chalk.default.cyan('configuration-version')} and ${_chalk.default.cyan('slug')} ` + `configured. For example:\n\n${_chalk.default.grey(getConfigurationFileExample())}`;
67
+ if (Array.isArray(configuration) || typeof configuration !== 'object') {
68
+ throw new Error(genericConfigurationError);
69
+ } else if (configuration['configuration-version'] === undefined || configuration.slug === undefined) {
70
+ throw new Error(genericConfigurationError);
71
+ }
72
+ const validVersions = getAllConfigurationFileVersions().map(version => _chalk.default.cyan(version)).join(', ');
73
+ if (!isValidConfigurationFileVersion(configuration['configuration-version'])) {
74
+ throw new Error(`Configuration file ${_chalk.default.grey(CONFIGURATION_FILE_NAME)} has an invalid ` + `${_chalk.default.cyan('configuration-version')} key. Update to a supported version. For example:\n\n` + _chalk.default.grey(getConfigurationFileExample()) + `\nSupported configuration versions: ${validVersions}.\n`);
75
+ }
76
+ const stringToBooleanIfDefined = value => {
77
+ if (value === undefined || !['true', 'false'].includes(value)) {
78
+ return undefined;
79
+ }
80
+ return value === 'true';
81
+ };
82
+ const sanitizedConfiguration = {
83
+ 'configuration-version': configuration['configuration-version'],
84
+ slug: configuration.slug,
85
+ title: configuration.title,
86
+ multisite: stringToBooleanIfDefined(configuration.multisite),
87
+ php: configuration.php,
88
+ wordpress: configuration.wordpress,
89
+ 'mu-plugins': configuration['mu-plugins'],
90
+ 'app-code': configuration['app-code'],
91
+ elasticsearch: stringToBooleanIfDefined(configuration.elasticsearch),
92
+ phpmyadmin: stringToBooleanIfDefined(configuration.phpmyadmin),
93
+ xdebug: stringToBooleanIfDefined(configuration.xdebug),
94
+ mailhog: stringToBooleanIfDefined(configuration.mailhog)
95
+ };
96
+
97
+ // Remove undefined values
98
+ Object.keys(sanitizedConfiguration).forEach(key => sanitizedConfiguration[key] === undefined && delete sanitizedConfiguration[key]);
99
+ return sanitizedConfiguration;
100
+ }
101
+ function mergeConfigurationFileOptions(preselectedOptions, configurationFileOptions) {
102
+ // configurationFileOptions holds different parameters than present in
103
+ // preselectedOptions like "slug", and friendly-named parameters (e.g.
104
+ // 'app-code' vs 'appCode'). Selectively merge configurationFileOptions
105
+ // parameters into preselectedOptions.
106
+ const configurationFileInstanceOptions = {
107
+ title: configurationFileOptions.title,
108
+ multisite: configurationFileOptions.multisite,
109
+ php: configurationFileOptions.php,
110
+ wordpress: configurationFileOptions.wordpress,
111
+ muPlugins: configurationFileOptions['mu-plugins'],
112
+ appCode: configurationFileOptions['app-code'],
113
+ elasticsearch: configurationFileOptions.elasticsearch,
114
+ phpmyadmin: configurationFileOptions.phpmyadmin,
115
+ xdebug: configurationFileOptions.xdebug,
116
+ mailhog: configurationFileOptions.mailhog
117
+ };
118
+ const mergedOptions = {};
119
+ Object.keys(configurationFileInstanceOptions).forEach(key => {
120
+ // preselectedOptions (supplied from command-line) override configurationFileOptions
121
+ if (preselectedOptions[key] !== undefined) {
122
+ mergedOptions[key] = preselectedOptions[key];
123
+ } else if (configurationFileInstanceOptions[key] !== undefined) {
124
+ mergedOptions[key] = configurationFileInstanceOptions[key];
125
+ }
126
+ });
127
+ return mergedOptions;
128
+ }
129
+ function printConfigurationFile(configurationOptions) {
130
+ const isConfigurationFileEmpty = Object.keys(configurationOptions).length === 0;
131
+ if (isConfigurationFileEmpty) {
132
+ return;
133
+ }
134
+
135
+ // Customized formatter because Lando's printTable() automatically uppercases keys
136
+ // which may be confusing for YAML configuration
137
+ const settingLines = [];
138
+ for (const [key, value] of Object.entries(configurationOptions)) {
139
+ settingLines.push(`${_chalk.default.cyan(key)}: ${String(value)}`);
140
+ }
141
+ console.log(settingLines.join('\n') + '\n');
142
+ }
143
+ const CONFIGURATION_FILE_VERSIONS = ['0.preview-unstable'];
144
+ function getAllConfigurationFileVersions() {
145
+ return CONFIGURATION_FILE_VERSIONS;
146
+ }
147
+ function getLatestConfigurationFileVersion() {
148
+ return CONFIGURATION_FILE_VERSIONS[CONFIGURATION_FILE_VERSIONS.length - 1];
149
+ }
150
+ function isValidConfigurationFileVersion(version) {
151
+ return CONFIGURATION_FILE_VERSIONS.includes(version);
152
+ }
153
+ function getConfigurationFileExample() {
154
+ return `configuration-version: ${getLatestConfigurationFileVersion()}
155
+ slug: dev-site
156
+ php: 8.0
157
+ wordpress: 6.0
158
+ app-code: ./site-code
159
+ mu-plugins: image
160
+ multisite: false
161
+ phpmyadmin: true
162
+ elasticsearch: true
163
+ xdebug: true
164
+ `;
165
+ }
@@ -17,6 +17,7 @@ exports.printAllEnvironmentsInfo = printAllEnvironmentsInfo;
17
17
  exports.printEnvironmentInfo = printEnvironmentInfo;
18
18
  exports.readEnvironmentData = readEnvironmentData;
19
19
  exports.resolveImportPath = resolveImportPath;
20
+ exports.showLogs = showLogs;
20
21
  exports.startEnvironment = startEnvironment;
21
22
  exports.stopEnvironment = stopEnvironment;
22
23
  exports.updateEnvironment = updateEnvironment;
@@ -200,6 +201,18 @@ function parseComponentForInfo(component) {
200
201
  }
201
202
  return component.tag || '[demo-image]';
202
203
  }
204
+ async function showLogs(lando, slug, options = {}) {
205
+ debug('Will display logs command on env', slug, 'with options', options);
206
+ const instancePath = getEnvironmentPath(slug);
207
+ debug('Instance path for', slug, 'is:', instancePath);
208
+ if (options.service) {
209
+ const appInfo = await (0, _devEnvironmentLando.landoInfo)(lando, instancePath);
210
+ if (!appInfo.services.includes(options.service)) {
211
+ throw new _userError.default(`Service '${options.service}' not found. Please choose from one: ${appInfo.services}`);
212
+ }
213
+ }
214
+ return (0, _devEnvironmentLando.landoLogs)(lando, instancePath, options);
215
+ }
203
216
  async function printEnvironmentInfo(lando, slug, options) {
204
217
  debug('Will get info for an environment', slug);
205
218
  const instancePath = getEnvironmentPath(slug);
@@ -9,7 +9,9 @@ exports.isEnvUp = isEnvUp;
9
9
  exports.landoDestroy = landoDestroy;
10
10
  exports.landoExec = landoExec;
11
11
  exports.landoInfo = landoInfo;
12
+ exports.landoLogs = landoLogs;
12
13
  exports.landoRebuild = landoRebuild;
14
+ exports.landoShell = landoShell;
13
15
  exports.landoStart = landoStart;
14
16
  exports.landoStop = landoStop;
15
17
  exports.validateDockerAccess = validateDockerAccess;
@@ -42,7 +44,6 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
42
44
  */
43
45
  const DEBUG_KEY = '@automattic/vip:bin:dev-environment';
44
46
  const debug = (0, _debug.default)(DEBUG_KEY);
45
-
46
47
  /**
47
48
  * @return {Promise<object>} Lando configuration
48
49
  */
@@ -124,7 +125,6 @@ async function landoRecovery(lando, instancePath, error) {
124
125
  console.error(_chalk.default.green('Recovery successful, trying to initialize again...'));
125
126
  try {
126
127
  const app = lando.getApp(instancePath);
127
- addHooks(app, lando);
128
128
  await app.init();
129
129
  return app;
130
130
  } catch (initError) {
@@ -142,7 +142,6 @@ async function getLandoApplication(lando, instancePath) {
142
142
  let app;
143
143
  try {
144
144
  app = lando.getApp(instancePath);
145
- await addHooks(app, lando);
146
145
  await app.init();
147
146
  } catch (error) {
148
147
  app = await landoRecovery(lando, instancePath, error);
@@ -152,24 +151,8 @@ async function getLandoApplication(lando, instancePath) {
152
151
  }
153
152
  async function bootstrapLando() {
154
153
  const lando = new _lando.default(await getLandoConfig());
155
- await lando.bootstrap();
156
- return lando;
157
- }
158
- async function landoStart(lando, instancePath) {
159
- debug('Will start lando app on path:', instancePath);
160
- const app = await getLandoApplication(lando, instancePath);
161
- await app.start();
162
- }
163
- async function landoRebuild(lando, instancePath) {
164
- debug('Will rebuild lando app on path:', instancePath);
165
- const app = await getLandoApplication(lando, instancePath);
166
- await ensureNoOrphantProxyContainer(lando);
167
- await app.rebuild();
168
- }
169
- async function addHooks(app, lando) {
170
- app.events.on('post-start', 1, () => healthcheckHook(app, lando));
171
154
  lando.events.once('pre-engine-build', async data => {
172
- const instanceData = (0, _devEnvironmentCore.readEnvironmentData)(app._name);
155
+ const instanceData = (0, _devEnvironmentCore.readEnvironmentData)(data.name);
173
156
  let registryResolvable = false;
174
157
  try {
175
158
  registryResolvable = (await _dns.default.promises.lookup('ghcr.io')).address || false;
@@ -178,70 +161,42 @@ async function addHooks(app, lando) {
178
161
  debug('Registry ghcr.io is not resolvable, image pull might be broken.');
179
162
  registryResolvable = false;
180
163
  }
181
- data.opts.pull = registryResolvable && instanceData.pullAfter < Date.now();
182
- if (Array.isArray(data.opts.pullable) && Array.isArray(data.opts.local) && data.opts.local.length === 0 && !data.opts.pull) {
164
+ const pull = registryResolvable && (instanceData.pullAfter || 0) < Date.now();
165
+ if (Array.isArray(data.opts.pullable) && Array.isArray(data.opts.local) && data.opts.local.length === 0 && !pull) {
166
+ // Setting `data.opts.pullable` to an empty array prevents Lando from pulling images with `docker pull`.
167
+ // Note that if some of the images are not available, they will still be pulled by `docker-compose`.
183
168
  data.opts.local = data.opts.pullable;
184
169
  data.opts.pullable = [];
185
170
  }
186
- if (data.opts.pull || !instanceData.pullAfter) {
171
+ if (pull || !instanceData.pullAfter) {
187
172
  instanceData.pullAfter = Date.now() + 7 * 24 * 60 * 60 * 1000;
188
- (0, _devEnvironmentCore.writeEnvironmentData)(app._name, instanceData);
173
+ (0, _devEnvironmentCore.writeEnvironmentData)(data.name, instanceData);
189
174
  }
190
175
  });
176
+ await lando.bootstrap();
177
+ return lando;
191
178
  }
192
- const healthChecks = {
193
- database: 'mysql -uroot --silent --execute "SHOW DATABASES;"',
194
- elasticsearch: "curl -s --noproxy '*' -XGET localhost:9200",
195
- php: '[[ -f /wp/wp-includes/pomo/mo.php ]]'
196
- };
197
- async function healthcheckHook(app, lando) {
198
- const now = new Date();
199
- try {
200
- await lando.Promise.retry(async () => {
201
- const list = await lando.engine.list({
202
- project: app.project
203
- });
204
- const notHealthyContainers = [];
205
- const checkPromises = [];
206
- const containerOrder = [];
207
- for (const container of list) {
208
- if (healthChecks[container.service]) {
209
- debug(`Testing ${container.service}: ${healthChecks[container.service]}`);
210
- containerOrder.push(container);
211
- checkPromises.push(app.engine.run({
212
- id: container.id,
213
- cmd: healthChecks[container.service],
214
- compose: app.compose,
215
- project: app.project,
216
- opts: {
217
- silent: true,
218
- noTTY: true,
219
- cstdio: 'pipe',
220
- services: [container.service]
221
- }
222
- }));
223
- }
224
- }
225
- const results = await Promise.allSettled(checkPromises);
226
- results.forEach((result, index) => {
227
- if (result.status === 'rejected') {
228
- debug(`${containerOrder[index].service} Health check failed`);
229
- notHealthyContainers.push(containerOrder[index]);
230
- }
231
- });
232
- if (notHealthyContainers.length) {
233
- notHealthyContainers.forEach(container => console.log(`Waiting for service ${container.service} ...`));
234
- return Promise.reject(notHealthyContainers);
235
- }
236
- }, {
237
- max: 20,
238
- backoff: 1000
239
- });
240
- } catch (containersWithFailingHealthCheck) {
241
- containersWithFailingHealthCheck.forEach(container => console.log(_chalk.default.yellow('WARNING:') + ` Service ${container.service} failed healthcheck`));
242
- }
243
- const duration = new Date().getTime() - now.getTime();
244
- debug(`Healthcheck completed in ${duration}ms`);
179
+ async function landoStart(lando, instancePath) {
180
+ debug('Will start lando app on path:', instancePath);
181
+ const app = await getLandoApplication(lando, instancePath);
182
+ await app.start();
183
+ }
184
+ async function landoLogs(lando, instancePath, options) {
185
+ debug('Will show lando logs on path:', instancePath, ' with options: ', options);
186
+ const app = await getLandoApplication(lando, instancePath);
187
+ const logTask = lando.tasks.find(task => task.command === 'logs');
188
+ await logTask.run({
189
+ follow: options.follow,
190
+ service: options.service,
191
+ timestamps: options.timestamps,
192
+ _app: app
193
+ });
194
+ }
195
+ async function landoRebuild(lando, instancePath) {
196
+ debug('Will rebuild lando app on path:', instancePath);
197
+ const app = await getLandoApplication(lando, instancePath);
198
+ await ensureNoOrphantProxyContainer(lando);
199
+ await app.rebuild();
245
200
  }
246
201
  async function landoStop(lando, instancePath) {
247
202
  debug('Will stop lando app on path:', instancePath);
@@ -404,6 +359,21 @@ async function landoExec(lando, instancePath, toolName, args, options) {
404
359
  process.argv = savedArgv;
405
360
  }
406
361
  }
362
+ async function landoShell(lando, instancePath, service, user, command) {
363
+ const app = await getLandoApplication(lando, instancePath);
364
+ const shellTask = lando.tasks.find(task => task.command === 'ssh');
365
+ if (!command.length) {
366
+ const interactive = process.stdin.isTTY ? '-i' : '';
367
+ command = ['/bin/sh', '-c', `if [ -x /bin/bash ]; then /bin/bash ${interactive}; else /bin/sh ${interactive}; fi; exit 0`];
368
+ }
369
+ debug('Running command "%o" in service "%s" as user "%s"', command, service, user);
370
+ await shellTask.run({
371
+ command,
372
+ service,
373
+ user,
374
+ _app: app
375
+ });
376
+ }
407
377
 
408
378
  /**
409
379
  * Sometimes the proxy network seems to disapper leaving only orphant stopped proxy container.
@@ -445,10 +415,10 @@ async function validateDockerInstalled(lando) {
445
415
  }
446
416
  async function validateDockerAccess(lando) {
447
417
  const docker = lando.engine.docker;
448
- lando.log.verbose('Fetching docker info to verify user is in docker group');
418
+ lando.log.verbose('Fetching docker info to verify Docker connection');
449
419
  try {
450
420
  await docker.info();
451
421
  } catch (error) {
452
- throw Error('Failed to connect to docker. Please verify that the current user is part of docker group and has access to docker commands.');
422
+ throw Error('Failed to connect to Docker. Please verify that Docker engine (service) is running and follow the troubleshooting instructions for your platform.');
453
423
  }
454
424
  }