@automattic/vip 3.19.1 → 3.19.3-dev.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.
@@ -13,17 +13,14 @@ const exampleUsage = 'vip dev-env info';
13
13
  const usage = 'vip dev-env info';
14
14
  const examples = [{
15
15
  usage: `${exampleUsage} --slug=example-site`,
16
- description: 'Retrieve basic information about the local environment named "example-site".'
17
- }, {
18
- usage: `${exampleUsage} --slug=example-site --extended`,
19
- description: 'Retrieve a larger amount of information about the local environment named "example-site".'
16
+ description: 'Retrieve information about the local environment named "example-site".'
20
17
  }, {
21
18
  usage: `${exampleUsage} --all`,
22
- description: 'Retrieve basic information about all local environments.'
19
+ description: 'Retrieve information about all local environments.'
23
20
  }];
24
21
  (0, _command.default)({
25
22
  usage
26
- }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('all', 'Retrieve information about all local environments.').option('extended', 'Retrieve a larger amount of information.').examples(examples).argv(process.argv, async (arg, opt) => {
23
+ }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('all', 'Retrieve information about all local environments.').option('extended', 'Deprecated, not used.').examples(examples).argv(process.argv, async (arg, opt) => {
27
24
  let trackingInfo;
28
25
  let slug;
29
26
  const lando = await (0, _devEnvironmentLando.bootstrapLando)();
@@ -41,7 +38,7 @@ const examples = [{
41
38
  debug('Args: ', arg, 'Options: ', opt);
42
39
  try {
43
40
  const options = {
44
- extended: Boolean(opt.extended),
41
+ extended: true,
45
42
  suppressWarnings: true
46
43
  };
47
44
  if (opt.all) {
@@ -4,8 +4,8 @@
4
4
  var _devEnvSyncSql = require("../commands/dev-env-sync-sql");
5
5
  var _command = _interopRequireDefault(require("../lib/cli/command"));
6
6
  var _devEnvironmentCli = require("../lib/dev-environment/dev-environment-cli");
7
- var _devEnvironmentCore = require("../lib/dev-environment/dev-environment-core");
8
7
  var _devEnvironmentLando = require("../lib/dev-environment/dev-environment-lando");
8
+ var _liveBackupCopy = require("../lib/live-backup-copy");
9
9
  var _tracker = require("../lib/tracker");
10
10
  var _userError = _interopRequireDefault(require("../lib/user-error"));
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -13,6 +13,21 @@ const usage = 'vip dev-env sync sql';
13
13
  const examples = [{
14
14
  usage: `vip @example-app.develop dev-env sync sql --slug=example-site`,
15
15
  description: 'Sync the database of the develop environment in the "example-app" application to a local environment named "example-site".'
16
+ }, {
17
+ usage: `vip @example-app.develop dev-env sync sql --slug=example-site --table=wp_posts --table=wp_comments`,
18
+ description: 'Sync only the wp_posts and wp_comments tables from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
19
+ }, {
20
+ usage: `vip @example-app.develop dev-env sync sql --slug=example-site --table=wp_posts,wp_comments`,
21
+ description: 'Sync only the wp_posts and wp_comments tables using comma-separated syntax from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
22
+ }, {
23
+ usage: `vip @example-app.develop dev-env sync sql --slug=example-site --subsite-id=2 --subsite-id=3`,
24
+ description: 'Sync only the tables for the subsites with IDs 2 and 3 from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
25
+ }, {
26
+ usage: `vip @example-app.develop dev-env sync sql --slug=example-site --subsite-id=2,3`,
27
+ description: 'Sync only the tables for the subsites with IDs 2 and 3 using comma-separated syntax from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
28
+ }, {
29
+ usage: `vip @example-app.develop dev-env sync sql --slug=example-site --config-file=~/dev-env-sync-config.json`,
30
+ description: 'Use the specified config file to determine what to sync from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
16
31
  }];
17
32
  const appQuery = `
18
33
  id,
@@ -43,12 +58,17 @@ const appQuery = `
43
58
  requiredArgs: 0,
44
59
  module: 'dev-env-sync-sql',
45
60
  usage
46
- }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
61
+ }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('table', 'A table to sync from the remote environment to the local environment. Multiple tables can be specified with multiple --table flags or as a comma-separated list.').option('subsite-id', 'The ID of a subsite/network site to sync from the remote environment to the local environment. Multiple subsite IDs can be specified with multiple --subsite-id flags or as a comma-separated list.').option('wpcli-command', 'The WP-CLI command to run on the remote environment to retrieve the database export configuration.').option('config-file', 'The backup copy config file to use for the sync.', undefined).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
47
62
  const {
48
63
  app,
49
64
  env,
65
+ configFile,
66
+ table,
67
+ subsiteId,
68
+ wpcliCommand,
50
69
  ...optRest
51
70
  } = opt;
71
+ const liveBackupCopyCLIOptions = (0, _liveBackupCopy.parseLiveBackupCopyCLIOptions)(configFile, table, subsiteId, wpcliCommand);
52
72
  const slug = await (0, _devEnvironmentCli.getEnvironmentName)(optRest);
53
73
  const trackerFn = (0, _tracker.makeCommandTracker)('dev_env_sync_sql', {
54
74
  app: app.id,
@@ -58,14 +78,14 @@ const appQuery = `
58
78
  });
59
79
  await trackerFn('execute');
60
80
  const lando = await (0, _devEnvironmentLando.bootstrapLando)();
61
- const envPath = (0, _devEnvironmentCore.getEnvironmentPath)(slug);
62
- if (!(await (0, _devEnvironmentLando.isEnvUp)(lando, envPath)) && !opt.force) {
81
+ const isUp = (await Promise.all([(0, _devEnvironmentLando.isContainerRunning)(lando, slug, 'php'), (0, _devEnvironmentLando.isContainerRunning)(lando, slug, 'database')])).every(Boolean);
82
+ if (!isUp && !opt.force) {
63
83
  await trackerFn('env_not_running_error', {
64
84
  errorMessage: 'Environment was not running'
65
85
  });
66
86
  throw new _userError.default('Environment needs to be started first');
67
87
  }
68
- const cmd = new _devEnvSyncSql.DevEnvSyncSQLCommand(app, env, slug, lando, trackerFn);
88
+ const cmd = new _devEnvSyncSql.DevEnvSyncSQLCommand(app, env, slug, lando, trackerFn, liveBackupCopyCLIOptions);
69
89
  // TODO: There's a function called handleCLIException for dev-env that handles exceptions but DevEnvSyncSQLCommand has its own implementation.
70
90
  // We should probably use handleCLIException instead?
71
91
  const didCommandRun = await cmd.run();
@@ -3,6 +3,7 @@
3
3
 
4
4
  var _exportSql = require("../commands/export-sql");
5
5
  var _command = _interopRequireDefault(require("../lib/cli/command"));
6
+ var _liveBackupCopy = require("../lib/live-backup-copy");
6
7
  var _tracker = require("../lib/tracker");
7
8
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
9
  const examples = [{
@@ -14,6 +15,21 @@ const examples = [{
14
15
  }, {
15
16
  usage: 'vip @example-app.develop export sql --generate-backup',
16
17
  description: 'Generate a fresh database backup for an environment and download a copy of that backup.'
18
+ }, {
19
+ usage: 'vip @example-app.develop export sql --table=wp_posts --table=wp_comments',
20
+ description: 'Generate a database backup including only the wp_posts and wp_comments tables, and download a copy of that backup.'
21
+ }, {
22
+ usage: 'vip @example-app.develop export sql --table=wp_posts,wp_comments',
23
+ description: 'Generate a database backup including only the wp_posts and wp_comments tables using comma-separated syntax, and download a copy of that backup.'
24
+ }, {
25
+ usage: 'vip @example-app.develop export sql --subsite-id=2 --subsite-id=3',
26
+ description: 'Generate a database backup including only the tables related to the subsites with IDs 2 and 3, and download a copy of that backup.'
27
+ }, {
28
+ usage: 'vip @example-app.develop export sql --subsite-id=2,3',
29
+ description: 'Generate a database backup including only the tables related to the subsites with IDs 2 and 3 using comma-separated syntax, and download a copy of that backup.'
30
+ }, {
31
+ usage: 'vip @example-app.develop export sql --config-file=~/db-export-config.json',
32
+ description: 'Generate a database backup using the specified config file, and download a copy of that backup.'
17
33
  }];
18
34
  const appQuery = `
19
35
  id,
@@ -36,10 +52,14 @@ const appQuery = `
36
52
  module: 'export-sql',
37
53
  requiredArgs: 0,
38
54
  usage: 'vip export sql'
39
- }).option('output', 'Download the file to a specific local directory path with a custom file name.').option('generate-backup', 'Generate a fresh database backup and export a copy of that backup.').examples(examples).argv(process.argv, async (arg, {
55
+ }).option('output', 'Download the file to a specific local directory path with a custom file name.').option('table', 'A table to export from the remote environment. Multiple tables can be specified with multiple --table flags or as a comma-separated list.').option('subsite-id', 'The ID of a subsite/network site to export from the remote environment. Multiple subsite IDs can be specified with multiple --subsite-id flags or as a comma-separated list.').option('wpcli-command', 'The WP-CLI command to run on the remote environment to retrieve the database export configuration.').option('config-file', 'The backup copy config file to use for the export.', undefined).option('generate-backup', 'Generate a fresh database backup and export a copy of that backup.').examples(examples).argv(process.argv, async (arg, {
40
56
  app,
41
57
  env,
42
58
  output,
59
+ configFile,
60
+ table,
61
+ subsiteId,
62
+ wpcliCommand,
43
63
  generateBackup
44
64
  }) => {
45
65
  const trackerFn = (0, _tracker.makeCommandTracker)('export_sql', {
@@ -48,9 +68,11 @@ const appQuery = `
48
68
  generate_backup: generateBackup
49
69
  });
50
70
  await trackerFn('execute');
71
+ const liveBackupCopyCLIOptions = (0, _liveBackupCopy.parseLiveBackupCopyCLIOptions)(configFile, table, subsiteId, wpcliCommand);
51
72
  const exportCommand = new _exportSql.ExportSQLCommand(app, env, {
52
73
  outputFile: output,
53
- generateBackup
74
+ generateBackup,
75
+ liveBackupCopyCLIOptions
54
76
  }, trackerFn);
55
77
  await exportCommand.run();
56
78
  await trackerFn('success');
@@ -10,6 +10,7 @@ var _socket2 = _interopRequireDefault(require("socket.io-stream"));
10
10
  var _stream = require("stream");
11
11
  var _wpSsh = require("../commands/wp-ssh");
12
12
  var _api = _interopRequireWildcard(require("../lib/api"));
13
+ var _app = require("../lib/app");
13
14
  var _command = _interopRequireWildcard(require("../lib/cli/command"));
14
15
  var exit = _interopRequireWildcard(require("../lib/cli/exit"));
15
16
  var _format = require("../lib/cli/format");
@@ -20,7 +21,7 @@ var _tracker = require("../lib/tracker");
20
21
  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); }
21
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
22
23
  const debug = (0, _debug.default)('@automattic/vip:wp');
23
- const appQuery = `id, name,
24
+ const appQuery = `id, name, typeId,
24
25
  organization {
25
26
  id
26
27
  name
@@ -317,6 +318,7 @@ const examples = [{
317
318
  const {
318
319
  id: appId,
319
320
  name: appName,
321
+ typeId: appTypeId,
320
322
  organization: {
321
323
  id: orgId
322
324
  }
@@ -325,6 +327,9 @@ const examples = [{
325
327
  id: envId,
326
328
  type: envName
327
329
  } = opts.env;
330
+ if ((0, _app.isAppNodejs)(appTypeId)) {
331
+ exit.withError('WP-CLI commands are not supported on Node.js environments.');
332
+ }
328
333
 
329
334
  /* eslint-disable camelcase */
330
335
  const commonTrackingParams = {
@@ -55,7 +55,8 @@ class DevEnvImportSQLCommand {
55
55
  } = this.options;
56
56
  const resolvedPath = await (0, _devEnvironmentCore.resolveImportPath)(this.slug, this.fileName, searchReplace, inPlace);
57
57
  if (!this.options.skipValidate) {
58
- if (!(await (0, _devEnvironmentLando.isEnvUp)(lando, (0, _devEnvironmentCore.getEnvironmentPath)(this.slug)))) {
58
+ const isUp = (await Promise.all([(0, _devEnvironmentLando.isContainerRunning)(lando, this.slug, 'php'), (0, _devEnvironmentLando.isContainerRunning)(lando, this.slug, 'database')])).every(Boolean);
59
+ if (!isUp) {
59
60
  throw new _userError.default('Environment needs to be started first');
60
61
  }
61
62
  const expectedDomain = `${this.slug}.${lando.config.domain}`;
@@ -87,30 +87,27 @@ class DevEnvSyncSQLCommand {
87
87
  tmpDir;
88
88
  siteUrls = [];
89
89
  searchReplaceMap = {};
90
+ liveBackupCopyCLIOptions;
90
91
  _track;
91
92
  _sqlDumpType;
92
93
 
93
94
  /**
94
95
  * Creates a new instance of the command
95
- *
96
- * @param app The app object
97
- * @param env The environment object
98
- * @param slug The site slug
99
- * @param lando The lando object
100
- * @param trackerFn Function to call for tracking
101
96
  */
102
- constructor(app, env, slug, lando, trackerFn = () => {}) {
97
+ constructor(app, env, slug, lando, trackerFn = () => {}, liveBackupCopyCLIOptions) {
103
98
  this.app = app;
104
99
  this.env = env;
105
100
  this.slug = slug;
106
101
  this.lando = lando;
107
102
  this._track = trackerFn;
108
103
  this.tmpDir = (0, _utils.makeTempDir)();
104
+ this.liveBackupCopyCLIOptions = liveBackupCopyCLIOptions;
109
105
  }
110
106
  track(name, eventProps) {
111
107
  return this._track(name, {
112
108
  ...eventProps,
113
- sqldump_type: this._sqlDumpType
109
+ sqldump_type: this._sqlDumpType,
110
+ live_backup_copy: this.liveBackupCopyCLIOptions?.useLiveBackupCopy
114
111
  });
115
112
  }
116
113
  get landoDomain() {
@@ -132,9 +129,9 @@ class DevEnvSyncSQLCommand {
132
129
  const dumpDetails = await (0, _database.getSqlDumpDetails)(this.sqlFile);
133
130
  this._sqlDumpType = dumpDetails.type;
134
131
  }
135
- async confirmEnoughStorage(job) {
136
- const storageAvailability = _backupStorageAvailability.BackupStorageAvailability.createFromDbCopyJob(job);
137
- return await storageAvailability.validateAndPromptDiskSpaceWarningForDevEnvBackupImport();
132
+ async confirmEnoughStorage(archiveSize) {
133
+ const storageAvailability = new _backupStorageAvailability.BackupStorageAvailability();
134
+ return await storageAvailability.validateAndPromptDiskSpaceWarningForDevEnvBackupImport(archiveSize);
138
135
  }
139
136
 
140
137
  /**
@@ -144,7 +141,8 @@ class DevEnvSyncSQLCommand {
144
141
  async generateExport() {
145
142
  const exportCommand = new _exportSql.ExportSQLCommand(this.app, this.env, {
146
143
  outputFile: this.gzFile,
147
- confirmEnoughStorageHook: this.confirmEnoughStorage.bind(this)
144
+ confirmEnoughStorageHook: this.confirmEnoughStorage.bind(this),
145
+ liveBackupCopyCLIOptions: this.liveBackupCopyCLIOptions
148
146
  }, this.track.bind(this));
149
147
  await exportCommand.run();
150
148
  }
@@ -5,7 +5,6 @@ exports.GENERATE_DOWNLOAD_LINK_MUTATION = exports.ExportSQLCommand = exports.CRE
5
5
  var _chalk = _interopRequireDefault(require("chalk"));
6
6
  var _fs = _interopRequireDefault(require("fs"));
7
7
  var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
8
- var _https = _interopRequireDefault(require("https"));
9
8
  var _path = _interopRequireDefault(require("path"));
10
9
  var _backupDb = require("./backup-db");
11
10
  var _api = _interopRequireWildcard(require("../lib/api"));
@@ -13,6 +12,9 @@ var _backupStorageAvailability = require("../lib/backup-storage-availability/bac
13
12
  var exit = _interopRequireWildcard(require("../lib/cli/exit"));
14
13
  var _format = require("../lib/cli/format");
15
14
  var _progress = require("../lib/cli/progress");
15
+ var _downloadFile = require("../lib/http/download-file");
16
+ var _liveBackupCopy = _interopRequireWildcard(require("../lib/live-backup-copy"));
17
+ var liveBackupCopy = _liveBackupCopy;
16
18
  var _retry = require("../lib/retry");
17
19
  var _utils = require("../lib/utils");
18
20
  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); }
@@ -189,6 +191,8 @@ class ExportSQLCommand {
189
191
  DOWNLOAD: 'download'
190
192
  };
191
193
  track;
194
+ liveBackupCopyCLIOptions;
195
+ showMyDumperWarning;
192
196
 
193
197
  /**
194
198
  * Creates an instance of SQLExportCommand
@@ -221,6 +225,8 @@ class ExportSQLCommand {
221
225
  name: 'Downloading file'
222
226
  }]);
223
227
  this.track = trackerFn;
228
+ this.liveBackupCopyCLIOptions = options.liveBackupCopyCLIOptions;
229
+ this.showMyDumperWarning = options.showMyDumperWarning === undefined ? true : options.showMyDumperWarning;
224
230
  }
225
231
 
226
232
  /**
@@ -258,39 +264,6 @@ class ExportSQLCommand {
258
264
  return metadata?.value?.split('/')[1];
259
265
  }
260
266
 
261
- /**
262
- * Downloads the exported file
263
- *
264
- * @param {string} url The download URL
265
- * @return {Promise} A promise which resolves to the path of the downloaded file
266
- * @throws {Error} Throws an error if the download fails
267
- */
268
- async downloadExportedFile(url) {
269
- const filename = this.outputFile || (await this.getExportedFileName()) || 'exported.sql.gz';
270
- const file = _fs.default.createWriteStream(filename);
271
- return new Promise((resolve, reject) => {
272
- _https.default.get(url, response => {
273
- response.pipe(file);
274
- const total = parseInt(response.headers['content-length'], 10);
275
- let current = 0;
276
- file.on('finish', () => {
277
- file.close();
278
- resolve(_path.default.resolve(file.path));
279
- });
280
- file.on('error', err => {
281
- // TODO: fs.unlink runs in the background so there's a chance that the app dies before it finishes.
282
- // This needs fixing.
283
- _fs.default.unlink(filename, () => null);
284
- reject(err);
285
- });
286
- response.on('data', chunk => {
287
- current += chunk.length;
288
- this.progressTracker.setProgress(`- ${(100 * current / total).toFixed(2)}% (${(0, _format.formatBytes)(current)}/${(0, _format.formatBytes)(total)})`);
289
- });
290
- });
291
- });
292
- }
293
-
294
267
  /**
295
268
  * Checks if the export job's preflight step is successful
296
269
  *
@@ -328,15 +301,12 @@ class ExportSQLCommand {
328
301
  console.log(noticeMessage);
329
302
  await cmd.run(false);
330
303
  }
331
- async confirmEnoughStorage(job) {
332
- if (!job) {
333
- throw new Error('confirmEnoughStorage: job is missing');
334
- }
304
+ async confirmEnoughStorage(archiveSize) {
335
305
  if (this.confirmEnoughStorageHook) {
336
- return await this.confirmEnoughStorageHook(job);
306
+ return await this.confirmEnoughStorageHook(archiveSize);
337
307
  }
338
- const storageAvailability = _backupStorageAvailability.BackupStorageAvailability.createFromDbCopyJob(job);
339
- return await storageAvailability.validateAndPromptDiskSpaceWarningForBackupImport();
308
+ const storageAvailability = new _backupStorageAvailability.BackupStorageAvailability();
309
+ return await storageAvailability.validateAndPromptDiskSpaceWarningForBackupImport(archiveSize);
340
310
  }
341
311
 
342
312
  /**
@@ -357,6 +327,66 @@ class ExportSQLCommand {
357
327
  exit.withError(`Cannot write to the specified path: ${error?.message}`);
358
328
  }
359
329
  }
330
+ const filename = this.outputFile || 'exported.sql.gz';
331
+ this.progressTracker.stepRunning(this.steps.PREPARE);
332
+ this.progressTracker.startPrinting();
333
+ let url;
334
+ let size;
335
+ if (this.liveBackupCopyCLIOptions?.useLiveBackupCopy) {
336
+ const result = await this.generateLiveBackupCopy();
337
+ url = result.url;
338
+ size = result.size;
339
+ } else {
340
+ url = await this.runBackup();
341
+ const exportJob = await this.getExportJob();
342
+ if (!exportJob) {
343
+ throw new Error('Export job not found');
344
+ }
345
+ const bytesWrittenMeta = exportJob.metadata?.find(meta => meta?.name === 'bytesWritten');
346
+ if (!bytesWrittenMeta?.value) {
347
+ throw new Error('Export job metadata does not contain bytesWritten');
348
+ }
349
+ size = Number(bytesWrittenMeta.value);
350
+ }
351
+ const storageConfirmed = await this.progressTracker.handleContinuePrompt(async setPromptShown => {
352
+ const status = await this.confirmEnoughStorage(size);
353
+ if (status.isPromptShown) {
354
+ setPromptShown();
355
+ }
356
+ return status.continue;
357
+ });
358
+ if (storageConfirmed) {
359
+ this.progressTracker.stepSuccess(this.steps.CONFIRM_ENOUGH_STORAGE);
360
+ } else {
361
+ this.progressTracker.stepFailed(this.steps.CONFIRM_ENOUGH_STORAGE);
362
+ this.stopProgressTracker();
363
+ exit.withError('Command canceled by user.');
364
+ }
365
+
366
+ // The export file is prepared. Let's download it
367
+ try {
368
+ const onProgressCallback = (current, total) => {
369
+ if (total) {
370
+ this.progressTracker.setProgress(`- ${(100 * current / total).toFixed(2)}% (${(0, _format.formatBytes)(current)}/${(0, _format.formatBytes)(total)})`);
371
+ }
372
+ };
373
+ await (0, _downloadFile.downloadFile)(url, filename, onProgressCallback);
374
+ this.progressTracker.stepSuccess(this.steps.DOWNLOAD);
375
+ this.stopProgressTracker();
376
+ console.log(`File saved to ${filename}`);
377
+ } catch (err) {
378
+ const error = err;
379
+ this.progressTracker.stepFailed(this.steps.DOWNLOAD);
380
+ this.stopProgressTracker();
381
+ await this.track('error', {
382
+ error_type: 'download_failed',
383
+ error_message: error.message,
384
+ stack: error.stack
385
+ });
386
+ exit.withError(`Error downloading exported file: ${error.message}`);
387
+ }
388
+ }
389
+ async runBackup() {
360
390
  if (this.generateBackup) {
361
391
  await this.runBackupJob();
362
392
  }
@@ -376,7 +406,7 @@ class ExportSQLCommand {
376
406
  } else {
377
407
  console.log(`${(0, _format.getGlyphForStatus)('success')} Backup created with timestamp ${latestBackup.createdAt}`);
378
408
  }
379
- const showMyDumperWarning = (latestBackup.sqlDumpTool ?? envSqlDumpTool) === 'mydumper';
409
+ const showMyDumperWarning = this.showMyDumperWarning && (latestBackup.sqlDumpTool ?? envSqlDumpTool) === 'mydumper';
380
410
  if (showMyDumperWarning) {
381
411
  console.warn(_chalk.default.yellow.bold('WARNING:'), _chalk.default.yellow('This is a large or complex database. The backup file for this database is generated with MyDumper. ' + 'The file can only be loaded with MyLoader. ' + 'For more information: https://github.com/mydumper/mydumper'));
382
412
  }
@@ -406,45 +436,84 @@ class ExportSQLCommand {
406
436
  exit.withError(`Error creating export job: ${error.message}`);
407
437
  }
408
438
  }
409
- this.progressTracker.stepRunning(this.steps.PREPARE);
410
- this.progressTracker.startPrinting();
411
439
  await (0, _utils.pollUntil)(this.getExportJob.bind(this), EXPORT_SQL_PROGRESS_POLL_INTERVAL, this.isPrepared.bind(this));
412
440
  this.progressTracker.stepSuccess(this.steps.PREPARE);
413
441
  await (0, _utils.pollUntil)(this.getExportJob.bind(this), EXPORT_SQL_PROGRESS_POLL_INTERVAL, this.isCreated.bind(this));
414
442
  this.progressTracker.stepSuccess(this.steps.CREATE);
415
- const storageConfirmed = await this.progressTracker.handleContinuePrompt(async setPromptShown => {
416
- const status = await this.confirmEnoughStorage(await this.getExportJob());
417
- if (status.isPromptShown) {
418
- setPromptShown();
419
- }
420
- return status.continue;
421
- });
422
- if (storageConfirmed) {
423
- this.progressTracker.stepSuccess(this.steps.CONFIRM_ENOUGH_STORAGE);
424
- } else {
425
- this.progressTracker.stepFailed(this.steps.CONFIRM_ENOUGH_STORAGE);
426
- this.stopProgressTracker();
427
- exit.withError('Command canceled by user.');
428
- }
429
443
  const url = await generateDownloadLink(this.app.id, this.env.id, latestBackup.id);
430
444
  this.progressTracker.stepSuccess(this.steps.DOWNLOAD_LINK);
431
-
432
- // The export file is prepared. Let's download it
445
+ return url;
446
+ }
447
+ async generateLiveBackupCopy() {
448
+ if (!this.liveBackupCopyCLIOptions) {
449
+ throw new Error('Configuration file is required for live backup copy.');
450
+ }
451
+ if (!this.app.id || !this.env.id) {
452
+ throw new Error('App ID and Environment ID are required to start live backup copy.');
453
+ }
454
+ const config = this.getLiveBackupConfigFromCLIOptions();
433
455
  try {
434
- const filepath = await this.downloadExportedFile(url);
435
- this.progressTracker.stepSuccess(this.steps.DOWNLOAD);
436
- this.stopProgressTracker();
437
- console.log(`File saved to ${filepath}`);
456
+ const copyId = await liveBackupCopy.startLiveBackupCopy({
457
+ appId: this.app.id,
458
+ environmentId: this.env.id,
459
+ config
460
+ });
461
+ this.progressTracker.stepSuccess(this.steps.PREPARE);
462
+ this.progressTracker.stepRunning(this.steps.CREATE);
463
+ const result = await liveBackupCopy.getDownloadURL({
464
+ appId: this.app.id,
465
+ environmentId: this.env.id,
466
+ copyId
467
+ });
468
+ this.progressTracker.stepSuccess(this.steps.CREATE);
469
+ this.progressTracker.stepSuccess(this.steps.DOWNLOAD_LINK);
470
+ return result;
438
471
  } catch (err) {
439
- const error = err;
440
- this.progressTracker.stepFailed(this.steps.DOWNLOAD);
441
- this.stopProgressTracker();
472
+ const message = err instanceof Error ? err.message : 'Unknown error';
442
473
  await this.track('error', {
443
- error_type: 'download_failed',
444
- error_message: error.message,
445
- stack: error.stack
474
+ error_type: 'live_backup_copy',
475
+ error_message: message,
476
+ stack: err instanceof Error ? err.stack : undefined
446
477
  });
447
- exit.withError(`Error downloading exported file: ${error.message}`);
478
+ exit.withError(`Error creating live backup copy: ${message}`);
479
+ }
480
+ }
481
+ getLiveBackupConfigFromCLIOptions() {
482
+ if (this.liveBackupCopyCLIOptions?.configFile) {
483
+ return this.loadLiveBackupCopyConfig(this.liveBackupCopyCLIOptions?.configFile);
484
+ }
485
+ let type = _liveBackupCopy.BackupLiveCopyType.TABLES;
486
+ let tables;
487
+ let subsiteIds;
488
+ if (this.liveBackupCopyCLIOptions?.tables) {
489
+ tables = Object.fromEntries(this.liveBackupCopyCLIOptions?.tables.map(key => [key, {}]));
490
+ }
491
+ if (this.liveBackupCopyCLIOptions?.subsiteIds) {
492
+ type = _liveBackupCopy.BackupLiveCopyType.SUBSITE_IDS;
493
+ subsiteIds = this.liveBackupCopyCLIOptions?.subsiteIds.map(id => Number(id));
494
+ }
495
+ if (this.liveBackupCopyCLIOptions?.wpcliCommand) {
496
+ type = _liveBackupCopy.BackupLiveCopyType.WP_CLI_COMMAND;
497
+ }
498
+ return {
499
+ type,
500
+ tables,
501
+ subsite_ids: subsiteIds,
502
+ wpcli_command: this.liveBackupCopyCLIOptions?.wpcliCommand
503
+ };
504
+ }
505
+ loadLiveBackupCopyConfig(configFile) {
506
+ if (!_fs.default.existsSync(configFile)) {
507
+ throw new Error(`Configuration file not found: ${configFile}`);
508
+ }
509
+ try {
510
+ return JSON.parse(_fs.default.readFileSync(configFile, 'utf-8'));
511
+ } catch (err) {
512
+ const errMessage = err instanceof Error ? err.message : 'Unknown error';
513
+ if (err instanceof SyntaxError) {
514
+ throw new Error(`Invalid JSON in configuration file: ${configFile} - ${errMessage}`);
515
+ }
516
+ throw new Error(`Error reading configuration file: ${configFile} - ${errMessage}`);
448
517
  }
449
518
  }
450
519
  }
@@ -13,16 +13,6 @@ var _format = require("../cli/format");
13
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
14
  const oneGiBInBytes = 1024 * 1024 * 1024;
15
15
  class BackupStorageAvailability {
16
- constructor(archiveSize) {
17
- this.archiveSize = archiveSize;
18
- }
19
- static createFromDbCopyJob(job) {
20
- const bytesWrittenMeta = job.metadata?.find(meta => meta?.name === 'bytesWritten');
21
- if (!bytesWrittenMeta?.value) {
22
- throw new Error('Meta not found');
23
- }
24
- return new BackupStorageAvailability(Number(bytesWrittenMeta.value));
25
- }
26
16
  getDockerStorageKiBRaw() {
27
17
  return (0, _shelljs.exec)(`docker run --rm alpine df -k`, {
28
18
  silent: true
@@ -48,31 +38,31 @@ class BackupStorageAvailability {
48
38
  getReserveSpace() {
49
39
  return oneGiBInBytes;
50
40
  }
51
- getSqlSize() {
41
+ getSqlSize(archiveSize) {
52
42
  // We estimated that it'd be about 3.5x the archive size.
53
- return this.archiveSize * 3.5;
43
+ return archiveSize * 3.5;
54
44
  }
55
- getArchiveSize() {
56
- return this.archiveSize;
45
+ getArchiveSize(archiveSize) {
46
+ return archiveSize;
57
47
  }
58
- getStorageRequiredInMainMachine() {
59
- return this.getArchiveSize() + this.getSqlSize() + this.getReserveSpace();
48
+ getStorageRequiredInMainMachine(archiveSize) {
49
+ return this.getArchiveSize(archiveSize) + this.getSqlSize(archiveSize) + this.getReserveSpace();
60
50
  }
61
- getStorageRequiredInDockerMachine() {
62
- return this.getSqlSize() + this.getReserveSpace();
51
+ getStorageRequiredInDockerMachine(archiveSize) {
52
+ return this.getSqlSize(archiveSize) + this.getReserveSpace();
63
53
  }
64
- async isStorageAvailableInMainMachine() {
65
- return (await this.getStorageAvailableInVipPath()) > this.getStorageRequiredInMainMachine();
54
+ async isStorageAvailableInMainMachine(archiveSize) {
55
+ return (await this.getStorageAvailableInVipPath()) > this.getStorageRequiredInMainMachine(archiveSize);
66
56
  }
67
- isStorageAvailableInDockerMachine() {
68
- return this.getDockerStorageAvailable() > this.getStorageRequiredInDockerMachine();
57
+ isStorageAvailableInDockerMachine(archiveSize) {
58
+ return this.getDockerStorageAvailable() > this.getStorageRequiredInDockerMachine(archiveSize);
69
59
  }
70
60
 
71
61
  // eslint-disable-next-line id-length
72
- async validateAndPromptDiskSpaceWarningForBackupImport() {
73
- const isStorageAvailable = (await this.getStorageAvailableInVipPath()) > this.getArchiveSize();
62
+ async validateAndPromptDiskSpaceWarningForBackupImport(archiveSize) {
63
+ const isStorageAvailable = (await this.getStorageAvailableInVipPath()) > this.getArchiveSize(archiveSize);
74
64
  if (!isStorageAvailable) {
75
- const storageRequired = this.getArchiveSize();
65
+ const storageRequired = this.getArchiveSize(archiveSize);
76
66
  const confirmPrompt = new _enquirer.Confirm({
77
67
  message: `We recommend that you have at least ${this.bytesToHuman(storageRequired)} of free space in your machine to download this database backup. Do you still want to continue with downloading the database backup?`
78
68
  });
@@ -88,13 +78,13 @@ class BackupStorageAvailability {
88
78
  }
89
79
 
90
80
  // eslint-disable-next-line id-length
91
- async validateAndPromptDiskSpaceWarningForDevEnvBackupImport() {
81
+ async validateAndPromptDiskSpaceWarningForDevEnvBackupImport(archiveSize) {
92
82
  let storageAvailableInMainMachinePrompted = false;
93
83
 
94
84
  // there's two prompts, so as long as one prompt is shown, we need to set isPromptShown
95
85
  let isPromptShown = false;
96
- if (!(await this.isStorageAvailableInMainMachine())) {
97
- const storageRequired = this.getStorageRequiredInMainMachine();
86
+ if (!(await this.isStorageAvailableInMainMachine(archiveSize))) {
87
+ const storageRequired = this.getStorageRequiredInMainMachine(archiveSize);
98
88
  const storageAvailableInVipPath = this.bytesToHuman(await this.getStorageAvailableInVipPath());
99
89
  const confirmPrompt = new _enquirer.Confirm({
100
90
  message: `We recommend that you have at least ${this.bytesToHuman(storageRequired)} of free space in your machine to import this database backup. We estimate that you currently have ${storageAvailableInVipPath} of space in your machine.
@@ -111,8 +101,8 @@ Do you still want to continue with importing the database backup?
111
101
  }
112
102
  }
113
103
  try {
114
- if (!this.isStorageAvailableInDockerMachine()) {
115
- const storageRequired = this.getStorageRequiredInDockerMachine();
104
+ if (!this.isStorageAvailableInDockerMachine(archiveSize)) {
105
+ const storageRequired = this.getStorageRequiredInDockerMachine(archiveSize);
116
106
  const storageAvailableInDockerMachine = this.bytesToHuman(this.getDockerStorageAvailable());
117
107
  const confirmPrompt = new _enquirer.Confirm({
118
108
  message: `We recommend that you have at least ${this.bytesToHuman(storageRequired)} of free space in your Docker machine to import this database backup. We estimate that you currently have ${storageAvailableInDockerMachine} of space in your machine.
@@ -4,6 +4,7 @@ exports.__esModule = true;
4
4
  exports.bootstrapLando = bootstrapLando;
5
5
  exports.checkEnvHealth = checkEnvHealth;
6
6
  exports.getProxyContainer = getProxyContainer;
7
+ exports.isContainerRunning = isContainerRunning;
7
8
  exports.isEnvUp = isEnvUp;
8
9
  exports.landoDestroy = landoDestroy;
9
10
  exports.landoExec = landoExec;
@@ -594,4 +595,18 @@ function validateDockerInstalled(lando) {
594
595
  throw new Error(`docker-compose version ${compose} is not supported. Please upgrade to version 2.0.0 or higher - https://docs.docker.com/compose/install/`);
595
596
  }
596
597
  }
598
+ }
599
+ async function isContainerRunning(lando, slug, serviceName) {
600
+ const envPath = (0, _devEnvironmentCore.getEnvironmentPath)(slug);
601
+ const app = await getLandoApplication(lando, envPath);
602
+ const {
603
+ docker
604
+ } = app.engine;
605
+ const containers = await docker.listContainers({
606
+ filters: {
607
+ label: [`com.docker.compose.project=${app.project}`, `com.docker.compose.service=${serviceName}`],
608
+ status: ['running']
609
+ }
610
+ });
611
+ return containers.length > 0;
597
612
  }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.downloadFile = void 0;
5
+ var fs = _interopRequireWildcard(require("fs"));
6
+ var _stream = require("stream");
7
+ var _promises = require("stream/promises");
8
+ 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
+ const downloadFile = async (fileUrl, destinationPath, onProgress) => {
10
+ let response;
11
+ try {
12
+ response = await fetch(fileUrl);
13
+ } catch (error) {
14
+ throw new Error(`Request to ${fileUrl} failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
15
+ }
16
+ if (!response.ok) {
17
+ const errorMessage = `Failed to download file. Status: ${response.status} ${response.statusText}`;
18
+ throw new Error(errorMessage);
19
+ }
20
+ if (!response.body) {
21
+ throw new Error('Failed to download file. The response body is empty.');
22
+ }
23
+ const totalSizeHeader = response.headers.get('content-length');
24
+ const totalBytes = totalSizeHeader ? parseInt(totalSizeHeader, 10) : null;
25
+ let downloadedBytes = 0;
26
+ const fileStream = fs.createWriteStream(destinationPath);
27
+ const nodeReadable = _stream.Readable.fromWeb(response.body);
28
+ nodeReadable.on('data', chunk => {
29
+ downloadedBytes += chunk.length;
30
+ if (onProgress) {
31
+ onProgress(downloadedBytes, totalBytes);
32
+ }
33
+ });
34
+ try {
35
+ nodeReadable.pipe(fileStream);
36
+ await (0, _promises.finished)(fileStream);
37
+ } catch (error) {
38
+ try {
39
+ await fs.promises.unlink(destinationPath);
40
+ } catch (unlinkErr) {
41
+ console.error(`Failed to delete partial file ${destinationPath}:`, unlinkErr);
42
+ }
43
+ throw new Error(`Failed to write file to disk: ${error instanceof Error ? error.message : 'Unknown error'}`);
44
+ }
45
+ };
46
+ exports.downloadFile = downloadFile;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.START_LIVE_COPY_MUTATION = exports.SQLDumpTool = exports.GENERATE_LIVE_BACKUP_DOWNLOAD_URL_MUTATION = exports.BackupLiveCopyType = void 0;
5
+ exports.getDownloadURL = getDownloadURL;
6
+ exports.parseLiveBackupCopyCLIOptions = parseLiveBackupCopyCLIOptions;
7
+ exports.startLiveBackupCopy = startLiveBackupCopy;
8
+ var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
9
+ var _userError = _interopRequireDefault(require("./user-error"));
10
+ var _utils = require("./utils");
11
+ var _api = _interopRequireDefault(require("../lib/api"));
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ function parseLiveBackupCopyCLIOptions(configFile, table, subsiteId, wpcliCommand) {
14
+ const options = {};
15
+ if (configFile && (table || subsiteId || wpcliCommand)) {
16
+ throw new _userError.default('The --config-file option cannot be used with the --table, --subsite-id, or --wpcli-command options. Please use only one of these options at a time.');
17
+ }
18
+ if (wpcliCommand && (table || subsiteId)) {
19
+ throw new _userError.default('The --wpcli-command option cannot be used with the --table or --subsite-id options. Please use only one of these options at a time.');
20
+ }
21
+ let useLiveBackupCopy = false;
22
+ if (table) {
23
+ if (Array.isArray(table)) {
24
+ options.tables = table.flatMap(tableItem => tableItem.split(',').map(name => name.trim()));
25
+ } else {
26
+ options.tables = table.split(',').map(name => name.trim());
27
+ }
28
+ useLiveBackupCopy = true;
29
+ }
30
+ if (subsiteId) {
31
+ if (Array.isArray(subsiteId)) {
32
+ options.subsiteIds = subsiteId.flatMap(id => String(id).split(',').map(idStr => Number(idStr.trim())));
33
+ } else {
34
+ options.subsiteIds = String(subsiteId).split(',').map(idStr => Number(idStr.trim()));
35
+ }
36
+ useLiveBackupCopy = true;
37
+ }
38
+ if (configFile) {
39
+ options.configFile = configFile;
40
+ useLiveBackupCopy = true;
41
+ }
42
+ if (wpcliCommand) {
43
+ options.wpcliCommand = wpcliCommand;
44
+ useLiveBackupCopy = true;
45
+ }
46
+ return {
47
+ ...options,
48
+ useLiveBackupCopy
49
+ };
50
+ }
51
+ const START_LIVE_COPY_MUTATION = exports.START_LIVE_COPY_MUTATION = (0, _graphqlTag.default)(`
52
+ mutation startLiveBackupCopy($input: LiveBackupCopyConfigInput!) {
53
+ startLiveBackupCopy(input: $input) {
54
+ message
55
+ copyId
56
+ }
57
+ }
58
+ `);
59
+
60
+ // eslint-disable-next-line id-length
61
+ const GENERATE_LIVE_BACKUP_DOWNLOAD_URL_MUTATION = exports.GENERATE_LIVE_BACKUP_DOWNLOAD_URL_MUTATION = (0, _graphqlTag.default)(`
62
+ mutation generateLiveBackupCopyDownloadURL(
63
+ $input: AppEnvironmentLiveBackupCopyDownloadURLInput!
64
+ ) {
65
+ generateLiveBackupCopyDownloadURL(input: $input) {
66
+ success
67
+ url
68
+ processing
69
+ size
70
+ }
71
+ }
72
+ `);
73
+ let BackupLiveCopyType = exports.BackupLiveCopyType = /*#__PURE__*/function (BackupLiveCopyType) {
74
+ BackupLiveCopyType["FULL"] = "full";
75
+ BackupLiveCopyType["TABLES"] = "tables";
76
+ BackupLiveCopyType["SUBSITE_IDS"] = "subsite_ids";
77
+ BackupLiveCopyType["WP_CLI_COMMAND"] = "wpcli_command";
78
+ return BackupLiveCopyType;
79
+ }({});
80
+ let SQLDumpTool = exports.SQLDumpTool = /*#__PURE__*/function (SQLDumpTool) {
81
+ SQLDumpTool["MYSQLDUMP"] = "mysqldump";
82
+ SQLDumpTool["MYDUMPER"] = "mydumper";
83
+ return SQLDumpTool;
84
+ }({});
85
+ async function startLiveBackupCopy({
86
+ appId,
87
+ environmentId,
88
+ config
89
+ }) {
90
+ const api = (0, _api.default)({
91
+ exitOnError: true
92
+ });
93
+ const result = await api.mutate({
94
+ mutation: START_LIVE_COPY_MUTATION,
95
+ variables: {
96
+ input: {
97
+ id: appId,
98
+ environmentId,
99
+ config
100
+ }
101
+ }
102
+ });
103
+
104
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
105
+ if (!result.data?.startLiveBackupCopy.copyId) {
106
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
107
+ throw new Error(`Failed to start live backup copy: ${result.data?.startLiveBackupCopy?.message ? result.data?.startLiveBackupCopy?.message : 'Unknown error'}`);
108
+ }
109
+ return result.data?.startLiveBackupCopy.copyId;
110
+ }
111
+ async function getDownloadURL({
112
+ appId,
113
+ environmentId,
114
+ copyId,
115
+ timeoutInSeconds = 2 * 60 * 60 // 2 hours default timeout
116
+ }) {
117
+ const api = (0, _api.default)({
118
+ exitOnError: true
119
+ });
120
+ try {
121
+ const result = await (0, _utils.pollUntil)(async () => {
122
+ return await api.mutate({
123
+ mutation: GENERATE_LIVE_BACKUP_DOWNLOAD_URL_MUTATION,
124
+ variables: {
125
+ input: {
126
+ id: appId,
127
+ environmentId,
128
+ copyId
129
+ }
130
+ }
131
+ });
132
+ }, 5000, fetchResult => {
133
+ return Boolean(fetchResult.data?.generateLiveBackupCopyDownloadURL?.url && !fetchResult.data?.generateLiveBackupCopyDownloadURL?.processing);
134
+ }, timeoutInSeconds * 1000);
135
+ if (!result.data?.generateLiveBackupCopyDownloadURL?.success || !result.data.generateLiveBackupCopyDownloadURL.url || !result.data.generateLiveBackupCopyDownloadURL.size) {
136
+ throw new Error(`Failed to generate download URL: ${result.data?.generateLiveBackupCopyDownloadURL?.url ? result.data?.generateLiveBackupCopyDownloadURL?.url : 'Unknown error'}`);
137
+ }
138
+ return {
139
+ url: result.data.generateLiveBackupCopyDownloadURL.url,
140
+ size: result.data.generateLiveBackupCopyDownloadURL.size
141
+ };
142
+ } catch (error) {
143
+ if (error instanceof _utils.PollingTimeoutError) {
144
+ throw new Error(`Failed to generate download URL: Polling timed out after ${timeoutInSeconds} seconds`);
145
+ }
146
+ const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
147
+ throw new Error(`Failed to generate download URL: ${errorMessage}`);
148
+ }
149
+ }
package/dist/lib/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
+ exports.PollingTimeoutError = void 0;
4
5
  exports.getAbsolutePath = getAbsolutePath;
5
6
  exports.makeTempDir = makeTempDir;
6
7
  exports.parseApiError = parseApiError;
@@ -12,27 +13,26 @@ var _os = _interopRequireDefault(require("os"));
12
13
  var _path = _interopRequireDefault(require("path"));
13
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
15
  const debug = (0, _debug.default)('@automattic/vip:lib:utils');
16
+ class PollingTimeoutError extends Error {}
15
17
 
16
18
  /**
17
19
  * Polls a function until its return value satisfies a condition
18
- *
19
- * @param {Function} fn A function to poll
20
- * @param {number} interval Poll interval in milliseconds
21
- * @param {Function} isDone A function that accepts the return of `fn`. Stops the polling if it returns true
22
- * @return {Promise} A promise which resolves when the polling is done
23
- * @throws {Error} If the fn throws an error
24
20
  */
25
- async function pollUntil(fn, interval, isDone) {
26
- let done = false;
27
- while (!done) {
21
+ exports.PollingTimeoutError = PollingTimeoutError;
22
+ async function pollUntil(fn, interval, isDone, timeoutMs = 6 * 60 * 60 * 1000 // Default to 6 hours
23
+ ) {
24
+ const startTime = Date.now();
25
+ while (Date.now() - startTime < timeoutMs) {
28
26
  // eslint-disable-next-line no-await-in-loop
29
27
  const result = await fn();
30
- done = isDone(result);
31
- if (!done) {
32
- // eslint-disable-next-line no-await-in-loop
33
- await (0, _promises.setTimeout)(interval);
28
+ if (isDone(result)) {
29
+ return result;
34
30
  }
31
+
32
+ // eslint-disable-next-line no-await-in-loop
33
+ await (0, _promises.setTimeout)(interval);
35
34
  }
35
+ throw new PollingTimeoutError('Polling timed out');
36
36
  }
37
37
 
38
38
  /**
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  ## Changelog
2
2
 
3
+ ### 3.19.2
4
+
5
+ * Improve environment checks when importing or syncing local databases to prevent common errors.
6
+ * Remove outdated options and clarify usage instructions for environment info commands.
7
+ * Prevent certain unsupported commands from running in incompatible environments.
8
+ * Upgrade internal components for better compatibility and future support.
9
+
10
+ * refactor(dev-env): deprecate `extended` option in `vip dev-env info`
11
+ * build(deps-dev): bump typescript from 5.8.3 to 5.9.2
12
+ * build(deps): bump @automattic/vip-search-replace from 1.1.3 to 2.0.0
13
+ * Do not allow WP-CLI commands to be run from Node Apps
14
+ * build(deps-dev): bump @types/node from 24.1.0 to 24.2.0
15
+ * refactor(dev-env): use `isContainerRunning` for import and sync commands
16
+
17
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.19.1...3.19.2
18
+
19
+ ### 3.19.1
20
+
21
+ * build(deps-dev): bump @types/node from 24.0.12 to 24.0.13
22
+ * build(deps): bump open from 10.1.2 to 10.2.0
23
+ * build(deps-dev): bump @types/node from 24.0.13 to 24.0.14
24
+ * build(deps): bump step-security/harden-runner from 2.12.2 to 2.13.0
25
+ * ci: add AI Changelog workflow
26
+ * build(deps-dev): bump @types/node from 24.0.14 to 24.0.15
27
+ * build(deps-dev): bump @types/node from 24.0.15 to 24.1.0
28
+ * build(deps-dev): bump the testing group with 3 updates
29
+ * security: update `form-data` to fix CVE-2025-7783
30
+ * fix(wp): resource leak during reconnection
31
+ * fix(wp): EventEmitter memory leak for `error` listeners
32
+ * refactor: remove deprecated `url.parse()` in favor of WHATWG URL API
33
+ * ci: simplify changelog generation workflow
34
+ * chore(deps): replace `socket.io-stream` with a fork
35
+ * fix: code smells detected by SonarCloud
36
+ * chore(deps): update Lando
37
+
38
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.19.0...3.19.1
39
+
3
40
  ### 3.19.0
4
41
 
5
42
  * build(deps-dev): bump the testing group across 1 directory with 4 updates
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.19.1",
3
+ "version": "3.19.3-dev.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@automattic/vip",
9
- "version": "3.19.1",
9
+ "version": "3.19.3-dev.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
13
  "@apollo/client": "3.3.6",
14
- "@automattic/vip-search-replace": "^1.1.1",
14
+ "@automattic/vip-search-replace": "^2.0.0",
15
15
  "@json2csv/plainjs": "^7.0.3",
16
16
  "@wwa/single-line-log": "^1.1.4",
17
17
  "args": "5.0.3",
@@ -253,22 +253,22 @@
253
253
  }
254
254
  },
255
255
  "node_modules/@automattic/vip-search-replace": {
256
- "version": "1.1.3",
257
- "resolved": "https://registry.npmjs.org/@automattic/vip-search-replace/-/vip-search-replace-1.1.3.tgz",
258
- "integrity": "sha512-UnP2cLfegI1sf/LHTemmvkZpC5rrmX29voZEw++DNH9B+nZEZ5BEP/skgvCyfLpag93ghENTRjtiXFy9E+w9Tg==",
256
+ "version": "2.0.0",
257
+ "resolved": "https://registry.npmjs.org/@automattic/vip-search-replace/-/vip-search-replace-2.0.0.tgz",
258
+ "integrity": "sha512-i1d3/KCK0sVgNjsJAV4UlMnC51AypLDfX8iEWTDLBiSR3wEvSWMSftF3f7yK46VdbBXB5zDvnVM48kFvASW9Ig==",
259
259
  "cpu": [
260
260
  "ia32",
261
261
  "x64",
262
262
  "arm64"
263
263
  ],
264
+ "license": "MIT",
264
265
  "os": [
265
266
  "darwin",
266
267
  "linux",
267
268
  "win32"
268
269
  ],
269
270
  "dependencies": {
270
- "debug": "^4.2.0",
271
- "follow-redirects": "^1.15.4"
271
+ "debug": "^4.2.0"
272
272
  }
273
273
  },
274
274
  "node_modules/@babel/cli": {
@@ -3631,12 +3631,12 @@
3631
3631
  "dev": true
3632
3632
  },
3633
3633
  "node_modules/@types/node": {
3634
- "version": "24.1.0",
3635
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
3636
- "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
3634
+ "version": "24.2.0",
3635
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
3636
+ "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
3637
3637
  "license": "MIT",
3638
3638
  "dependencies": {
3639
- "undici-types": "~7.8.0"
3639
+ "undici-types": "~7.10.0"
3640
3640
  }
3641
3641
  },
3642
3642
  "node_modules/@types/node-fetch": {
@@ -12663,9 +12663,9 @@
12663
12663
  }
12664
12664
  },
12665
12665
  "node_modules/typescript": {
12666
- "version": "5.8.3",
12667
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
12668
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
12666
+ "version": "5.9.2",
12667
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
12668
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
12669
12669
  "dev": true,
12670
12670
  "license": "Apache-2.0",
12671
12671
  "bin": {
@@ -12692,9 +12692,9 @@
12692
12692
  }
12693
12693
  },
12694
12694
  "node_modules/undici-types": {
12695
- "version": "7.8.0",
12696
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
12697
- "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
12695
+ "version": "7.10.0",
12696
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
12697
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
12698
12698
  "license": "MIT"
12699
12699
  },
12700
12700
  "node_modules/unicode-canonical-property-names-ecmascript": {
@@ -13669,12 +13669,11 @@
13669
13669
  }
13670
13670
  },
13671
13671
  "@automattic/vip-search-replace": {
13672
- "version": "1.1.3",
13673
- "resolved": "https://registry.npmjs.org/@automattic/vip-search-replace/-/vip-search-replace-1.1.3.tgz",
13674
- "integrity": "sha512-UnP2cLfegI1sf/LHTemmvkZpC5rrmX29voZEw++DNH9B+nZEZ5BEP/skgvCyfLpag93ghENTRjtiXFy9E+w9Tg==",
13672
+ "version": "2.0.0",
13673
+ "resolved": "https://registry.npmjs.org/@automattic/vip-search-replace/-/vip-search-replace-2.0.0.tgz",
13674
+ "integrity": "sha512-i1d3/KCK0sVgNjsJAV4UlMnC51AypLDfX8iEWTDLBiSR3wEvSWMSftF3f7yK46VdbBXB5zDvnVM48kFvASW9Ig==",
13675
13675
  "requires": {
13676
- "debug": "^4.2.0",
13677
- "follow-redirects": "^1.15.4"
13676
+ "debug": "^4.2.0"
13678
13677
  }
13679
13678
  },
13680
13679
  "@babel/cli": {
@@ -16051,11 +16050,11 @@
16051
16050
  "dev": true
16052
16051
  },
16053
16052
  "@types/node": {
16054
- "version": "24.1.0",
16055
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
16056
- "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
16053
+ "version": "24.2.0",
16054
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
16055
+ "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
16057
16056
  "requires": {
16058
- "undici-types": "~7.8.0"
16057
+ "undici-types": "~7.10.0"
16059
16058
  }
16060
16059
  },
16061
16060
  "@types/node-fetch": {
@@ -22365,9 +22364,9 @@
22365
22364
  }
22366
22365
  },
22367
22366
  "typescript": {
22368
- "version": "5.8.3",
22369
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
22370
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
22367
+ "version": "5.9.2",
22368
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
22369
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
22371
22370
  "dev": true
22372
22371
  },
22373
22372
  "unbox-primitive": {
@@ -22383,9 +22382,9 @@
22383
22382
  }
22384
22383
  },
22385
22384
  "undici-types": {
22386
- "version": "7.8.0",
22387
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
22388
- "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
22385
+ "version": "7.10.0",
22386
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
22387
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
22389
22388
  },
22390
22389
  "unicode-canonical-property-names-ecmascript": {
22391
22390
  "version": "2.0.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.19.1",
3
+ "version": "3.19.3-dev.0",
4
4
  "description": "The VIP Javascript library & CLI",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "dependencies": {
147
147
  "@apollo/client": "3.3.6",
148
- "@automattic/vip-search-replace": "^1.1.1",
148
+ "@automattic/vip-search-replace": "^2.0.0",
149
149
  "@json2csv/plainjs": "^7.0.3",
150
150
  "@wwa/single-line-log": "^1.1.4",
151
151
  "args": "5.0.3",