class-events.php 19.1 KB
Newer Older
1
2
3
4
5
<?php

namespace Automattic\WP\Cron_Control\CLI;

/**
6
 * Manage Cron Control's data
7
 */
8
class Events extends \WP_CLI_Command {
9
10
11
12
13
	/**
	 * List cron events
	 *
	 * Intentionally bypasses caching to ensure latest data is shown
	 *
14
	 * @subcommand list
15
	 * @synopsis [--status=<pending|completed>] [--page=<page>] [--limit=<limit>] [--format=<format>]
16
	 */
17
	public function list_events( $args, $assoc_args ) {
18
		$events = $this->get_events( $args, $assoc_args );
19

20
		// Prevent one from requesting a page that doesn't exist
21
22
		// Shouldn't error when first page is requested, though, as that is handled below and is an odd behaviour otherwise
		if ( $events['page'] > $events['total_pages'] && $events['page'] > 1 ) {
23
24
25
			\WP_CLI::error( __( 'Invalid page requested', 'automattic-cron-control' ) );
		}

26
27
28
29
30
31
32
33
34
35
		// Output in the requested format
		if ( isset( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			echo implode( ' ', wp_list_pluck( $events['items'], 'ID' ) );
		} else {
			// Lest someone think the `completed` record should be...complete
			if ( isset( $assoc_args['status'] ) && 'completed' === $assoc_args['status'] ) {
				\WP_CLI::warning( __( 'Entries are purged automatically, so this cannot be relied upon as a record of past event execution.', 'automattic-cron-control' ) );
			}

			// Not much to do
36
37
			if ( 0 === $events['total_items'] || empty( $events['items'] ) ) {
				\WP_CLI::warning( __( 'No events to display', 'automattic-cron-control' ) );
38
39
40
41
42
43
44
45
46
				return;
			}

			// Prepare events for display
			$events_for_display      = $this->format_events( $events['items'] );
			$total_events_to_display = count( $events_for_display );

			// Count, noting if showing fewer than all
			if ( $events['total_items'] <= $total_events_to_display ) {
47
				\WP_CLI::log( sprintf( _n( 'Displaying one entry', 'Displaying all %s entries', $total_events_to_display, 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ) ) );
48
			} else {
49
				\WP_CLI::log( sprintf( __( 'Displaying %1$s of %2$s entries, page %3$s of %4$s', 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ), number_format_i18n( $events['total_items'] ), number_format_i18n( $events['page'] ), number_format_i18n( $events['total_pages'] ) ) );
50
51
52
			}

			// And reformat
53
54
55
56
			$format = 'table';
			if ( isset( $assoc_args['format'] ) ) {
				$format = $assoc_args['format'];
			}
Erick Hitter's avatar
Erick Hitter committed
57

58
59
60
61
62
63
			\WP_CLI\Utils\format_items( $format, $events_for_display, array(
				'ID',
				'action',
				'instance',
				'next_run_gmt',
				'next_run_relative',
Erick Hitter's avatar
Erick Hitter committed
64
				'last_updated_gmt',
65
				'recurrence',
66
				'internal_event',
67
				'schedule_name',
68
69
70
				'event_args',
			) );
		}
71
72
	}

73
	/**
74
	 * Remove events
75
76
	 *
	 * @subcommand delete
77
	 * @synopsis [--event_id=<event_id>] [--action=<action>] [--completed]
78
79
80
81
82
	 */
	public function delete_events( $args, $assoc_args ) {
		// Remove a specific event
		if ( isset( $assoc_args['event_id'] ) ) {
			$this->delete_event_by_id( $args, $assoc_args );
83
			return;
84
85
86
87
		}

		// Remove all events with a given action
		if ( isset( $assoc_args['action'] ) ) {
Erick Hitter's avatar
Typo    
Erick Hitter committed
88
			$this->delete_event_by_action( $args, $assoc_args );
89
			return;
90
91
		}

92
93
		// Remove all completed events
		if ( isset( $assoc_args['completed'] ) ) {
94
			$this->delete_completed_events( $args, $assoc_args );
95
96
97
			return;
		}

98
99
100
		\WP_CLI::error( __( 'Specify something to delete, or see the `cron-control-fixers` command to remove all data.', 'automattic-cron-control' ) );
	}

101
102
103
104
	/**
	 * Run an event given an ID
	 *
	 * @subcommand run
105
	 * @synopsis <event_id>
106
107
	 */
	public function run_event( $args, $assoc_args ) {
108
109
110
		// Validate ID
		if ( ! is_numeric( $args[0] ) ) {
			\WP_CLI::error( __( 'Specify the ID of an event to run', 'automattic-cron-control' ) );
111
112
		}

113
114
		// Retrieve information needed to execute event
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $args[0] );
115

Erick Hitter's avatar
Erick Hitter committed
116
		if ( ! is_object( $event ) ) {
117
			\WP_CLI::error( sprintf( __( 'Failed to locate event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $args[0] ) );
118
119
		}

120
		\WP_CLI::log( sprintf( __( 'Found event %1$d with action `%2$s` and instance identifier `%3$s`', 'automattic-cron-control' ), $args[0], $event->action, $event->instance ) );
121
122

		// Proceed?
123
		$now = time();
Erick Hitter's avatar
Erick Hitter committed
124
125
		if ( $event->timestamp > $now ) {
			\WP_CLI::warning( sprintf( __( 'This event is not scheduled to run until %1$s GMT (%2$s)', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ), $this->calculate_interval( $event->timestamp - $now ) ) );
126
127
		}

128
		\WP_CLI::confirm( sprintf( __( 'Run this event?', 'automattic-cron-control' ) ) );
129
130

		// Environment preparation
131
		\Automattic\WP\Cron_Control\set_doing_cron();
132
133

		// Run the event
Erick Hitter's avatar
Erick Hitter committed
134
		$run = \Automattic\WP\Cron_Control\run_event( $event->timestamp, $event->action_hashed, $event->instance, true );
135
136
137
138
139
140
141
142
143
144
145

		// Output based on run attempt
		if ( is_array( $run ) ) {
			\WP_CLI::success( $run['message'] );
		} elseif ( is_wp_error( $run ) ) {
			\WP_CLI::error( $run->get_error_message() );
		} else {
			\WP_CLI::error( __( 'Failed to run event', 'automattic-cron-control' ) );
		}
	}

146
147
148
149
	/**
	 * Retrieve list of events, and related data, for a given request
	 */
	private function get_events( $args, $assoc_args ) {
150
		// Accept a status argument, with a default
151
152
153
154
155
		$status = 'pending';
		if ( isset( $assoc_args['status'] ) ) {
			$status = $assoc_args['status'];
		}

156
		// Convert to status used by Event Store
157
		$event_status = null;
158
159
		switch ( $status ) {
			case 'pending' :
160
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING;
161
162
				break;

163
164
165
166
			case 'running' :
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_RUNNING;
				break;

167
			case 'completed' :
168
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED;
169
170
171
				break;
		}

172
173
174
175
176
177
		if ( is_null( $event_status ) ) {
			\WP_CLI::error( __( 'Invalid status specified', 'automattic-cron-control' ) );
		}

		unset( $status );

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
		// Total to show
		$limit = 25;
		if ( isset( $assoc_args['limit'] ) && is_numeric( $assoc_args['limit'] ) ) {
			$limit = max( 1, min( absint( $assoc_args['limit'] ), 500 ) );
		}

		// Pagination
		$page = 1;
		if ( isset( $assoc_args['page'] ) && is_numeric( $assoc_args['page'] ) ) {
			$page = absint( $assoc_args['page'] );
		}

		$offset = absint( ( $page - 1 ) * $limit );

		// Query
Erick Hitter's avatar
Erick Hitter committed
193
		$items = \Automattic\WP\Cron_Control\get_events( array(
194
195
196
197
			'status'   => $event_status,
			'quantity' => $limit,
			'page'     => $page,
		) );
198
199
200
201
202
203

		// Bail if we don't get results
		if ( ! is_array( $items ) ) {
			\WP_CLI::error( __( 'Problem retrieving events', 'automattic-cron-control' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
204
		// Include totals for pagination etc
205
		$total_items = \Automattic\WP\Cron_Control\count_events_by_status( $event_status );
Erick Hitter's avatar
Erick Hitter committed
206
		$total_pages = ceil( $total_items / $limit );
207

Erick Hitter's avatar
Erick Hitter committed
208
		return compact( 'status', 'limit', 'page', 'offset', 'items', 'total_items', 'total_pages' );
209
210
211
	}

	/**
212
	 * Format event data into something human-readable
213
	 */
214
215
216
217
218
219
	private function format_events( $events ) {
		$formatted_events = array();

		// Reformat events
		foreach ( $events as $event ) {
			$row = array(
Erick Hitter's avatar
Erick Hitter committed
220
				'ID'                => $event->ID,
221
222
223
				'action'            => $event->action,
				'instance'          => $event->instance,
				'next_run_gmt'      => date( TIME_FORMAT, $event->timestamp ),
224
				'next_run_relative' => '',
225
				'last_updated_gmt'  => date( TIME_FORMAT, strtotime( $event->last_modified ) ),
226
				'recurrence'        => __( 'Non-repeating', 'automattic-cron-control' ),
227
				'internal_event'    => '',
228
				'schedule_name'     => __( 'n/a', 'automattic-cron-control' ),
229
230
231
				'event_args'        => '',
			);

232
233
			if ( $event->status === \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING ) {
				$row['next_run_relative'] = $this->calculate_interval( $event->timestamp - time() );
234
235
			}

236
237
238
239
			$row['internal_event'] = \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ? __( 'true', 'automattic-cron-control' ) : '';

			$row['event_args'] = maybe_serialize( $event->args );

240
241
242
243
244
			if ( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED === $event->status ) {
				$instance = md5( $row['event_args'] );
				$row['instance'] = "{$instance} - {$row['instance']}";
			}

245
246
247
248
249
250
			if ( isset( $event->interval ) && $event->interval ) {
				$row['recurrence'] = $this->calculate_interval( $event->interval );
			}

			if ( isset( $event->schedule ) && $event->schedule ) {
				$row['schedule_name'] = $event->schedule;
Erick Hitter's avatar
Erick Hitter committed
251
252
			}

253
254
255
			$formatted_events[] = $row;
		}

256
257
258
259
260
		// Sort results
		if ( ! empty( $formatted_events ) ) {
			usort( $formatted_events, array( $this, 'sort_events' ) );
		}

261
262
		return $formatted_events;
	}
Erick Hitter's avatar
Erick Hitter committed
263

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
	/**
	 * Sort events by timestamp, then action name
	 */
	private function sort_events( $first, $second ) {
		// Timestamp is usually sufficient
		if ( isset( $first['next_run_gmt'] ) ) {
			$first_timestamp = strtotime( $first['next_run_gmt'] );
			$second_timestamp = strtotime( $second['next_run_gmt'] );
		} elseif ( isset( $first['timestamp'] ) ) {
			$first_timestamp = $first['timestamp'];
			$second_timestamp = $second['timestamp'];
		} else {
			return 0;
		}

279
280
		if ( $first_timestamp !== $second_timestamp ) {
			return $first_timestamp - $second_timestamp;
281
282
283
		}

		// If timestamps are equal, consider action
284
		return strnatcasecmp( $first['action'], $second['action'] );
285
286
	}

Erick Hitter's avatar
Erick Hitter committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
	/**
	 * Convert a time interval into human-readable format.
	 *
	 * Similar to WordPress' built-in `human_time_diff()` but returns two time period chunks instead of just one.
	 *
	 * Borrowed from WP-CLI
	 *
	 * @param int $since An interval of time in seconds
	 * @return string The interval in human readable format
	 */
	private function calculate_interval( $since ) {
		if ( $since <= 0 ) {
			return 'now';
		}

		$since = absint( $since );

		// array of time period chunks
		$chunks = array(
			array( 60 * 60 * 24 * 365 , \_n_noop( '%s year', '%s years' ) ),
			array( 60 * 60 * 24 * 30 , \_n_noop( '%s month', '%s months' ) ),
			array( 60 * 60 * 24 * 7, \_n_noop( '%s week', '%s weeks' ) ),
			array( 60 * 60 * 24 , \_n_noop( '%s day', '%s days' ) ),
			array( 60 * 60 , \_n_noop( '%s hour', '%s hours' ) ),
			array( 60 , \_n_noop( '%s minute', '%s minutes' ) ),
			array(  1 , \_n_noop( '%s second', '%s seconds' ) ),
		);

		// we only want to output two chunks of time here, eg:
		// x years, xx months
		// x days, xx hours
		// so there's only two bits of calculation below:

		// step one: the first chunk
		for ( $i = 0, $j = count( $chunks ); $i < $j; $i++ ) {
			$seconds = $chunks[$i][0];
			$name = $chunks[$i][1];

			// finding the biggest chunk (if the chunk fits, break)
			if ( ( $count = floor( $since / $seconds ) ) != 0 ){
				break;
			}
		}

		// set output var
		$output = sprintf( \_n( $name[0], $name[1], $count ), $count );

		// step two: the second chunk
		if ( $i + 1 < $j ) {
			$seconds2 = $chunks[$i + 1][0];
			$name2    = $chunks[$i + 1][1];

			if ( ( $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 ) ) != 0 ) {
				// add to output var
				$output .= ' ' . sprintf( \_n( $name2[0], $name2[1], $count2 ), $count2 );
			}
		}

		return $output;
	}
347
348
349
350
351
352
353
354
355
356
357
358

	/**
	 * Delete an event by ID
	 */
	private function delete_event_by_id( $args, $assoc_args ) {
		$jid = absint( $assoc_args['event_id'] );

		// Validate ID
		if ( ! $jid ) {
			\WP_CLI::error( __( 'Invalid event ID', 'automattic-cron-control' ) );
		}

359
		\WP_CLI::log( __( 'Locating event...', 'automattic-cron-control' ) . "\n" );
360

361
		// Look up full event object
362
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $jid );
363

Erick Hitter's avatar
Erick Hitter committed
364
		if ( is_object( $event ) ) {
365
			// Warning about Internal Events
Erick Hitter's avatar
Erick Hitter committed
366
			if ( \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ) {
367
368
369
				\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
			}

370
371
372
373
			\WP_CLI::log( sprintf( __( 'Execution time: %s GMT', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ) ) );
			\WP_CLI::log( sprintf( __( 'Action: %s', 'automattic-cron-control' ), $event->action ) );
			\WP_CLI::log( sprintf( __( 'Instance identifier: %s', 'automattic-cron-control' ), $event->instance ) );
			\WP_CLI::log( '' );
374
375
			\WP_CLI::confirm( sprintf( __( 'Are you sure you want to delete this event?', 'automattic-cron-control' ) ) );

Erick Hitter's avatar
Erick Hitter committed
376
			// Try to delete the item and provide some relevant output
Erick Hitter's avatar
Erick Hitter committed
377
378
379
			\Automattic\WP\Cron_Control\_suspend_event_creation();
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event->ID, true );
			\Automattic\WP\Cron_Control\_resume_event_creation();
380

Erick Hitter's avatar
Erick Hitter committed
381
			if ( false === $deleted ) {
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
				\WP_CLI::error( sprintf( __( 'Failed to delete event %d', 'automattic-cron-control' ), $jid ) );
			} else {
				\Automattic\WP\Cron_Control\_flush_internal_caches();
				\WP_CLI::success( sprintf( __( 'Removed event %d', 'automattic-cron-control' ), $jid ) );
				return;
			}
		}

		\WP_CLI::error( sprintf( __( 'Failed to delete event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $jid ) );
	}

	/**
	 * Delete all events of the same action
	 */
	private function delete_event_by_action( $args, $assoc_args ) {
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
		$action = $assoc_args['action'];

		// Validate entry
		if ( empty( $action ) ) {
			\WP_CLI::error( __( 'Invalid action', 'automattic-cron-control' ) );
		}

		// Warning about Internal Events
		if ( \Automattic\WP\Cron_Control\is_internal_event( $action ) ) {
			\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
		}

		// Set defaults needed to gather all events
		$assoc_args['page']  = 1;
		$assoc_args['limit'] = 50;

		// Gather events
414
		\WP_CLI::log( __( 'Gathering events...', 'automattic-cron-control' ) );
415
416
417
418
419

		$events_to_delete = array();

		$events = $this->get_events( $args, $assoc_args );

420
		\WP_CLI::log( sprintf( _n( 'Found one event to check', 'Found %s events to check', $events['total_items'], 'automattic-cron-control' ), number_format_i18n( $events['total_items'] ) ) );
421
422
423
424
425
426
427
428
429
430
431

		$search_progress = \WP_CLI\Utils\make_progress_bar( sprintf( __( 'Searching events for those with the action `%s`', 'automattic-cron-control' ), $action ), $events['total_items'] );

		// Loop and pull out events to be deleted
		do {
			if ( ! is_array( $events ) || empty( $events['items'] ) ) {
				break;
			}

			// Check events for those that should be deleted
			foreach ( $events['items'] as $single_event ) {
Erick Hitter's avatar
Erick Hitter committed
432
433
				if ( $single_event->action === $action ) {
					$events_to_delete[] = (array) $single_event;
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
				}

				$search_progress->tick();
			}

			// Proceed to next batch
			$assoc_args['page']++;

			if ( $assoc_args['page'] > $events['total_pages'] ) {
				break;
			}

			$events = $this->get_events( $args, $assoc_args );
		} while( $events['page'] <= $events['total_pages'] );

		$search_progress->finish();

451
		\WP_CLI::log( '' );
452
453
454
455
456
457
458
459
460

		// Nothing more to do
		if ( empty( $events_to_delete ) ) {
			\WP_CLI::error( sprintf( __( 'No events with action `%s` found', 'automattic-cron-control' ), $action ) );
		}

		// List the items to remove
		$total_to_delete = count( $events_to_delete );

461
		\WP_CLI::log( sprintf( _n( 'Found one event with action `%2$s`:', 'Found %1$s events with action `%2$s`:', $total_to_delete, 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), $action ) );
462
463

		if ( $total_to_delete <= $assoc_args['limit'] ) {
464
465
466
467
468
469
			// Sort results
			if ( ! empty( $events_to_delete ) ) {
				usort( $events_to_delete, array( $this, 'sort_events' ) );
			}


470
471
			\WP_CLI\Utils\format_items( 'table', $events_to_delete, array(
				'ID',
Erick Hitter's avatar
Erick Hitter committed
472
473
				'created',
				'last_modified',
474
475
476
477
478
479
480
				'timestamp',
				'instance',
			) );
		} else {
			\WP_CLI::warning( sprintf( __( 'Events are not displayed as there are more than %s to remove', 'automattic-cron-control' ), number_format_i18n( $assoc_args['limit'] ) ) );
		}

481
		\WP_CLI::log( '' );
482
483
484
485
486
487
488
489
		\WP_CLI::confirm( _n( 'Are you sure you want to delete this event?', 'Are you sure you want to delete these events?', $total_to_delete, 'automattic-cron-control' ) );

		// Remove the items
		$delete_progress = \WP_CLI\Utils\make_progress_bar( __( 'Deleting events', 'automattic-cron-control' ), $total_to_delete );

		$events_deleted       = array();
		$events_deleted_count = $events_failed_delete = 0;

490
		// Don't create new events while deleting events
Erick Hitter's avatar
Erick Hitter committed
491
		\Automattic\WP\Cron_Control\_suspend_event_creation();
492

493
		foreach ( $events_to_delete as $event_to_delete ) {
Erick Hitter's avatar
Erick Hitter committed
494
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event_to_delete['ID'], false );
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516

			$events_deleted[] = array(
				'ID'      => $event_to_delete['ID'],
				'deleted' => false === $deleted ? 'no' : 'yes',
			);

			if ( $deleted ) {
				$events_deleted_count++;
			} else {
				$events_failed_delete++;
			}

			$delete_progress->tick();
		}

		$delete_progress->finish();

		// When deletes succeed, sync internal caches
		if ( $events_deleted_count > 0 ) {
			\Automattic\WP\Cron_Control\_flush_internal_caches();
		}

517
		// New events can be created now that removal is complete
Erick Hitter's avatar
Erick Hitter committed
518
		\Automattic\WP\Cron_Control\_resume_event_creation();
519

520
		// List the removed items
521
		\WP_CLI::log( "\n" . __( 'RESULTS:', 'automattic-cron-control' ) );
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542

		if ( 1 === $total_to_delete && 1 === $events_deleted_count ) {
			\WP_CLI::success( sprintf( __( 'Deleted one event: %d', 'automattic-cron-control' ), $events_deleted[0]['ID'] ) );
		} else {
			if ( $events_deleted_count === $total_to_delete ) {
				\WP_CLI::success( sprintf( __( 'Deleted %s events', 'automattic-cron-control' ), number_format_i18n( $events_deleted_count ) ) );
			} else {
				\WP_CLI::warning( sprintf( __( 'Expected to delete %1$s events, but could only delete %2$s events. It\'s likely that some events were executed while this command ran.', 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), number_format_i18n( $events_deleted_count ) ) );
			}

			// Limit just to failed deletes when many events are removed
			if ( count( $events_deleted ) > $assoc_args['limit'] ) {
				$events_deleted = array_filter( $events_deleted, function( $event ) {
					if ( 'no' === $event['deleted'] ) {
						return $event;
					} else {
						return false;
					}
				} );

				if ( count( $events_deleted ) > 0 ) {
543
					\WP_CLI::log( "\n" . __( 'Events that couldn\'t be deleted:', 'automattic-cron-control' ) );
544
545
				}
			} else {
546
				\WP_CLI::log( "\n" . __( 'Events deleted:', 'automattic-cron-control' ) );
547
548
549
550
551
552
553
554
555
556
			}

			// Don't display a table if there's nothing to display
			if ( count( $events_deleted ) > 0 ) {
				\WP_CLI\Utils\format_items( 'table', $events_deleted, array(
					'ID',
					'deleted',
				) );
			}
		}
557
558
559

		return;
	}
560
561
562
563
564
565
566
567
568
569
570
571
572

	/**
	 * Delete all completed events
	 */
	private function delete_completed_events( $args, $assoc_args ) {
		$count = \Automattic\WP\Cron_Control\count_events_by_status( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED );

		\WP_CLI::confirm( sprintf( _n( 'Found one completed event to remove. Continue?', 'Found %s completed events to remove. Continue?', $count, 'automattic-cron-control' ), number_format_i18n( $count ) ) );

		\Automattic\WP\Cron_Control\Events_Store::instance()->purge_completed_events( false );

		\WP_CLI::success( __( 'Entries removed', 'automattic-cron-control' ) );
	}
573
574
}

575
\WP_CLI::add_command( 'cron-control events', 'Automattic\WP\Cron_Control\CLI\Events' );