PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / trunk
ZIP AI – AI Website Builder & AI Agent (Beta) vtrunk
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / abilities / core / command-parser-trait.php

command-parser-trait.php in ZIP AI – AI Website Builder & AI Agent (Beta) trunk, at inc/abilities/core/command-parser-trait.php

131 lines 2.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Command Parser Trait — tokenise a WP-CLI command string and decode
4 * WP-CLI JSON stdout.
5 *
6 * Extracted from RunWpCli. The tokeniser is a pure function on a string
7 * with no WordPress dependencies; the JSON-decode helper depends only
8 * on the shared Response class.
9 *
10 * `parse_flags` is intentionally NOT in this trait — it is consumed only
11 * by `dispatch_native` and stays alongside the dispatcher.
12 *
13 * @package zip-ai
14 */
15
16 namespace ZipAI\MCP\Classes\Abilities\Core;
17
18 defined( 'ABSPATH' ) || exit;
19
20 use ZipAI\MCP\Classes\Core\Response;
21
22 /**
23 * Trait holding `parse_command_to_args` and `parse_output`.
24 */
25 trait Command_Parser_Trait {
26
27 /**
28 * Tokenise a WP-CLI command string into argv-style tokens.
29 *
30 * Handles single-quoted, double-quoted, and unquoted tokens. Returns
31 * WP_Error if the string contains unclosed quotes.
32 *
33 * @param string $command Command string without leading "wp".
34 * @return string[]|\WP_Error
35 */
36 private function parse_command_to_args( string $command ) {
37 $args = array();
38 $token = '';
39 $in_sq = false;
40 $in_dq = false;
41 $has_token = false;
42 $len = strlen( $command );
43
44 for ( $i = 0; $i < $len; $i++ ) {
45 $ch = $command[ $i ];
46
47 if ( $in_sq ) {
48 if ( "'" === $ch ) {
49 // POSIX '\'' concat trick — let a literal single quote through.
50 if ( $i + 3 < $len
51 && '\\' === $command[ $i + 1 ]
52 && "'" === $command[ $i + 2 ]
53 && "'" === $command[ $i + 3 ]
54 ) {
55 $token .= "'";
56 $i += 3;
57 continue;
58 }
59 $in_sq = false;
60 } else {
61 $token .= $ch;
62 }
63 continue;
64 }
65
66 if ( $in_dq ) {
67 if ( '"' === $ch ) {
68 $in_dq = false;
69 } elseif ( '\\' === $ch && $i + 1 < $len ) {
70 $next = $command[ $i + 1 ];
71 if ( '"' === $next || '\\' === $next ) {
72 $token .= $next;
73 ++$i;
74 } else {
75 $token .= $ch;
76 }
77 } else {
78 $token .= $ch;
79 }
80 continue;
81 }
82
83 if ( "'" === $ch ) {
84 $in_sq = true;
85 $has_token = true;
86 continue;
87 }
88 if ( '"' === $ch ) {
89 $in_dq = true;
90 $has_token = true;
91 continue;
92 }
93 if ( ' ' === $ch || "\t" === $ch ) {
94 if ( $has_token || '' !== $token ) {
95 $args[] = $token;
96 $token = '';
97 $has_token = false;
98 }
99 continue;
100 }
101
102 $token .= $ch;
103 $has_token = true;
104 }
105
106 if ( $in_sq || $in_dq ) {
107 return new \WP_Error( 'unclosed_quote', 'Command contains an unclosed quote.' );
108 }
109
110 if ( $has_token || '' !== $token ) {
111 $args[] = $token;
112 }
113
114 return $args;
115 }
116
117 /**
118 * Attempt to JSON-decode stdout from WP-CLI; fall back to the raw string.
119 *
120 * @param string $raw Raw stdout.
121 * @return array<string,mixed> Response data.
122 */
123 private function parse_output( string $raw ): array {
124 if ( '' === $raw ) {
125 return Response::success( '' );
126 }
127 $decoded = json_decode( $raw, true );
128 return JSON_ERROR_NONE === json_last_error() ? Response::success( $decoded ) : Response::success( $raw );
129 }
130 }
131