Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions data-machine-code.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,9 @@ function datamachine_code_load_chat_tools() {
* Register system tasks.
*/
add_filter( 'datamachine_tasks', function ( array $tasks ): array {
$tasks['github_create_issue'] = \DataMachineCode\Tasks\GitHubIssueTask::class;
$tasks['worktree_cleanup'] = \DataMachineCode\Tasks\WorktreeCleanupTask::class;
$tasks['github_create_issue'] = \DataMachineCode\Tasks\GitHubIssueTask::class;
$tasks['worktree_cleanup'] = \DataMachineCode\Tasks\WorktreeCleanupTask::class;
$tasks['workspace_hygiene_report'] = \DataMachineCode\Tasks\WorkspaceHygieneReportTask::class;
return $tasks;
} );

Expand All @@ -224,14 +225,27 @@ function datamachine_code_load_chat_tools() {
* @see https://github.com/Extra-Chill/data-machine/pull/1117
*/
add_filter( 'datamachine_recurring_schedules', function ( array $schedules ): array {
$schedules['worktree_cleanup'] = array(
$schedules['worktree_cleanup'] = array(
'task_type' => 'worktree_cleanup',
'interval' => 'daily',
'enabled_setting' => \DataMachineCode\Tasks\WorktreeCleanupTask::SETTING_KEY,
'default_enabled' => false,
'label' => 'Daily — cleans up merged worktrees',
'task_params' => array( 'source' => 'recurring_schedule' ),
);
$schedules['workspace_hygiene_report'] = array(
'task_type' => 'workspace_hygiene_report',
'interval' => 'weekly',
'enabled_setting' => \DataMachineCode\Tasks\WorkspaceHygieneReportTask::SETTING_KEY,
'default_enabled' => false,
'label' => 'Weekly — reports workspace disk hygiene',
'task_params' => array(
'source' => 'recurring_schedule',
'include_cleanup' => true,
'include_sizes' => true,
'size_limit' => 200,
),
);
return $schedules;
} );

Expand Down
68 changes: 68 additions & 0 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,52 @@ private function registerAbilities(): void {
)
);

wp_register_ability(
'datamachine/workspace-hygiene-report',
array(
'label' => 'Workspace Hygiene Report',
'description' => 'Build a non-destructive workspace hygiene report with disk, size, worktree, and local cleanup dry-run summaries.',
'category' => 'datamachine-code-workspace',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'include_cleanup' => array(
'type' => 'boolean',
'description' => 'Include a local-only worktree cleanup dry-run summary. Default true.',
),
'include_sizes' => array(
'type' => 'boolean',
'description' => 'Include best-effort top-level workspace size data. Default true.',
),
'size_limit' => array(
'type' => 'integer',
'description' => 'Maximum top-level workspace entries to size. Default 200.',
),
),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'generated_at' => array( 'type' => 'string' ),
'workspace_path' => array( 'type' => 'string' ),
'destructive' => array( 'type' => 'boolean' ),
'size' => array( 'type' => 'object' ),
'disk' => array( 'type' => 'object' ),
'worktrees' => array( 'type' => 'object' ),
'top_repos_by_worktrees' => array( 'type' => 'array' ),
'top_repos_by_size' => array( 'type' => 'array' ),
'cleanup' => array( 'type' => 'object' ),
'suggested_cleanup_command' => array( 'type' => 'string' ),
'notes' => array( 'type' => 'array' ),
),
),
'execute_callback' => array( self::class, 'workspaceHygieneReport' ),
'permission_callback' => fn() => PermissionHelper::can_manage(),
'meta' => array( 'show_in_rest' => false ),
)
);

wp_register_ability(
'datamachine/workspace-worktree-list',
array(
Expand Down Expand Up @@ -1449,6 +1495,28 @@ public static function worktreeList( array $input ): array|\WP_Error {
return $workspace->worktree_list( $repo, $state );
}

/**
* Build a non-destructive workspace hygiene report.
*
* @param array $input Input parameters.
* @return array|\WP_Error
*/
public static function workspaceHygieneReport( array $input ): array|\WP_Error {
$workspace = new Workspace();
$opts = array();
if ( array_key_exists( 'include_cleanup', $input ) ) {
$opts['include_cleanup'] = (bool) $input['include_cleanup'];
}
if ( array_key_exists( 'include_sizes', $input ) ) {
$opts['include_sizes'] = (bool) $input['include_sizes'];
}
if ( isset( $input['size_limit'] ) ) {
$opts['size_limit'] = (int) $input['size_limit'];
}

return $workspace->workspace_hygiene_report( $opts );
}

/**
* Remove a worktree.
*
Expand Down
188 changes: 188 additions & 0 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,63 @@ public function remove_repo( array $args, array $assoc_args ): void {
WP_CLI::success( $result['message'] );
}

/**
* Show a non-destructive workspace hygiene report.
*
* ## OPTIONS
*
* [--format=<format>]
* : Output format.
* ---
* default: table
* options:
* - table
* - json
* ---
*
* [--skip-cleanup]
* : Skip the local cleanup dry-run summary.
*
* [--skip-sizes]
* : Skip best-effort workspace size collection.
*
* [--size-limit=<count>]
* : Maximum top-level workspace entries to size.
* ---
* default: 200
* ---
*
* ## EXAMPLES
*
* wp datamachine-code workspace hygiene
* wp datamachine-code workspace hygiene --format=json
*
* @subcommand hygiene
*/
public function hygiene( array $args, array $assoc_args ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
$ability = wp_get_ability( 'datamachine/workspace-hygiene-report' );
if ( ! $ability ) {
WP_CLI::error( 'Workspace hygiene ability not available.' );
return;
}

$input = array(
'include_cleanup' => empty( $assoc_args['skip-cleanup'] ),
'include_sizes' => empty( $assoc_args['skip-sizes'] ),
);
if ( isset( $assoc_args['size-limit'] ) ) {
$input['size_limit'] = (int) $assoc_args['size-limit'];
}

$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
return;
}

$this->render_workspace_hygiene_report( $result, $assoc_args );
}

/**
* Show detailed info about a workspace repo.
*
Expand Down Expand Up @@ -1449,6 +1506,137 @@ private function renderWorktreeResult( string $operation, array $result, array $
}
}

/**
* Render workspace hygiene report output.
*
* @param array $report Hygiene report.
* @param array $assoc_args CLI args.
* @return void
*/
private function render_workspace_hygiene_report( array $report, array $assoc_args ): void {
$format = isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table';
if ( 'json' === $format ) {
$json = wp_json_encode( $report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
WP_CLI::log( false === $json ? '{}' : $json );
return;
}

$size = (array) ( $report['size'] ?? array() );
$disk = (array) ( $report['disk'] ?? array() );
$worktrees = (array) ( $report['worktrees'] ?? array() );
$cleanup = (array) ( $report['cleanup'] ?? array() );
$cleanup_summary = (array) ( $cleanup['summary'] ?? array() );

WP_CLI::log( 'Workspace hygiene:' );
$this->format_items(
array(
array(
'metric' => 'workspace_path',
'value' => (string) ( $report['workspace_path'] ?? '' ),
),
array(
'metric' => 'workspace_size',
'value' => (string) ( $size['total_human'] ?? '-' ),
),
array(
'metric' => 'size_mode',
'value' => (string) ( $size['mode'] ?? '-' ),
),
array(
'metric' => 'size_scan_complete',
'value' => ! empty( $size['scan_complete'] ) ? 'yes' : 'no',
),
array(
'metric' => 'disk_free',
'value' => (string) ( $disk['free_human'] ?? '-' ),
),
array(
'metric' => 'worktree_count',
'value' => (string) ( $worktrees['worktrees'] ?? 0 ),
),
array(
'metric' => 'dirty_protected',
'value' => (string) ( $worktrees['protected_dirty'] ?? 0 ),
),
array(
'metric' => 'unpushed_protected',
'value' => (string) ( $worktrees['protected_unpushed'] ?? 0 ),
),
array(
'metric' => 'missing_metadata',
'value' => (string) ( $worktrees['missing_metadata'] ?? 0 ),
),
array(
'metric' => 'external_worktrees',
'value' => (string) ( $worktrees['external'] ?? 0 ),
),
array(
'metric' => 'cleanup_candidates',
'value' => (string) ( $cleanup_summary['would_remove'] ?? 0 ),
),
),
array( 'metric', 'value' ),
array( 'format' => 'table' ),
'metric'
);

$top_size = array_slice( (array) ( $report['top_repos_by_size'] ?? array() ), 0, 10 );
if ( array() !== $top_size ) {
WP_CLI::log( '' );
WP_CLI::log( 'Top repos by size:' );
$this->format_items(
array_map(
fn( $row ) => array(
'repo' => $row['repo'] ?? '',
'size' => $row['human'] ?? '',
'bytes' => $row['bytes'] ?? 0,
),
$top_size
),
array( 'repo', 'size', 'bytes' ),
array( 'format' => 'table' ),
'bytes'
);
}

$top_counts = array_slice( (array) ( $report['top_repos_by_worktrees'] ?? array() ), 0, 10 );
if ( array() !== $top_counts ) {
WP_CLI::log( '' );
WP_CLI::log( 'Top repos by worktree count:' );
$this->format_items( $top_counts, array( 'repo', 'worktree_count' ), array( 'format' => 'table' ), 'worktree_count' );
}

$candidates = array_slice( (array) ( $cleanup['biggest_candidates'] ?? array() ), 0, 10 );
if ( array() !== $candidates ) {
WP_CLI::log( '' );
WP_CLI::log( 'Cleanup candidates:' );
$this->format_items(
array_map(
fn( $row ) => array(
'handle' => $row['handle'] ?? '',
'branch' => $row['branch'] ?? '',
'signal' => $row['signal'] ?? '',
'size' => $row['size_human'] ?? '',
),
$candidates
),
array( 'handle', 'branch', 'signal', 'size' ),
array( 'format' => 'table' ),
'handle'
);
}

if ( ! empty( $report['suggested_cleanup_command'] ) ) {
WP_CLI::log( '' );
WP_CLI::log( 'Suggested cleanup review:' );
WP_CLI::log( (string) $report['suggested_cleanup_command'] );
}

foreach ( (array) ( $report['notes'] ?? array() ) as $note ) {
WP_CLI::log( 'Note: ' . $note );
}
}

/**
* Render cleanup output with a machine-safe JSON contract and concise tables.
*
Expand Down
Loading