@automattic/vip 4.0.1 → 4.0.3

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,7 +104,8 @@ 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
  }
110
111
  function isOptionToken(arg) {
@@ -118,21 +119,43 @@ class CommanderArgsCompat {
118
119
  this.sub = [];
119
120
  this.examplesList = [];
120
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();
121
126
  this._opts = opts;
122
127
  this.program = new _commander.Command();
123
128
  this.program.allowUnknownOption(true);
124
129
  this.program.allowExcessArguments(true);
125
130
  this.program.helpOption(false);
126
131
  normalizeUsage(this.program, this._opts.usage);
132
+ this.optionDefaults = new Map();
127
133
  }
128
134
  option(name, description, defaultValue, parseFn) {
129
135
  const definition = createOptionDefinition(name, description, defaultValue, parseFn, this.usedShortNames);
130
136
  const {
131
137
  flags,
132
- parser
138
+ parser,
139
+ normalizedShortName
133
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.
134
156
  if (parser && defaultValue !== undefined) {
135
- this.program.option(flags, description, parser, defaultValue);
157
+ this.optionDefaults.set(normalizedLongName, defaultValue);
158
+ this.program.option(flags, description, parser);
136
159
  } else if (parser) {
137
160
  this.program.option(flags, description, parser);
138
161
  } else if (defaultValue !== undefined) {
@@ -194,16 +217,43 @@ class CommanderArgsCompat {
194
217
  return this.details.commands.some(entry => entry.usage === value);
195
218
  }
196
219
  parse(argv) {
197
- this.program.parse(argv, {
220
+ this.program.parse(this.normalizeShortOptionEqualsSyntax(argv), {
198
221
  from: 'node'
199
222
  });
200
223
  this.sub = this.program.args.slice();
201
- 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;
202
251
  }
203
252
  findSubcommand(argv) {
204
- const dashDashIndex = argv.indexOf('--', 2);
253
+ const searchStart = argv[2] === '--' ? 3 : 2;
254
+ const dashDashIndex = argv.indexOf('--', searchStart);
205
255
  const searchEnd = dashDashIndex === -1 ? argv.length : dashDashIndex;
206
- for (let index = 2; index < searchEnd; index++) {
256
+ for (let index = searchStart; index < searchEnd; index++) {
207
257
  const arg = argv[index];
208
258
  if (this.isDefined(arg, 'commands')) {
209
259
  return {
@@ -221,6 +271,56 @@ class CommanderArgsCompat {
221
271
  index++;
222
272
  }
223
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
+ }
224
324
  return null;
225
325
  }
226
326
  async executeSubcommand(argv, parsedAlias, subcommand) {
@@ -230,7 +330,15 @@ class CommanderArgsCompat {
230
330
  const baseScriptPath = extension ? currentScript.slice(0, -extension.length) : currentScript;
231
331
  const childScriptPath = extension ? `${baseScriptPath}-${subcommandName}${extension}` : `${baseScriptPath}-${subcommandName}`;
232
332
  const aliasFromRawArgv = argv.slice(2).find(arg => (0, _envAlias.isAlias)(arg));
233
- let childArgs = [...parsedAlias.argv.slice(2, subcommand.index), ...parsedAlias.argv.slice(subcommand.index + 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];
234
342
  if (aliasFromRawArgv) {
235
343
  childArgs = [aliasFromRawArgv, ...childArgs];
236
344
  }
@@ -285,6 +393,15 @@ CommanderArgsCompat.prototype.argv = async function (argv, cb) {
285
393
  await this.executeSubcommand(argv, parsedAlias, dispatchSubcommand);
286
394
  return {};
287
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
+ }
288
405
  if (_opts.format && !options.format) {
289
406
  options.format = 'table';
290
407
  }