Skip to content

Commit e5d588a

Browse files
authored
Merge pull request #458 from Extra-Chill/fix/workspace-source-inventory
Expose workspace files to source inventory
2 parents 20e265d + 35da0ef commit e5d588a

2 files changed

Lines changed: 204 additions & 4 deletions

File tree

data-machine-code.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function datamachine_code_bootstrap() {
9898
new \DataMachineCode\Abilities\GitSyncAbilities();
9999
new \DataMachineCode\Abilities\CodeTaskAbilities();
100100
new \DataMachineCode\Abilities\WordPressRuntimeAbilities();
101+
\DataMachineCode\SourceInventory\WorkspaceSourceInventory::register();
101102
( new \DataMachineCode\Bundle\WorkspacePreloadArtifact() )->register();
102103

103104
// Project active workspace identity into Data Machine's engine_data
@@ -264,7 +265,7 @@ function datamachine_code_register_ability_categories() {
264265
* Register WP-CLI commands after core is loaded.
265266
*/
266267
function datamachine_code_register_cli_commands() {
267-
if ( ! defined('WP_CLI') || ! WP_CLI ) {
268+
if ( ! class_exists('\WP_CLI') ) {
268269
return;
269270
}
270271

@@ -451,9 +452,14 @@ function datamachine_code_load_chat_tools() {
451452
return;
452453
}
453454

454-
\DataMachine\Engine\AI\MemoryFileRegistry::register(
455+
$registry_class = '\DataMachine\Engine\AI\MemoryFileRegistry';
456+
if ( ! method_exists( $registry_class, 'register' ) ) {
457+
return;
458+
}
459+
460+
$registry_class::register(
455461
'AGENTS.md', 5, array(
456-
'layer' => \DataMachine\Engine\AI\MemoryFileRegistry::LAYER_SHARED,
462+
'layer' => defined( $registry_class . '::LAYER_SHARED' ) ? constant( $registry_class . '::LAYER_SHARED' ) : 'shared',
457463
'protected' => true,
458464
'composable' => true,
459465
'convention_path' => 'AGENTS.md',
@@ -808,7 +814,7 @@ function datamachine_code_render_workspace_inventory_section( string $wp ): stri
808814

809815
ksort($by_repo, SORT_NATURAL | SORT_FLAG_CASE);
810816

811-
$workspace_path = $listing['path'] ?? $workspace->get_path();
817+
$workspace_path = $listing['path'];
812818

813819
/**
814820
* Filter the workspace-inventory render mode.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
/**
3+
* Workspace-backed source inventory executor.
4+
*
5+
* @package DataMachineCode\SourceInventory
6+
*/
7+
8+
namespace DataMachineCode\SourceInventory;
9+
10+
use DataMachineCode\Workspace\Workspace;
11+
12+
defined('ABSPATH') || exit;
13+
14+
class WorkspaceSourceInventory {
15+
16+
private const KIND = 'workspace_files';
17+
18+
public static function register(): void {
19+
add_filter('datamachine_source_inventory_capabilities', array( self::class, 'capabilities' ), 10, 2);
20+
add_filter('datamachine_source_aggregate_page_callback', array( self::class, 'page_callback' ), 10, 3);
21+
add_filter('datamachine_source_inventory_page_callback', array( self::class, 'page_callback' ), 10, 3);
22+
}
23+
24+
/**
25+
* @param array<string,mixed> $capabilities Source capabilities.
26+
* @param array<string,mixed> $source Source descriptor.
27+
* @return array<string,mixed>
28+
*/
29+
public static function capabilities( array $capabilities, array $source ): array {
30+
if ( self::KIND !== ( $source['kind'] ?? '' ) ) {
31+
return $capabilities;
32+
}
33+
34+
return array_merge(
35+
$capabilities,
36+
array(
37+
'enumerable' => true,
38+
'has_total_count' => true,
39+
'stable_ids' => true,
40+
)
41+
);
42+
}
43+
44+
/**
45+
* @param callable|null $callback Existing callback.
46+
* @param array<string,mixed> $source Source descriptor.
47+
* @param array<string,mixed> $input Ability input.
48+
*/
49+
public static function page_callback( $callback, array $source, array $_input ): ?callable {
50+
unset( $_input );
51+
52+
if ( is_callable( $callback ) || self::KIND !== ( $source['kind'] ?? '' ) ) {
53+
return $callback;
54+
}
55+
56+
return static function ( array $params, array $state ) use ( $source ): array {
57+
return self::page( $source, $params, $state );
58+
};
59+
}
60+
61+
/**
62+
* @param array<string,mixed> $source Source descriptor.
63+
* @param array<string,mixed> $params Page params.
64+
* @param array<string,mixed> $state Page state.
65+
* @return array<string,mixed>
66+
*/
67+
private static function page( array $source, array $params, array $state ): array {
68+
$workspace = new Workspace();
69+
$handle = sanitize_text_field( (string) ( $source['handle'] ?? $source['repo'] ?? '' ) );
70+
$base_path = ltrim( (string) ( $source['path'] ?? '' ), '/' );
71+
72+
if ( '' === $handle ) {
73+
return array(
74+
'items' => array(),
75+
'total' => 0,
76+
'error' => 'workspace_handle_required',
77+
);
78+
}
79+
80+
$repo_path = $workspace->get_repo_path( $handle );
81+
if ( ! is_dir( $repo_path ) ) {
82+
return array(
83+
'items' => array(),
84+
'total' => 0,
85+
'error' => 'workspace_repo_not_found',
86+
);
87+
}
88+
89+
$repo_real = realpath( $repo_path );
90+
if ( false === $repo_real ) {
91+
return array(
92+
'items' => array(),
93+
'total' => 0,
94+
'error' => 'workspace_repo_not_found',
95+
);
96+
}
97+
98+
$target = '' === $base_path ? $repo_real : $repo_real . '/' . $base_path;
99+
$check = $workspace->validate_containment( $target, $repo_real );
100+
if ( empty( $check['valid'] ) || empty( $check['real_path'] ) ) {
101+
return array(
102+
'items' => array(),
103+
'total' => 0,
104+
'error' => 'workspace_path_not_allowed',
105+
);
106+
}
107+
108+
$target_real = (string) $check['real_path'];
109+
if ( ! is_file( $target_real ) && ! is_dir( $target_real ) ) {
110+
return array(
111+
'items' => array(),
112+
'total' => 0,
113+
'error' => 'workspace_path_not_found',
114+
);
115+
}
116+
117+
$include_patterns = self::string_list( $source['include'] ?? array( '*.php' ) );
118+
$exclude_patterns = self::string_list( $source['exclude'] ?? array( '.git/*', 'node_modules/*', 'vendor/*' ) );
119+
$max = max( 1, min( 10000, (int) ( $source['max_files'] ?? 1000 ) ) );
120+
$files = self::collect_files( $repo_real, $target_real, $include_patterns, $exclude_patterns, $max );
121+
122+
$offset = max( 0, (int) ( $params['offset'] ?? $state['offset'] ?? 0 ) );
123+
$limit = max( 1, (int) ( $params['limit'] ?? $state['limit'] ?? 100 ) );
124+
125+
return array(
126+
'items' => array_slice( $files, $offset, $limit ),
127+
'total' => count( $files ),
128+
);
129+
}
130+
131+
/**
132+
* @param string[] $include_patterns Include glob patterns.
133+
* @param string[] $exclude_patterns Exclude glob patterns.
134+
* @return array<int,array<string,mixed>>
135+
*/
136+
private static function collect_files( string $repo_real, string $target_real, array $include_patterns, array $exclude_patterns, int $max ): array {
137+
$paths = is_file( $target_real )
138+
? array( $target_real )
139+
: new \RecursiveIteratorIterator(
140+
new \RecursiveDirectoryIterator( $target_real, \FilesystemIterator::SKIP_DOTS )
141+
);
142+
143+
$items = array();
144+
foreach ( $paths as $path ) {
145+
$file_path = is_string( $path ) ? $path : $path->getPathname();
146+
if ( ! is_file( $file_path ) ) {
147+
continue;
148+
}
149+
150+
$relative = ltrim( substr( $file_path, strlen( $repo_real ) ), '/' );
151+
if ( ! self::matches( $relative, $include_patterns ) || self::matches( $relative, $exclude_patterns ) ) {
152+
continue;
153+
}
154+
155+
$items[] = array(
156+
'id' => $relative,
157+
'item_type' => 'source-file',
158+
'source_path' => $relative,
159+
'size' => filesize( $file_path ),
160+
);
161+
162+
if ( count( $items ) >= $max ) {
163+
break;
164+
}
165+
}
166+
167+
usort( $items, static fn( array $a, array $b ): int => strcmp( (string) $a['source_path'], (string) $b['source_path'] ) );
168+
return $items;
169+
}
170+
171+
/** @return string[] */
172+
private static function string_list( mixed $value ): array {
173+
if ( is_string( $value ) ) {
174+
$value = array_map( 'trim', explode( ',', $value ) );
175+
}
176+
177+
if ( ! is_array( $value ) ) {
178+
return array();
179+
}
180+
181+
return array_values( array_filter( array_map( 'strval', $value ), static fn( string $item ): bool => '' !== trim( $item ) ) );
182+
}
183+
184+
/** @param string[] $patterns Patterns. */
185+
private static function matches( string $path, array $patterns ): bool {
186+
foreach ( $patterns as $pattern ) {
187+
if ( fnmatch( $pattern, $path ) || fnmatch( $pattern, basename( $path ) ) ) {
188+
return true;
189+
}
190+
}
191+
192+
return false;
193+
}
194+
}

0 commit comments

Comments
 (0)