* : The 8-character code from your Hypamoon project (Sites) or from your AI assistant. * * ## EXAMPLES * * wp hypamoon connect ABCD-EF23 * * @param array $args Positional arguments. */ public function connect( $args ) { $result = Hypamoon_Connect_Service::connect( $args[0] ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } WP_CLI::success( sprintf( 'Connected to project "%s" (%s). Access until %s.', $result['project']['name'], $result['project']['mode'], $result['expires_at'] ) ); if ( ! empty( $result['inventory_error'] ) ) { WP_CLI::warning( 'The inventory could not be sent: ' . $result['inventory_error'] . ' Run: wp hypamoon inventory' ); } else { WP_CLI::log( 'Inventory sent (counts only, no content). Choose what moves in your project or chat, then run: wp hypamoon export' ); } } /** * Show the connection, as stored here and as known by Hypamoon. * * @subcommand status */ public function status() { if ( ! Hypamoon_Connect_Store::is_connected() ) { WP_CLI::log( 'Not connected. Run: wp hypamoon connect ' ); return; } $remote = Hypamoon_Connect_Service::remote_status(); if ( is_wp_error( $remote ) ) { WP_CLI::error( $remote->get_error_message() ); } $last = isset( $remote['last_export'] ) && is_array( $remote['last_export'] ) ? $remote['last_export'] : null; $rows = array( array( 'field' => 'project', 'value' => isset( $remote['project']['name'] ) ? $remote['project']['name'] : '', ), array( 'field' => 'mode', 'value' => isset( $remote['project']['mode'] ) ? $remote['project']['mode'] : '', ), array( 'field' => 'site', 'value' => isset( $remote['site_url'] ) ? $remote['site_url'] : '', ), array( 'field' => 'expires_at', 'value' => isset( $remote['expires_at'] ) ? $remote['expires_at'] : '', ), array( 'field' => 'inventory_at', 'value' => isset( $remote['inventory_at'] ) ? (string) $remote['inventory_at'] : '', ), array( 'field' => 'last_export', 'value' => $last ? sprintf( '%s, %d records, started %s', $last['status'], $last['records'], $last['started_at'] ) : 'none', ), ); $local = Hypamoon_Connect_Exporter::state(); if ( ! empty( $local['export_id'] ) ) { $rows[] = array( 'field' => 'local_round', 'value' => sprintf( '%s, %d processed, %d sent', $local['status'], $local['processed'], $local['sent'] ), ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'field', 'value' ) ); } /** * Send the inventory (counts and metadata only, never content). * * @subcommand inventory */ public function inventory() { $this->require_connection(); $result = Hypamoon_Connect_Service::send_inventory(); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } $rows = array(); foreach ( Hypamoon_Connect_Service::stored_inventory() as $item ) { $rows[] = array( 'key' => $item['key'], 'label' => $item['label'], 'count' => $item['count'], 'oldest' => (string) $item['oldest'], 'newest' => (string) $item['newest'], 'personal' => ! empty( $item['personal_data'] ) ? 'yes' : 'no', ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'key', 'label', 'count', 'oldest', 'newest', 'personal' ) ); WP_CLI::success( 'Inventory sent.' ); } /** * Show the current selection (pulled from Hypamoon). * * @subcommand selection */ public function selection() { $this->require_connection(); $selection = Hypamoon_Connect_Service::pull_selection(); if ( is_wp_error( $selection ) ) { WP_CLI::error( $selection->get_error_message() ); } WP_CLI::log( sprintf( 'Version %d, chosen via %s at %s.', isset( $selection['version'] ) ? $selection['version'] : 0, isset( $selection['updated_via'] ) ? $selection['updated_via'] : '-', isset( $selection['updated_at'] ) ? $selection['updated_at'] : '-' ) ); $this->print_plan( $selection, false ); } /** * Send the selected data in batches. * * Without --include the selection made in your project, chat or wp-admin is used. * * ## OPTIONS * * [--include=] * : Comma separated keys or groups to send, replacing the selection. Groups: content, media, menus, * terms, acf, forms, products, orders, customers, coupons, shipping, tax, reviews, subscriptions, * woocommerce, tables. Keys: for example content:post, form_entries:wpforms:12 or table:hours. Personal * data must be named explicitly (forms, orders, customers, subscriptions, reviews, users, tables). * Settings (options) move only with names chosen in your project, chat or wp-admin. * * [--users] * : Also send users (name, email and role only). * * [--use-selection] * : Use the selection as stored on Hypamoon (the default when --include is not given). * * [--dry-run] * : Only print what would be sent, with counts. * * [--restart] * : Start a new round even if an earlier round did not finish. * * ## EXAMPLES * * wp hypamoon export --dry-run * wp hypamoon export * wp hypamoon export --include=content,media,acf * wp hypamoon export --include=orders,customers --users * * @param array $args Positional arguments. * @param array $assoc_args Options. */ public function export( $args, $assoc_args ) { $this->require_connection(); $dry_run = ! empty( $assoc_args['dry-run'] ); $include = isset( $assoc_args['include'] ) ? (string) $assoc_args['include'] : ''; $users = ! empty( $assoc_args['users'] ); $selection = Hypamoon_Connect_Store::get( Hypamoon_Connect_Store::SELECTION ); if ( ! $dry_run || empty( $selection ) ) { $pulled = Hypamoon_Connect_Service::pull_selection(); if ( is_wp_error( $pulled ) && ! $dry_run ) { WP_CLI::error( $pulled->get_error_message() ); } if ( ! is_wp_error( $pulled ) ) { $selection = $pulled; } } if ( '' !== $include || $users ) { $wanted = array(); foreach ( array_filter( array_map( 'trim', explode( ',', $include ) ) ) as $group ) { $keys = Hypamoon_Connect_Registry::keys_for( $group ); if ( empty( $keys ) ) { WP_CLI::warning( sprintf( 'Nothing found for "%s" on this site.', $group ) ); } $wanted = array_merge( $wanted, $keys ); } if ( $users ) { $wanted[] = 'users'; } $items = array(); foreach ( Hypamoon_Connect_Registry::all() as $key => $source ) { $rule = Hypamoon_Connect_Service::rule_for( $selection, $source ); $rule['enabled'] = in_array( $key, $wanted, true ); $items[ $key ] = $rule; } $selection['items'] = $items; if ( ! $dry_run ) { $saved = Hypamoon_Connect_Service::save_selection( $items, isset( $selection['version'] ) ? (int) $selection['version'] : 0 ); if ( is_wp_error( $saved ) ) { WP_CLI::error( $saved->get_error_message() ); } $selection = $saved; } } if ( $dry_run ) { $this->print_plan( $selection, true ); return; } if ( ! empty( $assoc_args['restart'] ) || ! Hypamoon_Connect_Exporter::has_unfinished() ) { $this->print_plan( $selection, false ); $summary = Hypamoon_Connect_Exporter::start(); if ( is_wp_error( $summary ) ) { WP_CLI::error( $summary->get_error_message() ); } } else { WP_CLI::log( 'Resuming the unfinished round. Use --restart to start a new one.' ); $summary = Hypamoon_Connect_Exporter::summary( Hypamoon_Connect_Exporter::state() ); } $progress = WP_CLI\Utils\make_progress_bar( 'Sending', max( 1, (int) $summary['total'] ) ); $progress->tick( min( (int) $summary['processed'], max( 1, (int) $summary['total'] ) ) ); $shown = (int) $summary['processed']; $failures = 0; while ( in_array( $summary['status'], array( 'running', 'stopped' ), true ) ) { $result = Hypamoon_Connect_Exporter::step(); if ( is_wp_error( $result ) ) { $data = (array) $result->get_error_data(); if ( ! empty( $data['retry_after'] ) ) { WP_CLI::log( sprintf( 'Rate limited, waiting %d seconds.', $data['retry_after'] ) ); sleep( (int) $data['retry_after'] ); continue; } if ( ! empty( $data['retryable'] ) && $failures < 5 ) { ++$failures; $wait = min( 60, 5 * ( 2 ** ( $failures - 1 ) ) ); WP_CLI::warning( $result->get_error_message() . sprintf( ' Trying again in %d seconds.', $wait ) ); sleep( $wait ); continue; } $progress->finish(); WP_CLI::error( $result->get_error_message() . ' Run the command again to resume; nothing is sent twice.' ); } $failures = 0; $summary = $result; $delta = (int) $summary['processed'] - $shown; if ( $delta > 0 ) { $progress->tick( $delta ); $shown = (int) $summary['processed']; } } $progress->finish(); if ( 'ready_to_finish' === $summary['status'] ) { $summary = Hypamoon_Connect_Exporter::finish(); if ( is_wp_error( $summary ) ) { WP_CLI::error( $summary->get_error_message() . ' Run the command again to finish.' ); } } WP_CLI::log( sprintf( 'Sent %d records in %d batches: %d created, %d updated, %d skipped by Hypamoon, %d left out by the period filter, %d duplicate batches, %d truncated.', $summary['sent'], $summary['batches'], $summary['created'], $summary['updated'], $summary['skipped'], $summary['filtered'], $summary['duplicates'], $summary['truncated'] ) ); $result = is_array( $summary['result'] ) ? $summary['result'] : array(); if ( ! empty( $result['collections'] ) ) { $rows = array(); foreach ( $result['collections'] as $collection ) { $rows[] = array( 'collection' => $collection['name'], 'documents' => $collection['documents'], 'personal_data' => ! empty( $collection['personal_data'] ) ? 'yes (private)' : 'no', ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'collection', 'documents', 'personal_data' ) ); } WP_CLI::success( ! empty( $result['next_step'] ) ? $result['next_step'] : 'Export finished.' ); } /** * Disconnect this site (revokes the token) and delete the local state. * * @subcommand disconnect */ public function disconnect() { $result = Hypamoon_Connect_Service::disconnect(); if ( is_wp_error( $result ) ) { WP_CLI::warning( 'Hypamoon could not be told: ' . $result->get_error_message() ); } WP_CLI::success( 'Disconnected. Local state deleted.' ); } /** * Stop when the site is not connected. */ private function require_connection() { if ( ! Hypamoon_Connect_Store::is_connected() ) { WP_CLI::error( 'Not connected. Run: wp hypamoon connect ' ); } } /** * Print the keys that would be sent. * * @param array $selection Selection. * @param bool $all Also list keys that are off. */ private function print_plan( $selection, $all ) { $inventory = Hypamoon_Connect_Service::stored_inventory(); $rows = array(); $total = 0; foreach ( Hypamoon_Connect_Registry::all() as $key => $source ) { $rule = Hypamoon_Connect_Service::rule_for( $selection, $source ); $count = isset( $inventory[ $key ]['count'] ) ? (int) $inventory[ $key ]['count'] : null; if ( null === $count ) { try { $item = $source->inventory(); $count = (int) $item['count']; } catch ( Throwable $e ) { $count = 0; } } if ( ! $rule['enabled'] && ! $all ) { continue; } $filters = array(); if ( $rule['since'] || $rule['until'] ) { $filters[] = ( $rule['since'] ? $rule['since'] : '...' ) . ' to ' . ( $rule['until'] ? $rule['until'] : '...' ); } if ( $rule['statuses'] ) { $filters[] = implode( '/', $rule['statuses'] ); } if ( $rule['omit_fields'] ) { $filters[] = 'without ' . implode( ', ', $rule['omit_fields'] ); } $rows[] = array( 'send' => $rule['enabled'] ? 'yes' : 'no', 'key' => $key, 'label' => $source->label(), 'count' => $count, 'personal' => $source->personal_data() ? 'yes' : 'no', 'filters' => implode( '; ', $filters ), ); if ( $rule['enabled'] ) { $total += $count; } } WP_CLI\Utils\format_items( 'table', $rows, array( 'send', 'key', 'label', 'count', 'personal', 'filters' ) ); WP_CLI::log( sprintf( 'About %d items selected (before period and status filters).', $total ) ); } }