@automattic/vip 4.0.0 → 4.0.2

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.
@@ -51,7 +51,7 @@ const appQuery = `
51
51
  requiredArgs: 0,
52
52
  module: 'dev-env-sync-sql',
53
53
  usage
54
- }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('table', 'The name of a table to include in the partial database sync. Accepts a string value and can be passed more than once with a different value, or add multiple values in a comma-separated list.').option('site-id', 'The ID of a network site to include in the partial database sync. Accepts an integer value (can be passed more than once with different values), or multiple integer values in a comma-separated list.').option('wpcli-command', 'Run a custom WP-CLI command that has logic to retrieve specific data for the partial database export.').option('config-file', 'A local configuration file that specifies the data to include in the partial database sync. Accepts a relative or absolute path to the file.', undefined).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
54
+ }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('table', 'The name of a table to include in the partial database sync. Accepts a string value and can be passed more than once with a different value, or add multiple values in a comma-separated list.').option('site-id', 'The ID of a network site to include in the partial database sync. Accepts an integer value (can be passed more than once with different values), or multiple integer values in a comma-separated list.', undefined, Number.parseInt).option('wpcli-command', 'Run a custom WP-CLI command that has logic to retrieve specific data for the partial database export.').option('config-file', 'A local configuration file that specifies the data to include in the partial database sync. Accepts a relative or absolute path to the file.', undefined).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
55
55
  const {
56
56
  app,
57
57
  env,
@@ -55,7 +55,7 @@ const appQuery = `
55
55
  module: 'export-sql',
56
56
  requiredArgs: 0,
57
57
  usage: 'vip export sql'
58
- }).option('output', 'Download the file to a specific local directory path with a custom file name.').option('table', 'The name of a table to include in the partial database export. Accepts a string value and can be passed more than once with a different value, or add multiple values in a comma-separated list.').option('site-id', 'The ID of a network site to include in the partial database export. Accepts an integer value and can be passed more than once with a different value, or add multiple values in a comma-separated list.').option('wpcli-command', 'Run a custom WP-CLI command that has logic to retrieve specific data for the partial database export.').option('config-file', 'A local configuration file that specifies the data to include in the partial database export. Accepts a relative or absolute path to the file.', undefined).option('generate-backup', 'Generate a fresh database backup and export an archived copy of that backup.').option('skip-download', 'Skip downloading the file.').examples(examples).argv(process.argv, async (arg, {
58
+ }).option('output', 'Download the file to a specific local directory path with a custom file name.').option('table', 'The name of a table to include in the partial database export. Accepts a string value and can be passed more than once with a different value, or add multiple values in a comma-separated list.').option('site-id', 'The ID of a network site to include in the partial database export. Accepts an integer value and can be passed more than once with a different value, or add multiple values in a comma-separated list.', undefined, Number.parseInt).option('wpcli-command', 'Run a custom WP-CLI command that has logic to retrieve specific data for the partial database export.').option('config-file', 'A local configuration file that specifies the data to include in the partial database export. Accepts a relative or absolute path to the file.', undefined).option('generate-backup', 'Generate a fresh database backup and export an archived copy of that backup.').option('skip-download', 'Skip downloading the file.').examples(examples).argv(process.argv, async (arg, {
59
59
  app,
60
60
  env,
61
61
  output,
@@ -218,7 +218,7 @@ const appQuery = exports.appQuery = `
218
218
  module: 'logs'
219
219
  }).option('type', 'Specify the type of Runtime Logs to retrieve. Accepts "batch" (only valid for WordPress environments).', 'app')
220
220
  // The default limit is set manually in the validateInputs function to address validation issues, avoiding incorrect replacement of the default value.
221
- .option('limit', `The maximum number of entries to return. Accepts an integer value between 1 and 5000 (defaults to ${LIMIT_DEFAULT}).`).option('follow', 'Output new entries as they are generated.').option('format', 'Render output in a particular format. Accepts “table“ (default), “csv“, “json“, and “text”.').examples([{
221
+ .option('limit', `The maximum number of entries to return. Accepts an integer value between 1 and 5000 (defaults to ${LIMIT_DEFAULT}).`, undefined, Number.parseInt).option('follow', 'Output new entries as they are generated.').option('format', 'Render output in a particular format. Accepts “table“ (default), “csv“, “json“, and “text”.').examples([{
222
222
  usage: 'vip @example-app.production logs',
223
223
  description: 'Retrieve up to 500 of the most recent entries of application Runtime Logs from web containers.'
224
224
  }, {
@@ -163,6 +163,7 @@ const appQuery = exports.appQuery = `
163
163
  name
164
164
  }
165
165
  `;
166
+ const parseLimit = value => Number.parseInt(String(value), 10);
166
167
  void (0, _command.default)({
167
168
  appContext: true,
168
169
  appQuery,
@@ -170,7 +171,7 @@ void (0, _command.default)({
170
171
  format: true,
171
172
  module: 'slowlogs',
172
173
  usage: baseUsage
173
- }).option('limit', 'Set the maximum number of log entries. Accepts an integer value between 1 and 500.', 500).examples([{
174
+ }).option('limit', 'Set the maximum number of log entries. Accepts an integer value between 1 and 500.', 500, parseLimit).examples([{
174
175
  description: 'Retrieve up to 500 of the most recent entries from the MySQL slow query logs in the default format.',
175
176
  usage: exampleUsage
176
177
  }, {
@@ -104,9 +104,13 @@ function createOptionDefinition(name, description, defaultValue, parseFn, usedSh
104
104
  flags,
105
105
  description,
106
106
  defaultValue,
107
- parser
107
+ parser,
108
+ normalizedShortName
108
109
  };
109
110
  }
111
+ function isOptionToken(arg) {
112
+ return arg !== '-' && arg.startsWith('-');
113
+ }
110
114
  class CommanderArgsCompat {
111
115
  constructor(opts) {
112
116
  this.details = {
@@ -115,21 +119,43 @@ class CommanderArgsCompat {
115
119
  this.sub = [];
116
120
  this.examplesList = [];
117
121
  this.usedShortNames = new Set();
122
+ this.shortOptionsExpectingValue = new Set();
123
+ this.longOptionsExpectingValue = new Set();
124
+ this.knownShortOptions = new Set();
125
+ this.knownLongOptions = new Set();
118
126
  this._opts = opts;
119
127
  this.program = new _commander.Command();
120
128
  this.program.allowUnknownOption(true);
121
129
  this.program.allowExcessArguments(true);
122
130
  this.program.helpOption(false);
123
131
  normalizeUsage(this.program, this._opts.usage);
132
+ this.optionDefaults = new Map();
124
133
  }
125
134
  option(name, description, defaultValue, parseFn) {
126
135
  const definition = createOptionDefinition(name, description, defaultValue, parseFn, this.usedShortNames);
127
136
  const {
128
137
  flags,
129
- parser
138
+ parser,
139
+ normalizedShortName
130
140
  } = definition;
141
+ const normalizedLongName = String(Array.isArray(name) ? name[1] : name).trim().replace(/^--?/, '');
142
+ const isValueOption = typeof defaultValue !== 'boolean';
143
+ this.knownLongOptions.add(normalizedLongName);
144
+ if (normalizedShortName) {
145
+ this.knownShortOptions.add(normalizedShortName);
146
+ }
147
+ if (isValueOption && normalizedShortName) {
148
+ this.shortOptionsExpectingValue.add(normalizedShortName);
149
+ }
150
+ if (isValueOption) {
151
+ this.longOptionsExpectingValue.add(normalizedLongName);
152
+ }
153
+
154
+ // When there's a parser, track the default separately and don't pass it to Commander.
155
+ // This prevents the parser from incorrectly accumulating the default value with the first explicit value.
131
156
  if (parser && defaultValue !== undefined) {
132
- this.program.option(flags, description, parser, defaultValue);
157
+ this.optionDefaults.set(normalizedLongName, defaultValue);
158
+ this.program.option(flags, description, parser);
133
159
  } else if (parser) {
134
160
  this.program.option(flags, description, parser);
135
161
  } else if (defaultValue !== undefined) {
@@ -191,22 +217,128 @@ class CommanderArgsCompat {
191
217
  return this.details.commands.some(entry => entry.usage === value);
192
218
  }
193
219
  parse(argv) {
194
- this.program.parse(argv, {
220
+ this.program.parse(this.normalizeShortOptionEqualsSyntax(argv), {
195
221
  from: 'node'
196
222
  });
197
223
  this.sub = this.program.args.slice();
198
- return this.program.opts();
224
+ const opts = this.program.opts();
225
+
226
+ // Apply tracked defaults for options with parsers
227
+ for (const [optionName, defaultValue] of this.optionDefaults) {
228
+ if (opts[optionName] === undefined) {
229
+ opts[optionName] = defaultValue;
230
+ }
231
+ }
232
+ return opts;
233
+ }
234
+ normalizeShortOptionEqualsSyntax(argv) {
235
+ const normalizedArgv = argv.slice(0, 2);
236
+ for (const arg of argv.slice(2)) {
237
+ const shortOptionEqualsMatch = /^-([A-Za-z0-9])=(.*)$/.exec(arg);
238
+ if (!shortOptionEqualsMatch) {
239
+ normalizedArgv.push(arg);
240
+ continue;
241
+ }
242
+ const shortOptionName = shortOptionEqualsMatch[1];
243
+ const optionValue = shortOptionEqualsMatch[2];
244
+ if (!this.shortOptionsExpectingValue.has(shortOptionName)) {
245
+ normalizedArgv.push(arg);
246
+ continue;
247
+ }
248
+ normalizedArgv.push(`-${shortOptionName}`, optionValue);
249
+ }
250
+ return normalizedArgv;
251
+ }
252
+ findSubcommand(argv) {
253
+ const searchStart = argv[2] === '--' ? 3 : 2;
254
+ const dashDashIndex = argv.indexOf('--', searchStart);
255
+ const searchEnd = dashDashIndex === -1 ? argv.length : dashDashIndex;
256
+ for (let index = searchStart; index < searchEnd; index++) {
257
+ const arg = argv[index];
258
+ if (this.isDefined(arg, 'commands')) {
259
+ return {
260
+ index,
261
+ name: arg
262
+ };
263
+ }
264
+ if (!isOptionToken(arg)) {
265
+ return null;
266
+ }
267
+ const nextArg = argv[index + 1];
268
+ const optionHasInlineValue = arg.includes('=');
269
+ const nextArgCouldBeOptionValue = nextArg && !isOptionToken(nextArg) && !this.isDefined(nextArg, 'commands');
270
+ if (!optionHasInlineValue && nextArgCouldBeOptionValue) {
271
+ index++;
272
+ }
273
+ }
274
+ if (dashDashIndex > -1 && this.isDefined(argv[dashDashIndex + 1], 'commands')) {
275
+ return {
276
+ index: dashDashIndex + 1,
277
+ name: argv[dashDashIndex + 1]
278
+ };
279
+ }
280
+ return null;
281
+ }
282
+ findUnknownOption(argv) {
283
+ const dashDashIndex = argv.indexOf('--', 2);
284
+ const searchEnd = dashDashIndex === -1 ? argv.length : dashDashIndex;
285
+ for (let index = 2; index < searchEnd; index++) {
286
+ const arg = argv[index];
287
+ if (!isOptionToken(arg)) {
288
+ continue;
289
+ }
290
+ if (arg.startsWith('--')) {
291
+ const [tokenName] = arg.slice(2).split('=');
292
+ if (!this.knownLongOptions.has(tokenName)) {
293
+ return tokenName;
294
+ }
295
+ const hasInlineValue = arg.includes('=');
296
+ const nextArg = argv[index + 1];
297
+ if (this.longOptionsExpectingValue.has(tokenName) && !hasInlineValue && nextArg && !isOptionToken(nextArg)) {
298
+ index++;
299
+ }
300
+ continue;
301
+ }
302
+ const shortEqualsMatch = /^-([A-Za-z0-9])=(.*)$/.exec(arg);
303
+ if (shortEqualsMatch) {
304
+ const shortName = shortEqualsMatch[1];
305
+ if (!this.knownShortOptions.has(shortName)) {
306
+ return shortName;
307
+ }
308
+ continue;
309
+ }
310
+ const shortMatch = /^-([A-Za-z0-9])$/.exec(arg);
311
+ if (shortMatch) {
312
+ const shortName = shortMatch[1];
313
+ if (!this.knownShortOptions.has(shortName)) {
314
+ return shortName;
315
+ }
316
+ const nextArg = argv[index + 1];
317
+ if (this.shortOptionsExpectingValue.has(shortName) && nextArg && !isOptionToken(nextArg)) {
318
+ index++;
319
+ }
320
+ continue;
321
+ }
322
+ return arg.replace(/^-+/, '');
323
+ }
324
+ return null;
199
325
  }
200
326
  async executeSubcommand(argv, parsedAlias, subcommand) {
201
327
  const currentScript = argv[1];
328
+ const subcommandName = subcommand.name;
202
329
  const extension = _nodePath.default.extname(currentScript);
203
330
  const baseScriptPath = extension ? currentScript.slice(0, -extension.length) : currentScript;
204
- const childScriptPath = extension ? `${baseScriptPath}-${subcommand}${extension}` : `${baseScriptPath}-${subcommand}`;
331
+ const childScriptPath = extension ? `${baseScriptPath}-${subcommandName}${extension}` : `${baseScriptPath}-${subcommandName}`;
205
332
  const aliasFromRawArgv = argv.slice(2).find(arg => (0, _envAlias.isAlias)(arg));
206
- const subcommandIndex = parsedAlias.argv.findIndex((arg, index) => {
207
- return index > 1 && arg === subcommand;
208
- });
209
- let childArgs = subcommandIndex > -1 ? parsedAlias.argv.slice(subcommandIndex + 1) : [];
333
+ const rawArgsBeforeSubcommand = parsedAlias.argv.slice(2, subcommand.index);
334
+ const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes('--');
335
+ const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter(arg => arg !== '--');
336
+ const subcommandArgs = parsedAlias.argv.slice(subcommand.index + 1);
337
+ const hasSeparator = argv.includes('--');
338
+ if (subcommandName === 'wp' && subcommandArgs.length && !hasSeparator) {
339
+ exit.withError('A double dash ("--") must separate the arguments of "vip" from those of "wp". Run "vip wp --help" for examples.');
340
+ }
341
+ let childArgs = [...argsBeforeSubcommand, ...(hasSeparatorBeforeSubcommand ? ['--'] : []), ...subcommandArgs];
210
342
  if (aliasFromRawArgv) {
211
343
  childArgs = [aliasFromRawArgv, ...childArgs];
212
344
  }
@@ -217,7 +349,7 @@ class CommanderArgsCompat {
217
349
  env: process.env
218
350
  });
219
351
  } else {
220
- const fallbackCommand = `${_nodePath.default.basename(baseScriptPath)}-${subcommand}`;
352
+ const fallbackCommand = `${_nodePath.default.basename(baseScriptPath)}-${subcommandName}`;
221
353
  if (process.env.VIP_CLI_SEA_MODE === '1' && (0, _internalBinLoader.hasInternalBin)(fallbackCommand)) {
222
354
  process.argv = [process.argv[0], process.argv[1], ...childArgs];
223
355
  const loaded = await (0, _internalBinLoader.loadInternalBin)(fallbackCommand);
@@ -256,10 +388,20 @@ CommanderArgsCompat.prototype.argv = async function (argv, cb) {
256
388
  const options = this.parse(parsedAlias.argv);
257
389
 
258
390
  // If there's a sub-command, run that instead
259
- if (this.isDefined(this.sub[0], 'commands')) {
260
- await this.executeSubcommand(argv, parsedAlias, this.sub[0]);
391
+ const dispatchSubcommand = this.findSubcommand(parsedAlias.argv);
392
+ if (dispatchSubcommand) {
393
+ await this.executeSubcommand(argv, parsedAlias, dispatchSubcommand);
261
394
  return {};
262
395
  }
396
+ if (!_opts.wildcardCommand) {
397
+ const unknownOption = this.findUnknownOption(parsedAlias.argv);
398
+ if (unknownOption) {
399
+ await (0, _tracker.trackEvent)('command_validation_error', {
400
+ error: `Unknown option: ${unknownOption}`
401
+ });
402
+ exit.withError(`The option "${unknownOption}" is unknown. Did you mean the following one?\n-h, --help Retrieve a description, examples, and available options for a (sub)command.`);
403
+ }
404
+ }
263
405
  if (_opts.format && !options.format) {
264
406
  options.format = 'table';
265
407
  }
@@ -267,11 +267,12 @@ function parseComponentForInfo(component) {
267
267
  async function showLogs(lando, slug, options = {}) {
268
268
  debug('Will display logs command on env', slug, 'with options', options);
269
269
  const instancePath = getEnvironmentPath(slug);
270
- debug('Instance path for', slug, 'is:', instancePath);
270
+ debug('Instance path for %s is %s', slug, instancePath);
271
271
  if (options.service) {
272
- const appInfo = await (0, _devEnvironmentLando.landoInfo)(lando, instancePath);
273
- if (!appInfo.services.includes(options.service)) {
274
- throw new _userError.default(`Service '${options.service}' not found. Please choose from one: ${appInfo.services.toString()}`);
272
+ const application = await (0, _devEnvironmentLando.getLandoApplication)(lando, instancePath);
273
+ const services = application.info.map(service => service.service);
274
+ if (!services.includes(options.service)) {
275
+ throw new _userError.default(`Service '${options.service}' not found. Please choose from: ${services.join(', ')}`);
275
276
  }
276
277
  }
277
278
  return (0, _devEnvironmentLando.landoLogs)(lando, instancePath, options);
@@ -3,6 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.bootstrapLando = bootstrapLando;
5
5
  exports.checkEnvHealth = checkEnvHealth;
6
+ exports.getLandoApplication = getLandoApplication;
6
7
  exports.getProxyContainer = getProxyContainer;
7
8
  exports.isContainerRunning = isContainerRunning;
8
9
  exports.isEnvUp = isEnvUp;