1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace Tamedevelopers \Support \Capsule ;
6+
7+ use Tamedevelopers \Support \Capsule \Logger ;
8+ use Tamedevelopers \Support \Capsule \Manager ;
9+ use Tamedevelopers \Support \Capsule \Traits \ArtisanTrait ;
10+
11+ /**
12+ * Minimal artisan-like dispatcher for Tamedevelopers Support
13+ *
14+ * Supports Laravel-like syntax:
15+ * php tame <command>[:subcommand] [--flag] [--option=value]
16+ * Examples:
17+ * php tame key:generate
18+ * php tame migrate:fresh --seed --database=mysql
19+ */
20+ class Artisan
21+ {
22+ use ArtisanTrait;
23+
24+ /**
25+ * Command registry
26+ * @var string
27+ */
28+ public $ cliname ;
29+
30+ /**
31+ * Registered commands map
32+ * @var array<string, array{instance?: object, handler?: callable, description: string}>
33+ */
34+ protected static array $ commands = [];
35+
36+ public function __construct ()
37+ {
38+ // Ensure environment variables are loaded before accessing them
39+ Manager::startEnvIFNotStarted ();
40+ }
41+
42+ /**
43+ * Register a command by name with description
44+ *
45+ * @param string $name
46+ * @param callable|object $handler Either a callable or a command class instance
47+ * @param string $description Short description for `list`
48+ */
49+ public function register (string $ name , $ handler , string $ description = '' ): void
50+ {
51+ if (\is_object ($ handler ) && !\is_callable ($ handler )) {
52+ self ::$ commands [$ name ] = [
53+ 'instance ' => $ handler ,
54+ 'description ' => $ description ,
55+ ];
56+ return ;
57+ }
58+
59+ // Fallback to callable handler registration
60+ self ::$ commands [$ name ] = [
61+ 'handler ' => $ handler ,
62+ 'description ' => $ description ,
63+ ];
64+ }
65+
66+ /**
67+ * Handle argv input and dispatch
68+ */
69+ public function run (array $ argv ): int
70+ {
71+ // In PHP CLI, $argv[0] is the script name (tame), so command starts at index 1
72+ $ commandInput = $ argv [1 ] ?? 'list ' ;
73+ $ rawArgs = array_slice ($ argv , 2 );
74+
75+ if ($ commandInput === 'list ' ) {
76+ $ this ->renderList ();
77+ return 0 ;
78+ }
79+
80+ // Parse base and optional subcommand: e.g., key:generate -> [key, generate]
81+ [$ base , $ sub ] = $ this ->splitCommand ($ commandInput );
82+
83+ if (!isset (self ::$ commands [$ base ])) {
84+ Logger::error ("Command \"{$ commandInput }\" is not defined. \n\n" );
85+ return 1 ;
86+ }
87+
88+ $ entry = self ::$ commands [$ base ];
89+
90+ // Parse flags/options once and pass where applicable
91+ [$ positionals , $ options ] = $ this ->parseArgs ($ rawArgs );
92+
93+ // If registered with a class instance, we support subcommands and flag-to-method routing
94+ if (isset ($ entry ['instance ' ]) && \is_object ($ entry ['instance ' ])) {
95+ $ instance = $ entry ['instance ' ];
96+
97+ // Resolve primary method to call
98+ $ primaryMethod = $ sub ?: 'handle ' ;
99+ if (!method_exists ($ instance , $ primaryMethod )) {
100+ // command instance name
101+ $ instanceName = get_class ($ instance );
102+ Logger::error ("Missing method \"{$ primaryMethod }() \" in {$ instanceName }. " );
103+
104+ // Show small hint for available methods on the instance (public only)
105+ $ hints = $ this ->introspectPublicMethods ($ instance );
106+
107+ if ( !empty ($ hints )) {
108+ Logger::info ("Available methods: {$ hints }\n" );
109+ }
110+ return 1 ;
111+ }
112+
113+ $ exitCode = (int ) ($ this ->invokeCommandMethod ($ instance , $ primaryMethod , $ positionals , $ options ) ?? 0 );
114+
115+ // Route flags as methods on the same instance
116+ $ invalidFlags = [];
117+ foreach ($ options as $ flag => $ value ) {
118+ $ method = $ this ->optionToMethodName ($ flag );
119+ // Skip if this flag matches the already-run primary method
120+ if ($ method === $ primaryMethod ) {
121+ continue ;
122+ }
123+ if (method_exists ($ instance , $ method )) {
124+ $ this ->invokeCommandMethod ($ instance , $ method , $ positionals , $ options , $ flag );
125+ } else {
126+ $ invalidFlags [] = $ flag ;
127+ }
128+ }
129+
130+ if (!empty ($ invalidFlags )) {
131+ Logger::error ("Invalid option/method: -- " . implode (', -- ' , $ invalidFlags ) . "\n" );
132+ }
133+
134+ return $ exitCode ;
135+ }
136+
137+ // Fallback: callable handler (no subcommands/flags routing)
138+ if (isset ($ entry ['handler ' ]) && \is_callable ($ entry ['handler ' ])) {
139+ $ handler = $ entry ['handler ' ];
140+ return (int ) ($ handler ($ rawArgs ) ?? 0 );
141+ }
142+
143+ Logger::error ("Command not properly registered: {$ commandInput }\n" );
144+ return 1 ;
145+ }
146+
147+ /**
148+ * Render list of available commands
149+ */
150+ private function renderList (): void
151+ {
152+ $ this ->cliname = "{$ this ->cliname }\n" ?: "Tamedevelopers Support CLI \n" ;
153+
154+ Logger::helpHeader ($ this ->cliname );
155+ Logger::writeln ('<yellow>Usage:</yellow> ' );
156+ Logger::writeln (' php tame <command> [:option] [arguments] ' );
157+ Logger::writeln ('' );
158+
159+ $ grouped = $ this ->buildGroupedCommandList (self ::$ commands );
160+
161+ // Root commands first
162+ if (isset ($ grouped ['__root ' ])) {
163+ Logger::helpHeader ('Available commands: ' , 'method ' );
164+ foreach ($ grouped ['__root ' ] as $ cmd => $ desc ) {
165+ // Label with color
166+ $ label = Logger::segments ([
167+ ['text ' => $ cmd , 'style ' => 'green ' ],
168+ ]);
169+
170+ // Pad description to align
171+ $ visibleLen = strlen (preg_replace ('/<\/?[a-zA-Z0-9_-]+>/ ' , '' , ' ' . $ label ) ?? '' );
172+ $ spaces = max (1 , 35 - $ visibleLen );
173+
174+ Logger::writeln (' ' . $ label . str_repeat (' ' , $ spaces ) . Logger::segments ([
175+ ['text ' => $ desc , 'style ' => 'desc ' ],
176+ ]));
177+ }
178+ Logger::writeln ('' );
179+ unset($ grouped ['__root ' ]);
180+ }
181+
182+ // Then grouped by base (e.g., auth, cache, migrate:*)
183+ foreach ($ grouped as $ group => $ items ) {
184+ Logger::helpHeader ($ group , 'method ' );
185+ foreach ($ items as $ full => $ desc ) {
186+ [$ ns , $ method ] = explode (': ' , $ full , 2 );
187+ // method (yellow), description (white)
188+ Logger::helpItem ($ ns , $ method , null , $ desc , 35 , false , ['green ' , 'green ' ]);
189+ }
190+ Logger::writeln ('' );
191+ }
192+ }
193+
194+ }
0 commit comments