@automattic/vip 3.16.0 → 3.17.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.
@@ -79,37 +79,117 @@ const SQL_IMPORT_PREFLIGHT_PROGRESS_STEPS = [{
79
79
  name: 'Queueing import'
80
80
  }];
81
81
 
82
+ /**
83
+ * Check if a string is a valid URL
84
+ * @param {string} str - The string to check
85
+ * @returns {boolean} True if the string is a valid URL
86
+ */
87
+ function isValidUrl(str) {
88
+ try {
89
+ new URL(str);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Validate MD5 hash format
98
+ * @param {string} md5 - The MD5 hash to validate
99
+ * @returns {boolean} True if the hash is valid
100
+ */
101
+ function isValidMd5(md5) {
102
+ return /^[a-f0-9]{32}$/i.test(md5);
103
+ }
104
+
82
105
  /**
83
106
  * @param {AppForImport} app
84
107
  * @param {EnvForImport} env
85
- * @param {FileMeta} fileMeta
108
+ * @param {string} fileNameOrURL
109
+ * @param {boolean} isUrl
110
+ * @param {string|null} md5
111
+ * @param searchReplace
86
112
  */
87
- async function gates(app, env, fileMeta) {
113
+ // eslint-disable-next-line complexity
114
+ async function gates(app, env, fileNameOrURL, isUrl = false, md5 = null, searchReplace = []) {
88
115
  const {
89
116
  id: envId,
90
117
  appId
91
118
  } = env;
92
119
  const track = _tracker.trackEventWithEnv.bind(null, appId, envId);
93
- const {
94
- fileName,
95
- basename
96
- } = fileMeta;
97
- try {
98
- // Extract base file name and exit if it contains unsafe character
99
- (0, _sql.validateFilename)(basename);
100
- } catch (error) {
101
- await track('import_sql_command_error', {
102
- error_type: 'invalid-filename'
103
- });
104
- exit.withError(error);
120
+ if (isUrl) {
121
+ if (!md5) {
122
+ await track('import_sql_command_error', {
123
+ error_type: 'missing-md5'
124
+ });
125
+ exit.withError('MD5 hash is required when importing from a URL. Please provide the --md5 parameter with a valid MD5 hash of the remote file.');
126
+ }
127
+ if (searchReplace.length > 0) {
128
+ await track('import_sql_command_error', {
129
+ error_type: 'search-replace-with-url'
130
+ });
131
+ exit.withError('Search and replace operations are not supported when importing from a URL. Please remove the --search-replace option.');
132
+ }
105
133
  }
106
- try {
107
- (0, _sql.validateImportFileExtension)(fileName);
108
- } catch (error) {
134
+ if (md5 && !isValidMd5(md5)) {
109
135
  await track('import_sql_command_error', {
110
- error_type: 'invalid-extension'
136
+ error_type: 'invalid-md5'
111
137
  });
112
- exit.withError(error);
138
+ exit.withError('The provided MD5 hash is invalid. It should be a 32-character hexadecimal string.');
139
+ }
140
+ if (!isUrl && md5) {
141
+ console.log(_chalk.default.yellowBright('The --md5 parameter is only used for URL imports. It will be ignored for local file imports.'));
142
+ }
143
+ if (!isUrl) {
144
+ const fileName = fileNameOrURL;
145
+ const fileMeta = await (0, _clientFileUploader.getFileMeta)(fileName);
146
+ try {
147
+ // Extract base file name and exit if it contains unsafe character
148
+ (0, _sql.validateFilename)(fileMeta.basename);
149
+ } catch (error) {
150
+ await track('import_sql_command_error', {
151
+ error_type: 'invalid-filename'
152
+ });
153
+ exit.withError(error);
154
+ }
155
+ try {
156
+ (0, _sql.validateImportFileExtension)(fileName);
157
+ } catch (error) {
158
+ await track('import_sql_command_error', {
159
+ error_type: 'invalid-extension'
160
+ });
161
+ exit.withError(error);
162
+ }
163
+ try {
164
+ await (0, _clientFileUploader.checkFileAccess)(fileName);
165
+ } catch (err) {
166
+ await track('import_sql_command_error', {
167
+ error_type: 'sqlfile-unreadable'
168
+ });
169
+ exit.withError(`File '${fileName}' does not exist or is not readable.`);
170
+ }
171
+ if (!(await (0, _clientFileUploader.isFile)(fileName))) {
172
+ await track('import_sql_command_error', {
173
+ error_type: 'sqlfile-notfile'
174
+ });
175
+ exit.withError(`Path '${fileName}' is not a file.`);
176
+ }
177
+ const fileSize = await (0, _clientFileUploader.getFileSize)(fileName);
178
+ if (!fileSize) {
179
+ await track('import_sql_command_error', {
180
+ error_type: 'sqlfile-empty'
181
+ });
182
+ exit.withError(`File '${fileName}' is empty.`);
183
+ }
184
+ const maxFileSize = env?.launched ? _dbFileImport.SQL_IMPORT_FILE_SIZE_LIMIT_LAUNCHED : _dbFileImport.SQL_IMPORT_FILE_SIZE_LIMIT;
185
+ if (fileSize > maxFileSize) {
186
+ await track('import_sql_command_error', {
187
+ error_type: 'sqlfile-toobig',
188
+ file_size: fileSize,
189
+ launched: Boolean(env?.launched)
190
+ });
191
+ exit.withError(`The sql import file size (${fileSize} bytes) exceeds the limit (${maxFileSize} bytes).` + (env.launched ? ' Note: This limit is lower for launched environments to maintain site stability.' : '') + '\n\nPlease split it into multiple files or contact support for assistance.');
192
+ }
113
193
  }
114
194
  if (!(0, _dbFileImport.currentUserCanImportForApp)(app)) {
115
195
  await track('import_sql_command_error', {
@@ -123,36 +203,6 @@ async function gates(app, env, fileMeta) {
123
203
  });
124
204
  exit.withError('The type of application you specified does not currently support SQL imports.');
125
205
  }
126
- try {
127
- await (0, _clientFileUploader.checkFileAccess)(fileName);
128
- } catch (err) {
129
- await track('import_sql_command_error', {
130
- error_type: 'sqlfile-unreadable'
131
- });
132
- exit.withError(`File '${fileName}' does not exist or is not readable.`);
133
- }
134
- if (!(await (0, _clientFileUploader.isFile)(fileName))) {
135
- await track('import_sql_command_error', {
136
- error_type: 'sqlfile-notfile'
137
- });
138
- exit.withError(`Path '${fileName}' is not a file.`);
139
- }
140
- const fileSize = await (0, _clientFileUploader.getFileSize)(fileName);
141
- if (!fileSize) {
142
- await track('import_sql_command_error', {
143
- error_type: 'sqlfile-empty'
144
- });
145
- exit.withError(`File '${fileName}' is empty.`);
146
- }
147
- const maxFileSize = env?.launched ? _dbFileImport.SQL_IMPORT_FILE_SIZE_LIMIT_LAUNCHED : _dbFileImport.SQL_IMPORT_FILE_SIZE_LIMIT;
148
- if (fileSize > maxFileSize) {
149
- await track('import_sql_command_error', {
150
- error_type: 'sqlfile-toobig',
151
- file_size: fileSize,
152
- launched: Boolean(env?.launched)
153
- });
154
- exit.withError(`The sql import file size (${fileSize} bytes) exceeds the limit (${maxFileSize} bytes).` + (env.launched ? ' Note: This limit is lower for launched environments to maintain site stability.' : '') + '\n\nPlease split it into multiple files or contact support for assistance.');
155
- }
156
206
  if (!env?.importStatus) {
157
207
  await track('import_sql_command_error', {
158
208
  error_type: 'empty-import-status'
@@ -186,10 +236,10 @@ const examples = [
186
236
  usage: 'vip @example-app.develop import sql file.sql',
187
237
  description: 'Import the local SQL database backup file "file.sql" to the develop environment of the "example-app" application.'
188
238
  },
189
- // `search-replace` flag
239
+ // URL import
190
240
  {
191
- usage: 'vip @example-app.develop import sql file.sql --search-replace="https://from.example.com,https://to.example.com"',
192
- description: 'Perform a search and replace operation on the SQL database file during the import process.'
241
+ usage: 'vip @example-app.develop import sql https://example.org/file.sql --md5 b5b39269e9105d6e1e9cd50ff54e6282',
242
+ description: 'Import a remote SQL database backup file from the URL with MD5 hash verification to the develop environment of the "example-app" application.'
193
243
  },
194
244
  // `search-replace` flag
195
245
  {
@@ -339,57 +389,68 @@ void (0, _command.default)({
339
389
  requiredArgs: 1,
340
390
  module: 'import-sql',
341
391
  usage
342
- }).command('status', 'Check the status of a SQL database import currently in progress.').option('skip-validate', 'Do not perform file validation prior to import. If the file contains unsupported entries, the import is likely to fail.').option('search-replace', 'Search for a string in the SQL file and replace it with a new string. Separate the values by a comma only; no spaces (e.g. --search-replace=“from,to).').option('in-place', 'Overwrite the local input file with the results of the search and replace operation prior to import.').option('output', 'The local file path to save a copy of the results from the search and replace operation when the --search-replace option is passed. Ignored when used with the --in-place option.').option('skip-maintenance-mode', 'Imports data without putting the environment in maintenance mode. Available only for unlaunched environments. Caution: This may cause site instability during import.').examples(examples).argv(process.argv, async (arg, opts) => {
392
+ }).command('status', 'Check the status of a SQL database import currently in progress.').option('skip-validate', 'Do not perform file validation prior to import. If the file contains unsupported entries, the import is likely to fail.').option('search-replace', 'Search for a string in the SQL file and replace it with a new string. Separate the values by a comma only; no spaces (e.g. --search-replace="from,to").').option('in-place', 'Overwrite the local input file with the results of the search and replace operation prior to import.').option('output', 'The local file path to save a copy of the results from the search and replace operation when the --search-replace option is passed. Ignored when used with the --in-place option.').option('skip-maintenance-mode', 'Imports data without putting the environment in maintenance mode. Available only for unlaunched environments. Caution: This may cause site instability during import.').option('md5', 'MD5 hash of the remote SQL file for verification. Required when importing from a URL.').examples(examples)
393
+ // eslint-disable-next-line complexity
394
+ .argv(process.argv, async (arg, opts) => {
343
395
  const {
344
396
  app,
345
397
  env
346
398
  } = opts;
347
- let {
399
+ const {
348
400
  skipValidate,
349
401
  searchReplace,
350
- skipMaintenanceMode
402
+ skipMaintenanceMode,
403
+ md5
351
404
  } = opts;
352
405
  const {
353
406
  id: envId,
354
407
  appId
355
408
  } = env;
356
- const [fileName] = arg;
409
+ const [fileNameOrURL] = arg;
357
410
  const isMultiSite = await (0, _isMultiSite.isMultiSiteInSiteMeta)(appId, envId);
358
- let fileMeta = await (0, _clientFileUploader.getFileMeta)(fileName);
359
- if (fileMeta.isCompressed) {
360
- console.log(_chalk.default.yellowBright('You are importing a compressed file. Validation and search-replace operations will be skipped.'));
361
- skipValidate = true;
362
- searchReplace = undefined;
411
+ const isUrl = isValidUrl(fileNameOrURL);
412
+ if (isUrl && opts.inPlace) {
413
+ console.log(_chalk.default.yellowBright('The --in-place option is not supported when importing from a URL. This option will be ignored.'));
414
+ opts.inPlace = false;
415
+ }
416
+ if (isUrl && opts.output) {
417
+ console.log(_chalk.default.yellowBright('The --output option is not supported when importing from a URL. This option will be ignored.'));
418
+ opts.output = undefined;
363
419
  }
364
420
  debug('Options: ', opts);
365
421
  debug('Args: ', arg);
366
422
  const track = _tracker.trackEventWithEnv.bind(null, appId, envId);
367
- await track('import_sql_command_execute');
423
+ await track('import_sql_command_execute', {
424
+ is_url: isUrl
425
+ });
368
426
 
369
- // // halt operation of the import based on some rules
370
- await gates(app, env, fileMeta);
427
+ // halt operation of the import based on some rules
428
+ await gates(app, env, fileNameOrURL, isUrl, md5, searchReplace);
371
429
 
372
430
  // Log summary of import details
373
431
  const domain = env?.primaryDomain?.name ? env.primaryDomain.name : `#${env.id}`;
374
432
  const formattedEnvironment = (0, _format.formatEnvironment)(opts.env.type);
375
433
  const launched = opts.env.launched;
376
- let fileNameToUpload = fileName;
434
+ const fileNameToUpload = fileNameOrURL;
377
435
 
378
- // SQL file validations
379
- const tableNames = await validateAndGetTableNames({
380
- skipValidate,
381
- appId,
382
- envId,
383
- fileNameToUpload,
384
- searchReplace
385
- });
436
+ // SQL file validations - only for local files
437
+ let tableNames = [];
438
+ if (!isUrl) {
439
+ tableNames = await validateAndGetTableNames({
440
+ skipValidate,
441
+ appId,
442
+ envId,
443
+ fileNameToUpload,
444
+ searchReplace
445
+ });
446
+ }
386
447
 
387
448
  // display playbook of what will happen during execution
388
449
  displayPlaybook({
389
450
  launched,
390
451
  tableNames,
391
452
  searchReplace,
392
- fileName,
453
+ fileName: fileNameOrURL,
393
454
  domain,
394
455
  formattedEnvironment,
395
456
  unformattedEnvironment: opts.env.type,
@@ -406,7 +467,7 @@ void (0, _command.default)({
406
467
  isMultiSite,
407
468
  tableNames
408
469
  });
409
- if (opts.inPlace) {
470
+ if (!isUrl && opts.inPlace) {
410
471
  const approved = await (0, _prompt.confirm)([], 'Are you sure you want to run search and replace on your input file? This operation is not reversible.');
411
472
  if (!approved) {
412
473
  await (0, _tracker.trackEventWithEnv)('search_replace_in_place_cancelled', {
@@ -433,7 +494,7 @@ void (0, _command.default)({
433
494
  =============================================================
434
495
  Processing the SQL import for your environment...
435
496
  `;
436
- progressTracker.suffix = `\n${(0, _format.getGlyphForStatus)(status, progressTracker.runningSprite)} ${status === 'running' ? 'Loading remaining steps' : ''}`; // TODO: maybe use progress tracker status
497
+ progressTracker.suffix = `\n${(0, _format.getGlyphForStatus)(status, progressTracker.runningSprite)} ${status === 'running' ? 'Loading remaining steps' : ''}`;
437
498
  };
438
499
  const failWithError = failureError => {
439
500
  status = 'failed';
@@ -446,89 +507,101 @@ Processing the SQL import for your environment...
446
507
  };
447
508
  progressTracker.startPrinting(setProgressTrackerPrefixAndSuffix);
448
509
 
449
- // Run Search and Replace if the --search-replace flag was provided
450
- if (searchReplace && searchReplace.length) {
510
+ // Run Search and Replace if the --search-replace flag was provided (local files only)
511
+ if (!isUrl && searchReplace && searchReplace.length) {
451
512
  progressTracker.stepRunning('replace');
452
513
  const {
453
514
  outputFileName
454
- } = await (0, _searchAndReplace.searchAndReplace)(fileName, searchReplace, {
515
+ } = await (0, _searchAndReplace.searchAndReplace)(fileNameOrURL, searchReplace, {
455
516
  isImport: true,
456
517
  inPlace: opts.inPlace,
457
518
  output: opts.output ?? true,
458
- // "true" creates a temp output file instead of printing to stdout, as we need to upload the output to S3.
459
519
  batchMode: true
460
520
  });
461
521
  if (typeof outputFileName !== 'string') {
462
522
  progressTracker.stepFailed('replace');
463
523
  return failWithError('Unable to determine location of the intermediate search and replace file.');
464
524
  }
465
- fileNameToUpload = outputFileName;
466
- fileMeta = await (0, _clientFileUploader.getFileMeta)(fileNameToUpload);
467
525
  progressTracker.stepSuccess('replace');
468
526
  } else {
469
527
  progressTracker.stepSkipped('replace');
470
528
  }
471
- progressTracker.stepRunning('upload');
472
529
 
473
530
  // Call the Public API
474
531
  const api = (0, _api.default)();
475
- const startImportVariables = {};
476
- const progressCallback = percentage => {
477
- progressTracker.setUploadPercentage(percentage);
478
- };
479
- fileMeta.fileName = fileNameToUpload;
480
- try {
481
- const {
482
- fileMeta: {
483
- basename
484
- },
485
- checksum: md5,
486
- result
487
- } = await (0, _clientFileUploader.uploadImportSqlFileToS3)({
488
- app,
489
- env,
490
- fileMeta,
491
- progressCallback
492
- });
493
- startImportVariables.input = {
532
+ const startImportVariables = {
533
+ input: {
494
534
  id: app.id,
495
535
  environmentId: env.id,
496
- basename,
497
- md5,
498
- searchReplace: [],
499
536
  skipMaintenanceMode
537
+ }
538
+ };
539
+ if (isUrl) {
540
+ progressTracker.stepSkipped('upload');
541
+ startImportVariables.input = {
542
+ ...startImportVariables.input,
543
+ url: fileNameOrURL,
544
+ searchReplace: [],
545
+ md5
500
546
  };
501
- if (searchReplace) {
502
- let pairs = searchReplace;
503
- if (!Array.isArray(pairs)) {
504
- pairs = [searchReplace];
505
- }
506
-
507
- // determine all the replacements required
508
- const replacementsArr = pairs.map(pair => pair.split(',').map(str => str.trim()));
509
- startImportVariables.input.searchReplace = replacementsArr.map(arr => {
510
- return {
511
- from: arr[0],
512
- to: arr[1]
513
- };
547
+ } else {
548
+ progressTracker.stepRunning('upload');
549
+ const fileMeta = await (0, _clientFileUploader.getFileMeta)(fileNameOrURL);
550
+ const progressCallback = percentage => {
551
+ progressTracker.setUploadPercentage(percentage);
552
+ };
553
+ fileMeta.fileName = fileNameToUpload;
554
+ try {
555
+ const {
556
+ fileMeta: {
557
+ basename
558
+ },
559
+ checksum: uploadedMD5,
560
+ result
561
+ } = await (0, _clientFileUploader.uploadImportSqlFileToS3)({
562
+ app,
563
+ env,
564
+ fileMeta,
565
+ progressCallback
514
566
  });
567
+ startImportVariables.input = {
568
+ ...startImportVariables.input,
569
+ basename,
570
+ md5: uploadedMD5,
571
+ searchReplace: []
572
+ };
573
+ debug({
574
+ basename,
575
+ uploadedMD5,
576
+ result,
577
+ startImportVariables
578
+ });
579
+ debug('Upload complete. Initiating the import.');
580
+ progressTracker.stepSuccess('upload');
581
+ await track('import_sql_upload_complete');
582
+ } catch (uploadError) {
583
+ await track('import_sql_command_error', {
584
+ error_type: 'upload_failed',
585
+ upload_error: uploadError.message
586
+ });
587
+ progressTracker.stepFailed('upload');
588
+ return failWithError(uploadError);
515
589
  }
516
- debug({
517
- basename,
518
- md5,
519
- result,
520
- startImportVariables
521
- });
522
- debug('Upload complete. Initiating the import.');
523
- progressTracker.stepSuccess('upload');
524
- await track('import_sql_upload_complete');
525
- } catch (uploadError) {
526
- await track('import_sql_command_error', {
527
- error_type: 'upload_failed',
528
- upload_error: uploadError.message
590
+ }
591
+ if (searchReplace) {
592
+ let pairs = searchReplace;
593
+ if (!Array.isArray(pairs)) {
594
+ pairs = [searchReplace];
595
+ }
596
+
597
+ // determine all the replacements required
598
+ const replacementsArr = pairs.map(pair => pair.split(',').map(str => str.trim()));
599
+ startImportVariables.input.searchReplace = replacementsArr.map(arr => {
600
+ return {
601
+ from: arr[0],
602
+ to: arr[1]
603
+ };
529
604
  });
530
- progressTracker.stepFailed('upload');
531
- return failWithError(uploadError);
532
605
  }
533
606
 
534
607
  // Start the import
@@ -541,7 +614,6 @@ Processing the SQL import for your environment...
541
614
  startImportResults
542
615
  });
543
616
  } catch (gqlErr) {
544
- progressTracker.stepFailed('queue_import');
545
617
  await track('import_sql_command_error', {
546
618
  error_type: 'StartImport-failed',
547
619
  gql_err: gqlErr
@@ -26,6 +26,7 @@ exports.promptForPhpVersion = promptForPhpVersion;
26
26
  exports.promptForText = promptForText;
27
27
  exports.promptForURL = promptForURL;
28
28
  exports.promptForWordPress = promptForWordPress;
29
+ exports.resolveMultisite = resolveMultisite;
29
30
  exports.resolvePath = resolvePath;
30
31
  exports.resolvePhpVersion = resolvePhpVersion;
31
32
  exports.setIsTTY = setIsTTY;
@@ -74,7 +74,7 @@ function sanitizeConfiguration(configuration, configurationFilePath) {
74
74
  slug: configuration.slug.toString(),
75
75
  // NOSONAR
76
76
  title: configuration.title?.toString(),
77
- multisite: stringToBooleanIfDefined(configuration.multisite),
77
+ multisite: (0, _devEnvironmentCli.resolveMultisite)((0, _devEnvironmentCli.processStringOrBooleanOption)(configuration.multisite)),
78
78
  php: configuration.php?.toString(),
79
79
  wordpress: configuration.wordpress?.toString(),
80
80
  'mu-plugins': configuration['mu-plugins']?.toString(),
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  ## Changelog
2
2
 
3
+ ### 3.16.1
4
+
5
+ * New develop release: 3.16.1-dev.0 by @github-actions in https://github.com/Automattic/vip-cli/pull/2382
6
+ * build(deps-dev): bump @types/node from 22.15.24 to 22.15.27 by @dependabot in https://github.com/Automattic/vip-cli/pull/2379
7
+ * fix(dev-env): Support multisite subdirectory/subdomain values in vip-dev-env.yml files by @saroshaga in https://github.com/Automattic/vip-cli/pull/2381
8
+ * chore(deps): update lando by @saroshaga in https://github.com/Automattic/vip-cli/pull/2388
9
+ * New package release: v3.16.1 by @github-actions in https://github.com/Automattic/vip-cli/pull/2389
10
+
11
+
12
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.16.0...3.16.1
13
+
14
+ ### 3.16.0
15
+
16
+ * New develop release: 3.15.1-dev.0 by @github-actions in https://github.com/Automattic/vip-cli/pull/2369
17
+ * build(deps-dev): bump @types/dockerode from 3.3.38 to 3.3.39 by @dependabot in https://github.com/Automattic/vip-cli/pull/2372
18
+ * build(deps-dev): bump @types/node from 22.15.18 to 22.15.21 by @dependabot in https://github.com/Automattic/vip-cli/pull/2373
19
+ * feat(dev-env) Update ES to elasticsearch:8.18.1 by @rinatkhaziev in https://github.com/Automattic/vip-cli/pull/2377
20
+ * build(deps-dev): bump @types/node from 22.15.21 to 22.15.24 by @dependabot in https://github.com/Automattic/vip-cli/pull/2378
21
+ * build(deps-dev): bump @babel/core from 7.27.1 to 7.27.3 in the babel group by @dependabot in https://github.com/Automattic/vip-cli/pull/2374
22
+ * New package release: v3.16.0 by @github-actions in https://github.com/Automattic/vip-cli/pull/2380
23
+
24
+
25
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.15.0...3.16.0
26
+
3
27
  ### 3.15.0
4
28
 
5
29
  * chore(deps): update `lando`
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@automattic/vip",
9
- "version": "3.16.0",
9
+ "version": "3.17.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -31,7 +31,7 @@
31
31
  "ini": "5.0.0",
32
32
  "js-yaml": "^4.1.0",
33
33
  "jwt-decode": "4.0.0",
34
- "lando": "github:automattic/lando-cli#1d3e8c0",
34
+ "lando": "github:automattic/lando-cli#1d3e8c01f5f7e7f5b51b29aba145f3bddd2ca32e",
35
35
  "node-fetch": "^2.6.1",
36
36
  "node-stream-zip": "1.15.0",
37
37
  "open": "^10.0.0",
@@ -3807,9 +3807,9 @@
3807
3807
  "dev": true
3808
3808
  },
3809
3809
  "node_modules/@types/node": {
3810
- "version": "22.15.24",
3811
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz",
3812
- "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
3810
+ "version": "22.15.27",
3811
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.27.tgz",
3812
+ "integrity": "sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==",
3813
3813
  "license": "MIT",
3814
3814
  "dependencies": {
3815
3815
  "undici-types": "~6.21.0"
@@ -16140,9 +16140,9 @@
16140
16140
  "dev": true
16141
16141
  },
16142
16142
  "@types/node": {
16143
- "version": "22.15.24",
16144
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz",
16145
- "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
16143
+ "version": "22.15.27",
16144
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.27.tgz",
16145
+ "integrity": "sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==",
16146
16146
  "requires": {
16147
16147
  "undici-types": "~6.21.0"
16148
16148
  }
@@ -20356,7 +20356,7 @@
20356
20356
  },
20357
20357
  "lando": {
20358
20358
  "version": "git+ssh://git@github.com/automattic/lando-cli.git#1d3e8c01f5f7e7f5b51b29aba145f3bddd2ca32e",
20359
- "from": "lando@github:automattic/lando-cli#1d3e8c0",
20359
+ "from": "lando@github:automattic/lando-cli#1d3e8c01f5f7e7f5b51b29aba145f3bddd2ca32e",
20360
20360
  "requires": {
20361
20361
  "axios": "^1.8.2",
20362
20362
  "bluebird": "^3.4.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "The VIP Javascript library & CLI",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -161,7 +161,7 @@
161
161
  "ini": "5.0.0",
162
162
  "js-yaml": "^4.1.0",
163
163
  "jwt-decode": "4.0.0",
164
- "lando": "github:automattic/lando-cli#1d3e8c0",
164
+ "lando": "github:automattic/lando-cli#1d3e8c01f5f7e7f5b51b29aba145f3bddd2ca32e",
165
165
  "node-fetch": "^2.6.1",
166
166
  "node-stream-zip": "1.15.0",
167
167
  "open": "^10.0.0",