@automattic/vip 4.0.3 → 4.0.5

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.
Files changed (35) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/bin/vip-app-deploy-token-generate.js +102 -0
  3. package/dist/bin/vip-app-deploy-token.js +20 -0
  4. package/dist/bin/vip-app-deploy.js +7 -1
  5. package/dist/bin/vip-app.js +1 -1
  6. package/dist/bin/vip-dev-env-update.js +1 -5
  7. package/dist/commands/dev-env-import-sql.js +4 -1
  8. package/dist/commands/dev-env-sync-sql.js +3 -4
  9. package/dist/lib/analytics/clients/tracks.js +2 -2
  10. package/dist/lib/api/http.js +19 -14
  11. package/dist/lib/api.js +1 -2
  12. package/dist/lib/cli/command.js +37 -1
  13. package/dist/lib/cli/internal-bin-loader.js +2 -0
  14. package/dist/lib/client-file-uploader.js +33 -14
  15. package/dist/lib/custom-deploy/custom-deploy.js +37 -0
  16. package/dist/lib/dev-environment/dev-environment-core.js +5 -5
  17. package/dist/lib/dev-environment/dev-environment-lando.js +7 -3
  18. package/dist/lib/http/download-file.js +6 -1
  19. package/dist/lib/http/proxy-dispatcher.js +97 -0
  20. package/dist/lib/keychain/secure.js +4 -4
  21. package/dist/lib/keychain.js +1 -1
  22. package/dist/lib/media-import/status.js +6 -1
  23. package/dist/lib/retry.js +3 -2
  24. package/dist/lib/search-and-replace.js +3 -4
  25. package/dist/lib/validations/custom-deploy.js +60 -4
  26. package/dist/lib/validations/line-by-line.js +4 -9
  27. package/dist/lib/validations/sql.js +2 -2
  28. package/docs/COMMANDER-MIGRATION.md +1 -0
  29. package/docs/RELEASING.md +2 -0
  30. package/docs/TESTING.md +16 -1
  31. package/helpers/build-sea.js +2 -2
  32. package/helpers/release-smoke-test.js +869 -0
  33. package/npm-shrinkwrap.json +825 -1041
  34. package/package.json +22 -16
  35. package/test-utils/undici-mock.js +25 -0
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.createProxyDispatcher = createProxyDispatcher;
5
+ var _debug = _interopRequireDefault(require("debug"));
6
+ var _undici = require("undici");
7
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
+ const debug = (0, _debug.default)('vip:proxy-dispatcher');
9
+ const proxyDispatchers = new Map();
10
+ function normalizeNoProxyToken(token) {
11
+ return token.trim().toLowerCase();
12
+ }
13
+ function isNoProxyMatch(hostname, port, token) {
14
+ // eslint-disable-next-line security/detect-possible-timing-attacks
15
+ if (token === '*') {
16
+ return true;
17
+ }
18
+ const [tokenHostRaw, tokenPort] = token.split(':');
19
+ const tokenHost = tokenHostRaw.replace(/^\./u, '');
20
+ if (tokenPort && tokenPort !== port) {
21
+ return false;
22
+ }
23
+ if (hostname === tokenHost) {
24
+ return true;
25
+ }
26
+ return hostname.endsWith(`.${tokenHost}`);
27
+ }
28
+ function isCoveredByNoProxy(url) {
29
+ const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || null; // NOSONAR
30
+
31
+ if (!NO_PROXY) {
32
+ return false;
33
+ }
34
+ const parsedUrl = new URL(url);
35
+ const hostname = parsedUrl.hostname.toLowerCase();
36
+ const port = parsedUrl.port || (parsedUrl.protocol === 'http:' ? '80' : '443');
37
+ const noProxyTokens = NO_PROXY.split(',').map(normalizeNoProxyToken).filter(Boolean);
38
+ return noProxyTokens.some(token => isNoProxyMatch(hostname, port, token));
39
+ }
40
+ function resolveProxyUrl(url) {
41
+ const VIP_PROXY = process.env.VIP_PROXY || process.env.vip_proxy || null; // NOSONAR
42
+ const SOCKS_PROXY = process.env.SOCKS_PROXY || process.env.socks_proxy || null; // NOSONAR
43
+ const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.https_proxy || null; // NOSONAR
44
+
45
+ if (VIP_PROXY) {
46
+ return VIP_PROXY;
47
+ }
48
+ if (process.env.VIP_USE_SYSTEM_PROXY && !isCoveredByNoProxy(url)) {
49
+ if (SOCKS_PROXY) {
50
+ return SOCKS_PROXY;
51
+ }
52
+ if (HTTPS_PROXY) {
53
+ return HTTPS_PROXY;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ function redactProxyUrl(proxyUrl) {
59
+ try {
60
+ const parsed = new URL(proxyUrl);
61
+ return `${parsed.protocol}//${parsed.host}`;
62
+ } catch {
63
+ return 'invalid-proxy-url';
64
+ }
65
+ }
66
+ let listeners = null;
67
+
68
+ // Suppress ExperimentalWarning: SOCKS5 proxy support is experimental and subject to change
69
+ function suppressExperimentalSocksWarning(warning) {
70
+ listeners?.forEach(listener => process.on('warning', listener));
71
+ if (warning.name !== 'ExperimentalWarning' || !/SOCKS5 proxy/u.test(warning.message)) {
72
+ listeners?.forEach(listener => listener(warning));
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Build an undici dispatcher from the existing proxy resolution logic.
78
+ *
79
+ * This keeps env precedence in one place while allowing fetch callers to
80
+ * provide `dispatcher` instead of `agent`.
81
+ */
82
+ function createProxyDispatcher(url) {
83
+ const proxyUrl = resolveProxyUrl(url);
84
+ if (!proxyUrl) {
85
+ return null;
86
+ }
87
+ if (!proxyDispatchers.has(proxyUrl)) {
88
+ debug(`Enabling fetch dispatcher proxy support using config: ${redactProxyUrl(proxyUrl)}`);
89
+ proxyDispatchers.set(proxyUrl, new _undici.ProxyAgent(proxyUrl));
90
+ }
91
+ if (listeners === null) {
92
+ listeners = process.rawListeners('warning');
93
+ process.removeAllListeners('warning');
94
+ process.once('warning', suppressExperimentalSocksWarning);
95
+ }
96
+ return proxyDispatchers.get(proxyUrl) ?? null;
97
+ }
@@ -2,18 +2,18 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
- var _nodeKeytar = _interopRequireDefault(require("@postman/node-keytar"));
5
+ var _keytar = _interopRequireDefault(require("@github/keytar"));
6
6
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
7
  class Secure {
8
8
  getPassword(service) {
9
- return _nodeKeytar.default.getPassword(service, service);
9
+ return _keytar.default.getPassword(service, service);
10
10
  }
11
11
  async setPassword(service, password) {
12
- await _nodeKeytar.default.setPassword(service, service, password);
12
+ await _keytar.default.setPassword(service, service, password);
13
13
  return true;
14
14
  }
15
15
  deletePassword(service) {
16
- return _nodeKeytar.default.deletePassword(service, service);
16
+ return _keytar.default.deletePassword(service, service);
17
17
  }
18
18
  }
19
19
  exports.default = Secure;
@@ -10,7 +10,7 @@ const debug = (0, _debug.default)('@automattic/vip:keychain');
10
10
  try {
11
11
  // Try using Secure keychain ("keytar") first
12
12
  // eslint-disable-next-line @typescript-eslint/no-require-imports
13
- const Secure = require('./keychain/secure');
13
+ const Secure = require('./keychain/secure').default;
14
14
  keychain = new Secure();
15
15
  } catch (error) {
16
16
  debug('Cannot use Secure keychain; falling back to Insecure keychain (Details: %o)', error);
@@ -9,9 +9,11 @@ var _enquirer = require("enquirer");
9
9
  var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
10
10
  var _promises = require("node:fs/promises");
11
11
  var _nodePath = require("node:path");
12
+ var _undici = require("undici");
12
13
  var _api = _interopRequireDefault(require("../../lib/api"));
13
14
  var _format = require("../../lib/cli/format");
14
15
  var _mediaFileImport = require("../../lib/media-import/media-file-import");
16
+ var _proxyDispatcher = require("../http/proxy-dispatcher");
15
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
18
  const IMPORT_MEDIA_PROGRESS_POLL_INTERVAL = 1000;
17
19
  const ONE_MINUTE_IN_MILLISECONDS = 1000 * 60;
@@ -237,7 +239,10 @@ Downloading errors details from ${fileErrorsUrl}
237
239
  clearAfter: true
238
240
  });
239
241
  try {
240
- const response = await fetch(fileErrorsUrl);
242
+ const proxyDispatcher = (0, _proxyDispatcher.createProxyDispatcher)(fileErrorsUrl);
243
+ const response = await (0, _undici.fetch)(fileErrorsUrl, {
244
+ dispatcher: proxyDispatcher ?? undefined
245
+ });
241
246
  return await response.json();
242
247
  } catch (err) {
243
248
  progressTracker.suffix += `${_chalk.default.red(`Could not download import errors report\n${err.message}`)}`;
package/dist/lib/retry.js CHANGED
@@ -4,6 +4,7 @@ exports.__esModule = true;
4
4
  exports.EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_200_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_100_MS = void 0;
5
5
  exports.exponentialBackoff = exponentialBackoff;
6
6
  exports.retry = retry;
7
+ var _promises = require("node:timers/promises");
7
8
  // copied over from our internal lib
8
9
 
9
10
  const EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exponentialBackoff(50);
@@ -108,7 +109,7 @@ function isValidInterval(interval) {
108
109
  }
109
110
  return typeof interval === 'number' && Number.isInteger(interval) && interval >= 0 && interval <= MAX_INTERVAL_BETWEEN_ATTEMPTS;
110
111
  }
111
- async function awaitInterval(interval, attemptNumber) {
112
+ function awaitInterval(interval, attemptNumber) {
112
113
  let newInterval;
113
114
  if (typeof interval === 'function') {
114
115
  newInterval = interval(attemptNumber);
@@ -118,7 +119,7 @@ async function awaitInterval(interval, attemptNumber) {
118
119
  } else {
119
120
  newInterval = interval;
120
121
  }
121
- return new Promise(resolve => setTimeout(resolve, newInterval));
122
+ return (0, _promises.setTimeout)(newInterval);
122
123
  }
123
124
  function validateTask(task) {
124
125
  if (typeof task !== 'function') {
@@ -130,13 +130,12 @@ const searchAndReplace = async (fileName, pairs, {
130
130
  // replace() throws strings... OMG
131
131
  exit.withError(replaceError);
132
132
  }
133
- const streams = [replacedStream];
133
+ const transforms = [];
134
134
  if (isMyDumper) {
135
- streams.push((0, _database.fixMyDumperTransform)());
135
+ transforms.push((0, _database.fixMyDumperTransform)());
136
136
  }
137
- streams.push(writeStream);
138
137
  try {
139
- await (0, _promises.pipeline)(streams);
138
+ await (0, _promises.pipeline)(replacedStream, ...transforms, writeStream);
140
139
  } catch (error) {
141
140
  console.log(_chalk.default.red("Oh no! We couldn't write to the output file. Please check your available disk space and file/folder permissions."));
142
141
  throw error;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
+ exports.LARGE_ARCHIVE_FILE_SIZE_LIMIT = void 0;
5
+ exports.findLargeArchiveFilesInDeployArchive = findLargeArchiveFilesInDeployArchive;
4
6
  exports.validateDeployFileExt = validateDeployFileExt;
5
7
  exports.validateFilename = validateFilename;
6
8
  exports.validateName = validateName;
@@ -11,6 +13,7 @@ var _nodeFs = require("node:fs");
11
13
  var _path = _interopRequireDefault(require("path"));
12
14
  var tar = _interopRequireWildcard(require("tar"));
13
15
  var exit = _interopRequireWildcard(require("../../lib/cli/exit"));
16
+ var _fileSize = require("../../lib/constants/file-size");
14
17
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
19
  const errorMessages = {
@@ -22,6 +25,62 @@ const errorMessages = {
22
25
  };
23
26
  const symlinkIgnorePattern = /\/node_modules\/[^/]+\/\.bin\//;
24
27
  const macosxDir = '__MACOSX';
28
+ const LARGE_ARCHIVE_FILE_SIZE_LIMIT = exports.LARGE_ARCHIVE_FILE_SIZE_LIMIT = 20 * _fileSize.MB_IN_BYTES;
29
+ function getDeployFileExt(filename) {
30
+ const lower = filename.toLowerCase();
31
+ if (lower.endsWith('.tar.gz')) {
32
+ return '.tar.gz';
33
+ }
34
+ if (lower.endsWith('.tgz')) {
35
+ return '.tgz';
36
+ }
37
+ if (lower.endsWith('.zip')) {
38
+ return '.zip';
39
+ }
40
+ return _path.default.extname(lower);
41
+ }
42
+ function isDeployArchiveFile(filename) {
43
+ return ['.zip', '.tar.gz', '.tgz'].includes(getDeployFileExt(filename));
44
+ }
45
+ async function findLargeArchiveFilesInZipArchive(filePath) {
46
+ const zipFile = new _nodeStreamZip.default.async({
47
+ file: filePath
48
+ });
49
+ try {
50
+ const zipEntries = await zipFile.entries();
51
+ return Object.values(zipEntries).filter(entry => !entry.isDirectory && isDeployArchiveFile(entry.name) && entry.size > LARGE_ARCHIVE_FILE_SIZE_LIMIT).map(entry => ({
52
+ path: entry.name,
53
+ size: entry.size
54
+ }));
55
+ } finally {
56
+ await zipFile.close();
57
+ }
58
+ }
59
+ async function findLargeArchiveFilesInTarArchive(filePath) {
60
+ const largeArchiveFiles = [];
61
+ await tar.list({
62
+ file: filePath,
63
+ onReadEntry: entry => {
64
+ if (entry.type === 'File' && isDeployArchiveFile(entry.path) && entry.size > LARGE_ARCHIVE_FILE_SIZE_LIMIT) {
65
+ largeArchiveFiles.push({
66
+ path: entry.path,
67
+ size: entry.size
68
+ });
69
+ }
70
+ }
71
+ });
72
+ return largeArchiveFiles;
73
+ }
74
+ async function findLargeArchiveFilesInDeployArchive(filePath) {
75
+ const ext = getDeployFileExt(filePath);
76
+ if (ext === '.zip') {
77
+ return findLargeArchiveFilesInZipArchive(filePath);
78
+ }
79
+ if (ext === '.tar.gz' || ext === '.tgz') {
80
+ return findLargeArchiveFilesInTarArchive(filePath);
81
+ }
82
+ return [];
83
+ }
25
84
 
26
85
  /**
27
86
  * Check if a file has a valid extension
@@ -30,10 +89,7 @@ const macosxDir = '__MACOSX';
30
89
  * @returns {boolean} True if the extension is valid
31
90
  */
32
91
  function validateDeployFileExt(filename) {
33
- let ext = _path.default.extname(filename).toLowerCase();
34
- if (ext === '.gz' && _path.default.extname(_path.default.basename(filename, ext)) === '.tar') {
35
- ext = '.tar.gz';
36
- }
92
+ const ext = getDeployFileExt(filename);
37
93
  if (!['.zip', '.tar.gz', '.tgz'].includes(ext)) {
38
94
  exit.withError(errorMessages.invalidExt);
39
95
  }
@@ -4,9 +4,8 @@ exports.__esModule = true;
4
4
  exports.fileLineValidations = fileLineValidations;
5
5
  exports.getReadInterface = getReadInterface;
6
6
  var _debug = _interopRequireDefault(require("debug"));
7
- var _nodeFs = require("node:fs");
7
+ var _nodeEvents = require("node:events");
8
8
  var _promises = require("node:fs/promises");
9
- var _nodeReadline = require("node:readline");
10
9
  var exit = _interopRequireWildcard(require("../../lib/cli/exit"));
11
10
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -18,11 +17,8 @@ async function getReadInterface(filename) {
18
17
  } catch {
19
18
  exit.withError('The file at the provided path is either missing or not readable. Please check the input and try again.');
20
19
  }
21
- return (0, _nodeReadline.createInterface)({
22
- input: (0, _nodeFs.createReadStream)('', {
23
- fd
24
- }),
25
- output: undefined
20
+ return fd.readLines({
21
+ encoding: 'binary'
26
22
  });
27
23
  }
28
24
  async function fileLineValidations(appId, envId, fileName, validations, searchReplace) {
@@ -41,8 +37,7 @@ async function fileLineValidations(appId, envId, fileName, validations, searchRe
41
37
  });
42
38
 
43
39
  // Block until the processing completes
44
- await new Promise(resolve => readInterface.on('close', resolve));
45
- readInterface.close();
40
+ await (0, _nodeEvents.once)(readInterface, 'close');
46
41
  return Promise.all(validations.map(validation => {
47
42
  if (Object.hasOwn(validation, 'postLineExecutionProcessing') && typeof validation.postLineExecutionProcessing === 'function') {
48
43
  return validation.postLineExecutionProcessing({
@@ -6,6 +6,7 @@ exports.validateFilename = validateFilename;
6
6
  exports.validateImportFileExtension = void 0;
7
7
  var _singleLineLog = require("@wwa/single-line-log");
8
8
  var _chalk = _interopRequireDefault(require("chalk"));
9
+ var _nodeEvents = require("node:events");
9
10
  var _nodePath = _interopRequireDefault(require("node:path"));
10
11
  var exit = _interopRequireWildcard(require("../../lib/cli/exit"));
11
12
  var _clientFileUploader = require("../../lib/client-file-uploader");
@@ -472,8 +473,7 @@ const validate = async (filename, options = DEFAULT_VALIDATION_OPTIONS) => {
472
473
  });
473
474
 
474
475
  // Block until the processing completes
475
- await new Promise(resolve => readInterface.on('close', resolve));
476
- readInterface.close();
476
+ await (0, _nodeEvents.once)(readInterface, 'close');
477
477
  await postLineExecutionProcessing({
478
478
  isImport: options.isImport,
479
479
  skipChecks: options.skipChecks
@@ -16,6 +16,7 @@ Goal: remove the abandoned `args` package, keep CLI behavior stable, and support
16
16
  - `_opts` controls are still honored: app/env context fetch, confirmation gating, output formatting, wildcard command handling, required positional args.
17
17
  - Shared formatting/output and telemetry hooks are still in the wrapper path.
18
18
  - Local nested subcommand dispatch still works via sibling executable resolution.
19
+ - Short-option equals normalization: `-x=value` is rewritten to `-x value` for short options expecting values before parsing; boolean short flags are not affected. Tracked option defaults are applied after parsing when the option remains undefined post-parse, preserving default+parser parity.
19
20
 
20
21
  ## Post-Migration Hardening
21
22
 
package/docs/RELEASING.md CHANGED
@@ -51,6 +51,8 @@ A few steps should be completed before releasing:
51
51
 
52
52
  1. You have completed [final testing before deployment](TESTING.md#final-testing-before-releasing).
53
53
 
54
+ 1. Run the release smoke tests to verify high-risk CLI parser/dispatch commands. First ensure the binaries are built (`npm run build`), then run `npm run smoke:release`. This tests option parsing, short-option equals syntax, and command routing against the built binaries.
55
+
54
56
  1. The pre-publish [script](https://github.com/Automattic/vip-cli/blob/trunk/helpers/prepublishOnly.js) has been run. This script performs some confidence checks to avoid common mistakes.
55
57
 
56
58
  1. Finally, release your changes as a [new minor or major NPM version](#releasing-a-new-version).
package/docs/TESTING.md CHANGED
@@ -64,4 +64,19 @@ VIP_PROXY="" API_HOST=http://localhost:4000 node ./dist/bin/vip -- wp option get
64
64
 
65
65
  ## Final testing before releasing
66
66
 
67
- TODO: How should final testing before releasing be done?
67
+ Before releasing a new version, run the release smoke tests to verify high-risk CLI commands:
68
+
69
+ ```bash
70
+ npm run smoke:release
71
+ ```
72
+
73
+ This script tests critical parser/dispatch paths for commands like `logs`, `slowlogs`, `wp`, and `dev-env shell`. It verifies:
74
+
75
+ - Short-option equals syntax (e.g., `-l=10`)
76
+ - Default value explicit matches
77
+ - Option-only and separator/subcommand handling
78
+ - Command routing without requiring credentials, API access, or Docker/Lando
79
+
80
+ The tests target the built `dist/bin/*.js` binaries, so ensure `npm run build` has been run first. All tests use `--help` or other non-executing forms to avoid side effects.
81
+
82
+ Exit code 0 indicates all tests passed; non-zero indicates failures that should be reviewed before release.
@@ -83,8 +83,8 @@ function buildBundle() {
83
83
  format: 'cjs',
84
84
  outfile: bundlePath,
85
85
  external: [
86
- '@postman/node-keytar',
87
- '@postman/node-keytar/*',
86
+ '@github/keytar',
87
+ '@github/keytar/*',
88
88
  'cpu-features',
89
89
  'cpu-features/*',
90
90
  'lando',