@automattic/vip 4.0.2 → 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 +864 -1092
  34. package/package.json +22 -17
  35. package/test-utils/undici-mock.js +25 -0
package/AGENTS.md CHANGED
@@ -96,3 +96,5 @@ Guide for future agents working on this codebase. Focus on traps, cross-cutting
96
96
  - Respect exit semantics: uncaught exceptions routed to `exit.withError`; GraphQL errors call `process.exit(1)` unless explicitly disabled; 401 auth errors exit with guidance.
97
97
  - Watch shared state: current implementation mutates a global; ensure new parser avoids cross-command bleeding when multiple command instances run in-process (tests/CLI wrappers).
98
98
  - Login guardrails in `vip.js` let certain commands bypass auth; preserve equivalent shortcuts or add explicit non-interactive flags for CI.
99
+ - Normalize short-option equals syntax: rewrite `-x=value` to `-x value` before parsing for short options that expect values; track which short options take values to avoid breaking boolean flags that should remain flag-only.
100
+ - Apply tracked defaults after parsing: when an option has both a default and a custom parser function, apply the default only when `opts[optionName] === undefined` after the parser runs, so that explicit values and defaults maintain parity with the parser contract (see test `parses short options using equals syntax without prefixing values` in `__tests__/lib/cli/command.js`).
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ exports.__esModule = true;
5
+ exports.appDeployTokenGenerateCmd = appDeployTokenGenerateCmd;
6
+ var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
7
+ var _api = _interopRequireDefault(require("../lib/api"));
8
+ var _command = _interopRequireDefault(require("../lib/cli/command"));
9
+ var exit = _interopRequireWildcard(require("../lib/cli/exit"));
10
+ var _tracker = require("../lib/tracker");
11
+ var _utils = require("../lib/utils");
12
+ 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); }
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ const usage = 'vip app deploy-token generate';
15
+ const examples = [{
16
+ usage: 'vip @example-app.develop app deploy-token generate',
17
+ description: 'Generate a custom deploy access token for the selected environment.'
18
+ }, {
19
+ usage: 'vip @example-app.develop app deploy-token generate --format=json',
20
+ description: 'Generate a custom deploy access token and print the result as JSON.'
21
+ }];
22
+ const appQuery = `
23
+ id,
24
+ name,
25
+ environments{
26
+ id
27
+ appId
28
+ type
29
+ name
30
+ uniqueLabel
31
+ }
32
+ `;
33
+ const GENERATE_CUSTOM_DEPLOY_ACCESS_MUTATION = (0, _graphqlTag.default)`
34
+ mutation GenerateCustomDeployAccess($input: GenerateCustomDeployAccessInput) {
35
+ generateCustomDeployAccess(input: $input) {
36
+ token
37
+ expiresAt
38
+ }
39
+ }
40
+ `;
41
+ async function appDeployTokenGenerateCmd(_arg, {
42
+ app,
43
+ env
44
+ }) {
45
+ const trackerFn = (0, _tracker.makeCommandTracker)('deploy_token_generate', {
46
+ app: app.id,
47
+ env: env.uniqueLabel
48
+ });
49
+ await trackerFn('execute');
50
+ const envId = env.id;
51
+ if (typeof envId !== 'number') {
52
+ await trackerFn('error', {
53
+ error: 'Missing environment id'
54
+ });
55
+ exit.withError('Failed to generate deploy token: environment id is missing.');
56
+ return;
57
+ }
58
+ const api = (0, _api.default)();
59
+ let response;
60
+ try {
61
+ response = await api.mutate({
62
+ mutation: GENERATE_CUSTOM_DEPLOY_ACCESS_MUTATION,
63
+ variables: {
64
+ input: {
65
+ environmentIds: [envId]
66
+ }
67
+ }
68
+ });
69
+ } catch (err) {
70
+ const message = (0, _utils.parseApiError)(err) ?? 'Unknown error';
71
+ await trackerFn('error', {
72
+ error: message
73
+ });
74
+ exit.withError(`Failed to generate deploy token: ${message}`);
75
+ return;
76
+ }
77
+ const tokenPayload = response.data?.generateCustomDeployAccess;
78
+ if (!tokenPayload?.token || !tokenPayload.expiresAt) {
79
+ await trackerFn('error', {
80
+ error: 'Missing generateCustomDeployAccess payload'
81
+ });
82
+ exit.withError('Failed to generate deploy token: API returned an empty response.');
83
+ return;
84
+ }
85
+ await trackerFn('success');
86
+ return [{
87
+ appId: app.id,
88
+ appName: app.name,
89
+ envId,
90
+ env: env.uniqueLabel,
91
+ token: tokenPayload.token,
92
+ expiresAt: tokenPayload.expiresAt
93
+ }];
94
+ }
95
+ void (0, _command.default)({
96
+ appContext: true,
97
+ appQuery,
98
+ envContext: true,
99
+ format: true,
100
+ requiredArgs: 0,
101
+ usage
102
+ }).examples(examples).argv(process.argv, appDeployTokenGenerateCmd);
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ var _command = _interopRequireDefault(require("../lib/cli/command"));
5
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
+ const usage = 'vip app deploy-token';
7
+ const exampleUsage = 'vip @example-app.develop app deploy-token';
8
+ const examples = [{
9
+ usage: `${exampleUsage} generate`,
10
+ description: 'Generate a custom deploy access token for an environment that already has Custom Deployment enabled.'
11
+ }, {
12
+ usage: `${exampleUsage} generate --format=json | jq -r '.[0].token'`,
13
+ description: 'Generate a token and extract the token value for scripting workflows.'
14
+ }];
15
+ void (0, _command.default)({
16
+ requiredArgs: 0,
17
+ usage
18
+ }).command('generate', 'Generate a custom deploy access token for an environment.').examples(examples).argv(process.argv, () => {
19
+ process.exit(0);
20
+ });
@@ -79,6 +79,9 @@ async function appDeployCmd(arg = [], opts = {}) {
79
79
  const track = _tracker.trackEventWithEnv.bind(null, appId, envId);
80
80
  debug('Validating file...');
81
81
  await (0, _customDeploy.validateFile)(appId, envId, fileMeta);
82
+ if (!opts.skipLargeFileValidation) {
83
+ await (0, _customDeploy.validateLargeArchiveFiles)(appId, envId, fileMeta);
84
+ }
82
85
  await track('deploy_app_command_execute');
83
86
 
84
87
  // Upload file as different name to avoid overwriting existing same named files
@@ -229,8 +232,11 @@ const examples = [
229
232
  }, {
230
233
  usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.tar.gz --skip-confirmation',
231
234
  description: 'Skip the confirmation prompt for the Custom Deployment of the archived file named "file.tar.gz" to the environment.'
235
+ }, {
236
+ usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip --skip-large-file-validation',
237
+ description: 'Skip checking the deploy archive for repository archive files larger than 20 MB.'
232
238
  }];
233
239
  void (0, _command.default)({
234
240
  requiredArgs: 1,
235
241
  usage: baseUsage
236
- }).command('validate', 'Validate the directory structure of an archived file.').examples(examples).option('message', 'Add a description of a deployment.').option('skip-confirmation', 'Skip the confirmation prompt.').option('force', 'Skip confirmation prompt (deprecated)').option('app', 'Target an application. Accepts a string value for the application name or an integer for the application ID.').option('env', 'Target an environment. Accepts a string value for the environment type.').argv(process.argv, appDeployCmd);
242
+ }).command('validate', 'Validate the directory structure of an archived file.').examples(examples).option('message', 'Add a description of a deployment.').option('skip-confirmation', 'Skip the confirmation prompt.').option('skip-large-file-validation', 'Skip checking for repository archive files over 20 MB.', false).option('force', 'Skip confirmation prompt (deprecated)').option('app', 'Target an application. Accepts a string value for the application name or an integer for the application ID.').option('env', 'Target an environment. Accepts a string value for the environment type.').argv(process.argv, appDeployCmd);
@@ -11,7 +11,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
11
11
  (0, _command.default)({
12
12
  requiredArgs: 1,
13
13
  format: true
14
- }).example('vip app list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').example('vip app example-app', 'Retrieve information about the application named "example-app" and its environments.').example('WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip', 'Deploy a local archived file named "file.zip" that contains application code to a VIP Platform environment that has Custom Deployment enabled.').command('list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').command('deploy', 'Deploy an archived file of application code to an environment that has Custom Deployment enabled.').argv(process.argv, async arg => {
14
+ }).example('vip app list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').example('vip app example-app', 'Retrieve information about the application named "example-app" and its environments.').example('WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip', 'Deploy a local archived file named "file.zip" that contains application code to a VIP Platform environment that has Custom Deployment enabled.').example('vip @example-app.develop app deploy-token generate --format=json', 'Generate a custom deploy token for the environment and print it in JSON format for use with tools like jq.').command('list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').command('deploy', 'Deploy an archived file of application code to an environment that has Custom Deployment enabled.').command('deploy-token', 'Generate and manage custom deploy access tokens for environments that have Custom Deployment enabled.').argv(process.argv, async arg => {
15
15
  await (0, _tracker.trackEvent)('app_command_execute');
16
16
  let res;
17
17
  try {
@@ -53,7 +53,6 @@ cmd.argv(process.argv, async (arg, opt) => {
53
53
  ...opt
54
54
  };
55
55
  const configurationFileOptions = await (0, _devEnvironmentConfigurationFile.getConfigurationFileOptions)();
56
- const thereAreOptionsFromConfigFile = Object.keys(configurationFileOptions).length > 0;
57
56
  const finalPreselectedOptions = (0, _devEnvironmentConfigurationFile.mergeConfigurationFileOptions)(preselectedOptions, configurationFileOptions);
58
57
 
59
58
  /** @type {InstanceOptions} */
@@ -74,10 +73,7 @@ cmd.argv(process.argv, async (arg, opt) => {
74
73
  cron: currentInstanceData.cron,
75
74
  adminPassword: currentInstanceData.adminPassword
76
75
  };
77
- const providedOptions = Object.keys(opt).filter(option => option.length > 1) // Filter out single letter aliases
78
- .filter(option => !['debug', 'help', 'slug'].includes(option)); // Filter out options that are not related to instance configuration
79
-
80
- const suppressPrompts = providedOptions.length > 0 || thereAreOptionsFromConfigFile;
76
+ const suppressPrompts = !process.stdin.isTTY;
81
77
  const instanceData = await (0, _devEnvironmentCli.promptForArguments)(finalPreselectedOptions, defaultOptions, suppressPrompts, false);
82
78
  instanceData.siteSlug = slug;
83
79
  await (0, _devEnvironmentCore.updateEnvironment)(lando, instanceData);
@@ -115,7 +115,10 @@ class DevEnvImportSQLCommand {
115
115
  if (!dumpDetails.sourceDb) {
116
116
  console.log(`${_chalk.default.yellow('!')} Unable to identify source DB. Skipping myloader source-db option`);
117
117
  }
118
- importArg = ['db-myloader', '--overwrite-tables', ...(dumpDetails.sourceDb ? [`--source-db=${dumpDetails.sourceDb}`] : []), `--threads=${threadCount}`, '--max-threads-for-schema-creation=10', '--max-threads-for-index-creation=10', '--skip-triggers', '--skip-post', '--optimize-keys', '--checksum=SKIP', '--metadata-refresh-interval=2000000', '--stream'].concat(this.options.quiet ? ['--verbose=0'] : ['--verbose=3']);
118
+ importArg = ['db-myloader',
119
+ // Drop existing tables before restoring. Do not use the long form:
120
+ // `--overwrite-tables` is a silent no-op on myloader >= 0.20.
121
+ '-o', ...(dumpDetails.sourceDb ? [`--source-db=${dumpDetails.sourceDb}`] : []), `--threads=${threadCount}`, '--max-threads-for-schema-creation=10', '--max-threads-for-index-creation=10', '--skip-triggers', '--skip-post', '--optimize-keys', '--checksum=SKIP', '--metadata-refresh-interval=2000000', '--stream'].concat(this.options.quiet ? ['--verbose=0'] : ['--verbose=3']);
119
122
  }
120
123
  return importArg;
121
124
  }
@@ -180,12 +180,11 @@ class DevEnvSyncSQLCommand {
180
180
  const readStream = _fs.default.createReadStream(this.sqlFile);
181
181
  const replacedStream = await (0, _vipSearchReplace.replace)(readStream, replacements);
182
182
  const outputFile = `${this.tmpDir}/sql-export-sr.sql`;
183
- const streams = [replacedStream];
183
+ const transforms = [];
184
184
  if (this.getSqlDumpType() === _database.SqlDumpType.MYDUMPER) {
185
- streams.push((0, _database.fixMyDumperTransform)());
185
+ transforms.push((0, _database.fixMyDumperTransform)());
186
186
  }
187
- streams.push(_fs.default.createWriteStream(outputFile));
188
- await (0, _promises.pipeline)(streams);
187
+ await (0, _promises.pipeline)(replacedStream, ...transforms, _fs.default.createWriteStream(outputFile));
189
188
  _fs.default.renameSync(outputFile, this.sqlFile);
190
189
  }
191
190
  getSiteUrlsFromSDS() {
@@ -3,8 +3,8 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _debug = _interopRequireDefault(require("debug"));
6
- var _nodeFetch = _interopRequireDefault(require("node-fetch"));
7
6
  var _querystring = _interopRequireDefault(require("querystring"));
7
+ var _undici = require("undici");
8
8
  var _apiConfig = require("../../cli/apiConfig");
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
10
  const debug = (0, _debug.default)('@automattic/vip:analytics:clients:tracks');
@@ -94,7 +94,7 @@ class Tracks {
94
94
  'User-Agent': this.userAgent
95
95
  };
96
96
  debug('send()', body);
97
- return (0, _nodeFetch.default)(Tracks.ENDPOINT, {
97
+ return (0, _undici.fetch)(Tracks.ENDPOINT, {
98
98
  method,
99
99
  body,
100
100
  headers
@@ -3,19 +3,25 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _debug = _interopRequireDefault(require("debug"));
6
- var _nodeFetch = _interopRequireDefault(require("node-fetch"));
6
+ var _undici = require("undici");
7
7
  var _api = require("../../lib/api");
8
8
  var _env = _interopRequireDefault(require("../../lib/env"));
9
- var _proxyAgent = require("../../lib/http/proxy-agent");
9
+ var _proxyDispatcher = require("../../lib/http/proxy-dispatcher");
10
10
  var _token = _interopRequireDefault(require("../../lib/token"));
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
12
  const debug = (0, _debug.default)('@automattic/vip:http');
13
+ const isPlainObjectBody = value => {
14
+ if (!value || typeof value !== 'object') {
15
+ return false;
16
+ }
17
+ const prototype = Object.getPrototypeOf(value);
18
+ return prototype === Object.prototype || prototype === null;
19
+ };
20
+
13
21
  /**
14
22
  * Call the Public API with an arbitrary path (e.g. to connect to REST endpoints).
15
23
  * This will include the token in an Authorization header so requests are "logged-in."
16
24
  *
17
- * This is simply a wrapper around node-fetch
18
- *
19
25
  * @param {string} path API path to pass to `fetch` -- will be prefixed by the API_HOST
20
26
  * @param {Object} options options to pass to `fetch`
21
27
  * @return {Promise} Return value of the `fetch` call
@@ -29,26 +35,25 @@ var _default = async (path, options = {}) => {
29
35
  url = `${_api.API_HOST}${path}`;
30
36
  }
31
37
  const authToken = await _token.default.get();
32
- const proxyAgent = (0, _proxyAgent.createProxyAgent)(url);
38
+ const proxyDispatcher = (0, _proxyDispatcher.createProxyDispatcher)(url);
39
+ const shouldSerializeJsonBody = isPlainObjectBody(options.body);
33
40
  debug('running fetch', url);
34
- const headers = new Headers({
35
- ...options.headers
36
- });
41
+ const headers = new _undici.Headers(options.headers);
37
42
  if (!headers.has('Authorization')) {
38
43
  headers.set('Authorization', `Bearer ${authToken.raw}`);
39
44
  }
40
45
  if (!headers.has('User-Agent')) {
41
46
  headers.set('User-Agent', _env.default.userAgent);
42
47
  }
43
- if (!headers.has('Content-Type') && options.method !== 'GET') {
48
+ if (!headers.has('Content-Type') && options.method !== 'GET' && shouldSerializeJsonBody) {
44
49
  headers.set('Content-Type', 'application/json');
45
50
  }
46
- const opts = {
51
+ const requestBody = shouldSerializeJsonBody ? JSON.stringify(options.body) : options.body;
52
+ return (0, _undici.fetch)(url, {
47
53
  ...options,
48
- agent: proxyAgent ?? undefined,
54
+ dispatcher: proxyDispatcher ?? undefined,
49
55
  headers,
50
- body: typeof options.body === 'object' ? JSON.stringify(options.body) : options.body
51
- };
52
- return (0, _nodeFetch.default)(url, opts);
56
+ body: requestBody
57
+ });
53
58
  };
54
59
  exports.default = _default;
package/dist/lib/api.js CHANGED
@@ -12,7 +12,6 @@ var _retry = require("@apollo/client/link/retry");
12
12
  var _chalk = _interopRequireDefault(require("chalk"));
13
13
  var _debug = _interopRequireDefault(require("debug"));
14
14
  var _graphql = require("graphql");
15
- var _nodeFetch = require("node-fetch");
16
15
  var _http = _interopRequireDefault(require("./api/http"));
17
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
17
  // Config
@@ -41,7 +40,7 @@ function shouldRetryRequest(attempt, operation, error) {
41
40
  debug(`Request failed and max retry attempts reached. ${debugSuffix}`, error);
42
41
  return false;
43
42
  }
44
- if (error instanceof _nodeFetch.FetchError && error.code === 'ECONNREFUSED') {
43
+ if (error?.code === 'ECONNREFUSED') {
45
44
  debug(`Request failed. Retrying request due to connection refused error. ${debugSuffix}`);
46
45
  return true;
47
46
  }
@@ -323,6 +323,41 @@ class CommanderArgsCompat {
323
323
  }
324
324
  return null;
325
325
  }
326
+ hasWpCommandPayload(subcommandArgs) {
327
+ const wpFlagsWithoutValue = new Set(['--help', '-h', '--version', '-v', '--yes', '-y']);
328
+ let expectOptionValue = false;
329
+ for (const arg of subcommandArgs) {
330
+ if (expectOptionValue) {
331
+ if (!isOptionToken(arg)) {
332
+ expectOptionValue = false;
333
+ continue;
334
+ }
335
+ expectOptionValue = false;
336
+ }
337
+ if (!isOptionToken(arg)) {
338
+ return true;
339
+ }
340
+ if (arg.startsWith('--')) {
341
+ if (arg.includes('=') || wpFlagsWithoutValue.has(arg)) {
342
+ continue;
343
+ }
344
+ expectOptionValue = true;
345
+ continue;
346
+ }
347
+ if (/^-[A-Za-z0-9]$/.test(arg)) {
348
+ if (wpFlagsWithoutValue.has(arg)) {
349
+ continue;
350
+ }
351
+ expectOptionValue = true;
352
+ continue;
353
+ }
354
+ if (/^-[A-Za-z0-9]+=/.test(arg)) {
355
+ continue;
356
+ }
357
+ return true;
358
+ }
359
+ return false;
360
+ }
326
361
  async executeSubcommand(argv, parsedAlias, subcommand) {
327
362
  const currentScript = argv[1];
328
363
  const subcommandName = subcommand.name;
@@ -334,8 +369,9 @@ class CommanderArgsCompat {
334
369
  const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes('--');
335
370
  const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter(arg => arg !== '--');
336
371
  const subcommandArgs = parsedAlias.argv.slice(subcommand.index + 1);
372
+ const hasWpCommandPayload = this.hasWpCommandPayload(subcommandArgs);
337
373
  const hasSeparator = argv.includes('--');
338
- if (subcommandName === 'wp' && subcommandArgs.length && !hasSeparator) {
374
+ if (subcommandName === 'wp' && hasWpCommandPayload && !hasSeparator) {
339
375
  exit.withError('A double dash ("--") must separate the arguments of "vip" from those of "wp". Run "vip wp --help" for examples.');
340
376
  }
341
377
  let childArgs = [...argsBeforeSubcommand, ...(hasSeparatorBeforeSubcommand ? ['--'] : []), ...subcommandArgs];
@@ -8,6 +8,8 @@ const internalBinLoaders = {
8
8
  vip: () => import('../../bin/vip'),
9
9
  'vip-app': () => import('../../bin/vip-app'),
10
10
  'vip-app-deploy': () => import('../../bin/vip-app-deploy'),
11
+ 'vip-app-deploy-token': () => import('../../bin/vip-app-deploy-token'),
12
+ 'vip-app-deploy-token-generate': () => import('../../bin/vip-app-deploy-token-generate'),
11
13
  'vip-app-deploy-validate': () => import('../../bin/vip-app-deploy-validate'),
12
14
  'vip-app-list': () => import('../../bin/vip-app-list'),
13
15
  'vip-backup': () => import('../../bin/vip-backup'),
@@ -10,6 +10,7 @@ exports.getFileSize = getFileSize;
10
10
  exports.getPartBoundaries = getPartBoundaries;
11
11
  exports.getSignedUploadRequestData = getSignedUploadRequestData;
12
12
  exports.isFile = isFile;
13
+ exports.parseEtagHeader = parseEtagHeader;
13
14
  exports.unzipFile = void 0;
14
15
  exports.uploadImportFileToS3 = uploadImportFileToS3;
15
16
  exports.uploadParts = uploadParts;
@@ -17,28 +18,44 @@ var _chalk = _interopRequireDefault(require("chalk"));
17
18
  var _crypto = require("crypto");
18
19
  var _debug = _interopRequireDefault(require("debug"));
19
20
  var _fs = require("fs");
20
- var _nodeFetch = _interopRequireDefault(require("node-fetch"));
21
21
  var _promises = require("node:fs/promises");
22
22
  var _promises2 = require("node:stream/promises");
23
+ var _promises3 = require("node:timers/promises");
23
24
  var _os = _interopRequireDefault(require("os"));
24
25
  var _path = _interopRequireDefault(require("path"));
25
26
  var _stream = require("stream");
27
+ var _undici = require("undici");
26
28
  var _xml2js = require("xml2js");
27
29
  var _zlib = require("zlib");
28
30
  var _http = _interopRequireDefault(require("../lib/api/http"));
29
31
  var _fileSize = require("../lib/constants/file-size");
30
32
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
31
- // Need to use CommonJS imports here as the `fetch-retry` typedefs are messed up and throwing TypeJS errors when using `import`
32
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
33
- const fetchWithRetry =
34
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-require-imports
35
- require('fetch-retry')(_nodeFetch.default, {
36
- // Set default retry options
37
- retries: 3,
38
- retryDelay: attempt => {
39
- return Math.pow(2, attempt) * 1000; // 1000, 2000, 4000
33
+ function parseEtagHeader(etag) {
34
+ const normalizedEtag = etag.replace(/^W\//u, '').trim();
35
+ if (normalizedEtag.startsWith('"') && normalizedEtag.endsWith('"')) {
36
+ try {
37
+ return JSON.parse(normalizedEtag);
38
+ } catch (err) {
39
+ debug(`Unable to JSON.parse ETag header, falling back to raw value: ${err.message}`);
40
+ }
40
41
  }
41
- });
42
+ return normalizedEtag.replace(/^"(.*)"$/u, '$1');
43
+ }
44
+ async function fetchWithRetry(input, init, retries = 3) {
45
+ for (let attempt = 0; attempt <= retries; attempt++) {
46
+ try {
47
+ // eslint-disable-next-line no-await-in-loop
48
+ return await (0, _undici.fetch)(input, init);
49
+ } catch (err) {
50
+ if (attempt === retries) {
51
+ throw err;
52
+ }
53
+ // eslint-disable-next-line no-await-in-loop
54
+ await (0, _promises3.setTimeout)(Math.pow(2, attempt) * 1000); // 1000, 2000, 4000
55
+ }
56
+ }
57
+ throw new Error('unreachable');
58
+ }
42
59
  const debug = (0, _debug.default)('vip:lib/client-file-uploader');
43
60
 
44
61
  // Files smaller than COMPRESS_THRESHOLD will not be compressed before upload
@@ -510,9 +527,11 @@ async function uploadPart({
510
527
  }).pipe(progressPassThrough);
511
528
  const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions);
512
529
  if (fetchResponse.status === 200) {
513
- const responseHeaders = fetchResponse.headers.raw();
514
- const [etag] = responseHeaders.etag;
515
- return JSON.parse(etag);
530
+ const etag = fetchResponse.headers.get('etag');
531
+ if (!etag) {
532
+ throw new Error('Unable to upload file part. Missing ETag response header.');
533
+ }
534
+ return parseEtagHeader(etag);
516
535
  }
517
536
  const result = await fetchResponse.text();
518
537
 
@@ -3,6 +3,8 @@
3
3
  exports.__esModule = true;
4
4
  exports.validateCustomDeployKey = validateCustomDeployKey;
5
5
  exports.validateFile = validateFile;
6
+ exports.validateLargeArchiveFiles = validateLargeArchiveFiles;
7
+ var _chalk = _interopRequireDefault(require("chalk"));
6
8
  var _fs = _interopRequireDefault(require("fs"));
7
9
  var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
8
10
  var _api = _interopRequireDefault(require("../../lib/api"));
@@ -15,6 +17,14 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
15
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
18
  const DEPLOY_MAX_FILE_SIZE = 4 * _fileSize.GB_IN_BYTES;
17
19
  const WPVIP_DEPLOY_TOKEN = process.env.WPVIP_DEPLOY_TOKEN;
20
+ const SKIP_LARGE_FILE_VALIDATION_FLAG = '--skip-large-file-validation';
21
+ function formatFileSize(bytes) {
22
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
23
+ }
24
+ function getLargeArchiveFilesMessage(largeArchiveFiles) {
25
+ const fileList = largeArchiveFiles.map(file => `- ${file.path} (${formatFileSize(file.size)})`).join('\n');
26
+ return `Deploy archive contains archive file(s) larger than ${formatFileSize(_customDeploy.LARGE_ARCHIVE_FILE_SIZE_LIMIT)} in the deploy archive:\n${fileList}\nRemove these files from the repository, or rerun with ${_chalk.default.bold(SKIP_LARGE_FILE_VALIDATION_FLAG)} to skip this check.`;
27
+ }
18
28
  async function validateCustomDeployKey(app, env) {
19
29
  if (!WPVIP_DEPLOY_TOKEN) {
20
30
  exit.withError('Valid custom deploy key is required.');
@@ -119,4 +129,31 @@ async function validateFile(appId, envId, fileMeta) {
119
129
  });
120
130
  exit.withError(`The deploy file size (${fileSize} bytes) exceeds the limit (${DEPLOY_MAX_FILE_SIZE} bytes).`);
121
131
  }
132
+ }
133
+
134
+ /**
135
+ * @param {number} appId
136
+ * @param {number} envId
137
+ * @param {FileMeta} fileMeta
138
+ */
139
+ async function validateLargeArchiveFiles(appId, envId, fileMeta) {
140
+ const track = _tracker.trackEventWithEnv.bind(null, appId, envId);
141
+ let largeArchiveFiles;
142
+ try {
143
+ largeArchiveFiles = await (0, _customDeploy.findLargeArchiveFilesInDeployArchive)(fileMeta.fileName);
144
+ } catch (error) {
145
+ await track('deploy_app_command_error', {
146
+ error_type: 'large-archive-file-verify-failed',
147
+ verify_error: error.message
148
+ });
149
+ return exit.withError(`Unable to verify large archive files in the deploy archive: ${error.message}. Rerun with ${SKIP_LARGE_FILE_VALIDATION_FLAG} to skip this check.`);
150
+ }
151
+ if (!largeArchiveFiles.length) {
152
+ return;
153
+ }
154
+ await track('deploy_app_command_error', {
155
+ error_type: 'large-archive-files',
156
+ large_archive_files: largeArchiveFiles.map(file => file.path)
157
+ });
158
+ return exit.withError(getLargeArchiveFilesMessage(largeArchiveFiles));
122
159
  }
@@ -28,19 +28,19 @@ var _debug = _interopRequireDefault(require("debug"));
28
28
  var _ejs = _interopRequireDefault(require("ejs"));
29
29
  var _enquirer = require("enquirer");
30
30
  var _graphql = require("graphql");
31
- var _nodeFetch = _interopRequireDefault(require("node-fetch"));
32
31
  var _nodeCrypto = require("node:crypto");
33
32
  var _nodeFs = _interopRequireDefault(require("node:fs"));
34
33
  var _promises = require("node:fs/promises");
35
34
  var _nodePath = _interopRequireDefault(require("node:path"));
36
35
  var _semver = _interopRequireDefault(require("semver"));
36
+ var _undici = require("undici");
37
37
  var _devEnvironmentCli = require("./dev-environment-cli");
38
38
  var _devEnvironmentLando = require("./dev-environment-lando");
39
39
  var _landoLoader = require("./lando-loader");
40
40
  var _app = _interopRequireDefault(require("../api/app"));
41
41
  var _software = require("../config/software");
42
42
  var _devEnvironment = require("../constants/dev-environment");
43
- var _proxyAgent = require("../http/proxy-agent");
43
+ var _proxyDispatcher = require("../http/proxy-dispatcher");
44
44
  var _searchAndReplace = require("../search-and-replace");
45
45
  var _userError = _interopRequireDefault(require("../user-error"));
46
46
  var _xdgData = require("../xdg-data");
@@ -740,9 +740,9 @@ async function maybeUpdateVersion(lando, slug) {
740
740
  */
741
741
  function fetchVersionList() {
742
742
  const url = `https://${_devEnvironment.DEV_ENVIRONMENT_RAW_GITHUB_HOST}${_devEnvironment.DEV_ENVIRONMENT_WORDPRESS_VERSIONS_URI}`;
743
- const proxyAgent = (0, _proxyAgent.createProxyAgent)(url);
744
- return (0, _nodeFetch.default)(url, {
745
- agent: proxyAgent ?? undefined
743
+ const proxyDispatcher = (0, _proxyDispatcher.createProxyDispatcher)(url);
744
+ return (0, _undici.fetch)(url, {
745
+ dispatcher: proxyDispatcher ?? undefined
746
746
  }).then(res => res.json());
747
747
  }
748
748
 
@@ -487,11 +487,15 @@ async function landoInfo(lando, instancePath, options = {}) {
487
487
 
488
488
  // Add login information
489
489
  if (frontEndUrl) {
490
- let loginUrl = `${frontEndUrl}wp-admin/`;
490
+ const loginURL = new URL(`${frontEndUrl}wp-admin/`);
491
491
  if (options.autologinKey) {
492
- loginUrl += `?vip-dev-autologin=${options.autologinKey}`;
492
+ loginURL.searchParams.set('vip-dev-autologin', options.autologinKey);
493
493
  }
494
- appInfo['Login URL'] = loginUrl;
494
+ loginURL.protocol = 'http:';
495
+ const httpLoginUrl = loginURL.toString();
496
+ loginURL.protocol = 'https:';
497
+ const httpsLoginUrl = loginURL.toString();
498
+ appInfo['Login URL'] = [httpsLoginUrl, httpLoginUrl].join(_nodeOs.EOL);
495
499
  appInfo['Default username'] = 'vipgo';
496
500
 
497
501
  // Get the stored password from instance data
@@ -5,11 +5,16 @@ exports.downloadFile = void 0;
5
5
  var fs = _interopRequireWildcard(require("fs"));
6
6
  var _stream = require("stream");
7
7
  var _promises = require("stream/promises");
8
+ var _undici = require("undici");
9
+ var _proxyDispatcher = require("./proxy-dispatcher");
8
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); }
9
11
  const downloadFile = async (fileUrl, destinationPath, onProgress) => {
10
12
  let response;
13
+ const proxyDispatcher = (0, _proxyDispatcher.createProxyDispatcher)(fileUrl);
11
14
  try {
12
- response = await fetch(fileUrl);
15
+ response = await (0, _undici.fetch)(fileUrl, {
16
+ dispatcher: proxyDispatcher ?? undefined
17
+ });
13
18
  } catch (error) {
14
19
  throw new Error(`Request to ${fileUrl} failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
15
20
  }