diff --git a/extrachill-cli.php b/extrachill-cli.php index 86fd742..0442952 100644 --- a/extrachill-cli.php +++ b/extrachill-cli.php @@ -23,23 +23,16 @@ |-------------------------------------------------------------------------- | AGENTS.md — composable file section registration |-------------------------------------------------------------------------- -| Registers the Extra Chill CLI section in the AGENTS.md composable file -| so that external agent runtimes (Claude Code, OpenCode, etc.) discover -| the platform CLI surface automatically. Runs outside the WP_CLI guard -| because the compose command and auto-regeneration may fire in non-CLI -| WordPress contexts (e.g. plugin activation hooks). +| Registers concise ownership, routing, multisite, and discovery guidance for +| the Extra Chill CLI. Runs outside the WP_CLI guard because composition and +| auto-regeneration may fire in non-CLI WordPress contexts. */ add_action( 'plugins_loaded', function () { if ( ! class_exists( '\DataMachine\Engine\AI\SectionRegistry' ) ) { return; } - // This section can be composed in non-CLI (web/cron) contexts where the - // WP_CLI guard below has not registered the autoloader. Register it here - // so the generator can reflect over the real command classes (and any - // traits they use) regardless of context. - require_once EXTRACHILL_CLI_PATH . 'inc/Autoloader.php'; - \ExtraChill\CLI\Autoloader::register( EXTRACHILL_CLI_PATH ); + require_once EXTRACHILL_CLI_PATH . 'inc/AgentsMdSection.php'; $wp = 'wp --allow-root --path=' . ABSPATH; diff --git a/homeboy.json b/homeboy.json index 53343e9..ecae9bc 100644 --- a/homeboy.json +++ b/homeboy.json @@ -8,6 +8,7 @@ "remote_path": "wp-content/plugins/extrachill-cli", "self_checks": { "test": [ + "php tests/AgentsMdSectionTest.php", "php tests/QRCodeCommandTest.php", "php tests/ExperimentsCommandTest.php", "php tests/ExperimentsOwnerContractTest.php", diff --git a/inc/AgentsMdSection.php b/inc/AgentsMdSection.php index 6dcd892..87deae5 100644 --- a/inc/AgentsMdSection.php +++ b/inc/AgentsMdSection.php @@ -2,30 +2,11 @@ /** * AGENTS.md Section Generator * - * Generates the "Extra Chill CLI" section of the composed AGENTS.md file by - * introspecting the real registered command tree instead of hand-maintaining - * a heredoc. Walks every command registered in CommandRegistry, reflects over - * each command class's public methods, and emits each namespace with its real - * subcommands and their PHPDoc short descriptions. - * - * Context-safe: the section callback runs on `plugins_loaded` in web/cron - * compose contexts where the WP-CLI runtime and the plugin's PSR-4 autoloader - * (both guarded by `if ( WP_CLI )`) are NOT loaded. This generator therefore - * resolves command class files directly from disk via the same PSR-4 layout - * and reflects over the class files, never relying on the live WP-CLI runner. - * - * Pending Extra-Chill/data-machine#2613: once that shared - * `CliCommandIntrospector` helper lands, this class can delegate the - * reflection/parsing to it and keep only the CommandRegistry map local. - * * @package ExtraChill\CLI */ namespace ExtraChill\CLI; -use ReflectionClass; -use ReflectionMethod; - if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -35,198 +16,28 @@ class AgentsMdSection { /** * Build the Markdown body for the Extra Chill CLI AGENTS.md section. * - * @param string $wp The `wp --allow-root --path=...` invocation prefix. + * @param string $wp The composed WP-CLI invocation prefix. * @return string */ public static function render( $wp ) { $lines = array(); $lines[] = '### Extra Chill CLI'; $lines[] = ''; - $lines[] = 'Platform-specific tooling wrapping common operations into unified commands.'; - $lines[] = "Discover everything: `{$wp} extrachill --help`"; + $lines[] = 'Extra Chill CLI owns the unified WP-CLI surface for Extra Chill platform operations. Feature plugins own the underlying abilities and business behavior.'; $lines[] = ''; - - foreach ( self::collect_commands() as $command => $subcommands ) { - // Strip the leading "extrachill " for the human-readable summary, - // but keep the full invocation in the code span. - $summary = self::summarize_subcommands( $subcommands ); - - if ( '' !== $summary ) { - $lines[] = "- `{$wp} {$command}` — {$summary}"; - } else { - $lines[] = "- `{$wp} {$command}`"; - } - - foreach ( $subcommands as $sub ) { - if ( '__default' === $sub['name'] ) { - continue; - } - $desc = $sub['description']; - if ( '' !== $desc ) { - $lines[] = " - `{$sub['name']}` — {$desc}"; - } else { - $lines[] = " - `{$sub['name']}`"; - } - } - } - + $lines[] = '**Default routing**'; + $lines[] = "- Platform health and analytics: `{$wp} extrachill platform health` and `{$wp} extrachill analytics summary`"; + $lines[] = "- Events and venues: `{$wp} extrachill events --help` and `{$wp} extrachill venues --help`"; + $lines[] = "- Artists, users, and community: `{$wp} extrachill artists --help`, `{$wp} extrachill users --help`, and `{$wp} extrachill community --help`"; + $lines[] = "- Content and newsletters: `{$wp} extrachill content --help` and `{$wp} extrachill newsletter --help`"; + $lines[] = "- Diagnostics: `{$wp} extrachill analytics errors`, `{$wp} extrachill analytics 404 summary`, and `{$wp} extrachill cache status`"; $lines[] = ''; - $lines[] = 'All commands support `--help` for full options and subcommand discovery.'; + $lines[] = '**Multisite**'; + $lines[] = 'Keep the composed WP-CLI prefix and its site target. Use `--url=` when a command needs data or abilities owned by another network site; without a site target, WP-CLI uses the main site.'; + $lines[] = ''; + $lines[] = '**Discovery**'; + $lines[] = "Use `{$wp} extrachill --help` and `{$wp} extrachill --help` for the complete live command, subcommand, and options contract. Live `--help` is authoritative; this section intentionally does not enumerate every action."; return implode( "\n", $lines ); } - - /** - * Walk the CommandRegistry and reflect each class into its subcommands. - * - * @return array> - * command string => ordered list of subcommands. - */ - private static function collect_commands() { - $out = array(); - - foreach ( CommandRegistry::map() as $command => $class ) { - $subcommands = self::reflect_subcommands( $class ); - $out[ $command ] = $subcommands; - } - - return $out; - } - - /** - * Reflect a command class into its list of public subcommands. - * - * Public methods are WP-CLI subcommands. The subcommand name is taken from - * the `@subcommand ` annotation when present, otherwise the method - * name with underscores converted to hyphens (WP-CLI's own convention). - * `__invoke` / `@subcommand __default` represent the directly-invokable - * namespace itself. Private, protected, static, and magic helper methods - * are skipped. - * - * @param class-string $class Command class. - * @return array - */ - private static function reflect_subcommands( $class ) { - // The autoloader (registered before this section is composed) resolves - // the class file and any traits it uses; class_exists triggers it. - if ( ! class_exists( $class ) ) { - return array(); - } - - try { - $reflection = new ReflectionClass( $class ); - } catch ( \Throwable $e ) { - return array(); - } - - $subcommands = array(); - - foreach ( $reflection->getMethods( ReflectionMethod::IS_PUBLIC ) as $method ) { - // Only methods declared on the command class itself. - if ( $method->getDeclaringClass()->getName() !== $reflection->getName() ) { - continue; - } - - if ( $method->isStatic() || $method->isConstructor() || $method->isDestructor() ) { - continue; - } - - $method_name = $method->getName(); - $doc = $method->getDocComment() ?: ''; - - // `__invoke` (or any method tagged @subcommand __default) maps to - // the namespace itself, so it has no sub-word. - $annotated = self::parse_subcommand_annotation( $doc ); - - if ( '__invoke' === $method_name && '' === $annotated ) { - $annotated = '__default'; - } - - if ( '' !== $annotated ) { - $name = $annotated; - } else { - // Skip other magic methods that are not subcommands. - if ( 0 === strpos( $method_name, '__' ) ) { - continue; - } - $name = str_replace( '_', '-', $method_name ); - } - - $subcommands[] = array( - 'name' => $name, - 'description' => self::parse_short_description( $doc ), - ); - } - - return $subcommands; - } - - /** - * Build a comma-separated summary of subcommand names for the headline. - * - * @param array $subcommands Subcommands. - * @return string - */ - private static function summarize_subcommands( $subcommands ) { - $names = array(); - foreach ( $subcommands as $sub ) { - if ( '__default' === $sub['name'] ) { - continue; - } - $names[] = $sub['name']; - } - - return implode( ', ', $names ); - } - - /** - * Parse the `@subcommand ` annotation from a docblock. - * - * @param string $doc Raw docblock. - * @return string Subcommand name, or '' when not annotated. - */ - private static function parse_subcommand_annotation( $doc ) { - if ( preg_match( '/@subcommand\s+(\S+)/', $doc, $m ) ) { - return $m[1]; - } - return ''; - } - - /** - * Parse the short description (first prose line) from a docblock. - * - * Mirrors how WP-CLI derives a command's summary for `--help`: the first - * non-empty content line of the docblock, before any `## SECTION` heading. - * - * @param string $doc Raw docblock. - * @return string - */ - private static function parse_short_description( $doc ) { - if ( '' === $doc ) { - return ''; - } - - // Strip the comment framing. - $doc = preg_replace( '#^/\*\*|\*/$#', '', $doc ); - $lines = preg_split( '/\r\n|\r|\n/', $doc ); - - foreach ( $lines as $line ) { - $line = preg_replace( '/^\s*\*\s?/', '', $line ); - $line = trim( $line ); - - if ( '' === $line ) { - continue; - } - - // Stop at structured sections / annotations. - if ( 0 === strpos( $line, '##' ) || 0 === strpos( $line, '@' ) ) { - return ''; - } - - // Drop a trailing period for a tighter inline summary. - return rtrim( $line, '.' ); - } - - return ''; - } } diff --git a/inc/Autoloader.php b/inc/Autoloader.php index 301d2c7..2de596a 100644 --- a/inc/Autoloader.php +++ b/inc/Autoloader.php @@ -3,10 +3,8 @@ * PSR-4 Autoloader * * Registers a namespace-scoped autoloader for the `ExtraChill\CLI\` namespace - * rooted at the plugin's `inc/` directory. Used in both the WP-CLI runtime - * (to load command classes on demand) and in non-CLI compose contexts (so the - * AGENTS.md section generator can reflect over command classes and resolve - * any traits or dependencies they `use` without a fatal error). + * rooted at the plugin's `inc/` directory so WP-CLI can load command classes + * on demand. * * @package ExtraChill\CLI */ diff --git a/inc/CommandRegistry.php b/inc/CommandRegistry.php index 9bc4a56..b1f950c 100644 --- a/inc/CommandRegistry.php +++ b/inc/CommandRegistry.php @@ -2,12 +2,8 @@ /** * Command Registry * - * Single source of truth mapping `wp extrachill ...` command strings to their - * implementing command classes. Both the WP-CLI bootstrap (which calls - * WP_CLI::add_command for each entry) and the AGENTS.md section generator - * (which reflects over each class to enumerate real subcommands) read from - * this map, so the documented CLI surface can never drift from what is - * actually registered. + * Maps `wp extrachill ...` command strings to their implementing command + * classes. The WP-CLI bootstrap registers every entry in this map. * * @package ExtraChill\CLI */ @@ -25,7 +21,7 @@ class CommandRegistry { * * Keys are the exact strings passed to WP_CLI::add_command (the command * namespace, e.g. "extrachill venues" or "extrachill users access"). - * Order here determines both registration order and documentation order. + * Order here determines registration order. * * @return array */ diff --git a/inc/bootstrap.php b/inc/bootstrap.php index b58dae9..fb71986 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -17,9 +17,7 @@ return; } -// Register every command from the single-source-of-truth map. The same map -// drives the AGENTS.md section generator, so documentation cannot drift from -// what is actually registered here. +// Register every command from the command map. foreach ( ExtraChill\CLI\CommandRegistry::map() as $command => $command_class ) { WP_CLI::add_command( $command, $command_class ); } diff --git a/tests/AgentsMdSectionTest.php b/tests/AgentsMdSectionTest.php new file mode 100644 index 0000000..5eb325c --- /dev/null +++ b/tests/AgentsMdSectionTest.php @@ -0,0 +1,55 @@ + --help`" ), 'Nested live discovery command must be present.' ); +$check( false !== strpos( $output, 'Live `--help` is authoritative' ), 'Guidance must make live help authoritative.' ); +$check( false !== strpos( $output, 'Use `--url=`' ), 'Guidance must explain multisite targeting.' ); +$check( false === strpos( $output, 'publish-campaign' ), 'Guidance must not exhaustively enumerate command actions.' ); +$check( 30 >= substr_count( $output, "\n" ) + 1, 'Guidance must remain bounded to 30 lines.' ); + +if ( ! empty( $failures ) ) { + fwrite( STDERR, implode( "\n", $failures ) . "\n" ); + exit( 1 ); +} + +fwrite( STDOUT, "AgentsMdSection tests passed.\n" );