File Coverage

blib/lib/Trickster/CLI.pm
Criterion Covered Total %
statement 27 215 12.5
branch 0 38 0.0
condition 0 3 0.0
subroutine 9 27 33.3
pod 1 20 5.0
total 37 303 12.2


line stmt bran cond sub pod time code
1             package Trickster::CLI;
2              
3 1     1   358022 use strict;
  1         2  
  1         66  
4 1     1   6 use warnings;
  1         2  
  1         69  
5 1     1   21 use v5.14;
  1         3  
6              
7 1     1   5 use File::Path qw(make_path);
  1         1  
  1         78  
8 1     1   8 use File::Spec;
  1         2  
  1         32  
9 1     1   17 use Cwd qw(getcwd);
  1         2  
  1         60  
10 1     1   824 use Getopt::Long qw(GetOptionsFromArray);
  1         13114  
  1         4  
11              
12             our $VERSION = '0.01';
13              
14             sub new {
15 8     8 1 5510 my ($class) = @_;
16 8         29 return bless {}, $class;
17             }
18              
19             sub run {
20 0     0 0 0 my ($class, @args) = @_;
21            
22 0         0 my $self = $class->new;
23            
24 0 0       0 unless (@args) {
25 0         0 $self->show_help;
26 0         0 return;
27             }
28            
29 0         0 my $command = shift @args;
30            
31 0         0 my $method = "cmd_$command";
32 0 0       0 if ($self->can($method)) {
33 0         0 $self->$method(@args);
34             } else {
35 0         0 say "Unknown command: $command";
36 0         0 say "Run 'trickster help' for usage information.";
37 0         0 exit 1;
38             }
39             }
40              
41             sub cmd_help {
42 0     0 0 0 my ($self) = @_;
43 0         0 $self->show_help;
44             }
45              
46             sub show_help {
47 0     0 0 0 say "🎩 Trickster v$VERSION - Modern Perl Web Framework";
48 0         0 say "";
49 0         0 say "Usage: trickster [options]";
50 0         0 say "";
51 0         0 say "Commands:";
52 0         0 say " new Create a new Trickster application";
53 0         0 say " generate Generate a component (controller, model, template)";
54 0         0 say " server [options] Start the development server";
55 0         0 say " routes Display all registered routes";
56 0         0 say " version Show Trickster version";
57 0         0 say " help Show this help message";
58 0         0 say "";
59 0         0 say "Examples:";
60 0         0 say " trickster new myapp";
61 0         0 say " trickster generate controller User";
62 0         0 say " trickster server --port 3000";
63 0         0 say " trickster routes";
64             }
65              
66             sub cmd_version {
67 0     0 0 0 my ($self) = @_;
68 0         0 say "🎩 Trickster v$VERSION";
69 0         0 say "🐪 Perl $^V";
70             }
71              
72             sub cmd_new {
73 0     0 0 0 my ($self, $name, @args) = @_;
74            
75 0 0       0 unless ($name) {
76 0         0 say "Error: Application name required";
77 0         0 say "Usage: trickster new ";
78 0         0 exit 1;
79             }
80            
81 0 0       0 if (-e $name) {
82 0         0 say "Error: Directory '$name' already exists";
83 0         0 exit 1;
84             }
85            
86 0         0 say "🎩 Creating new Trickster application: $name";
87 0         0 say "";
88            
89             # Create directory structure
90 0         0 my @dirs = (
91             $name,
92             "$name/lib",
93             "$name/lib/$name",
94             "$name/lib/$name/Controller",
95             "$name/lib/$name/Model",
96             "$name/templates",
97             "$name/templates/layouts",
98             "$name/public",
99             "$name/public/css",
100             "$name/public/js",
101             "$name/t",
102             );
103            
104 0         0 for my $dir (@dirs) {
105 0         0 make_path($dir);
106 0         0 say " 📁 Created: $dir/";
107             }
108            
109             # Create files
110 0         0 $self->create_app_file($name);
111 0         0 $self->create_cpanfile($name);
112 0         0 $self->create_gitignore($name);
113 0         0 $self->create_readme($name);
114 0         0 $self->create_layout($name);
115 0         0 $self->create_home_template($name);
116 0         0 $self->create_test($name);
117            
118 0         0 say "";
119 0         0 say "✓ Application created successfully!";
120 0         0 say "";
121 0         0 say "Next steps:";
122 0         0 say " cd $name";
123 0         0 say " cpanm --installdeps .";
124 0         0 say " plackup app.psgi";
125 0         0 say "";
126 0         0 say "Visit http://localhost:5678";
127             }
128              
129             sub create_app_file {
130 0     0 0 0 my ($self, $name) = @_;
131            
132 0         0 my $content = <<"EOF";
133             #!/usr/bin/env perl
134             use strict;
135             use warnings;
136             use FindBin;
137             use lib "\$FindBin::Bin/lib";
138              
139             use Trickster;
140             use Trickster::Template;
141              
142             my \$app = Trickster->new(debug => 1);
143              
144             # Initialize template engine
145             my \$template = Trickster::Template->new(
146             path => ["\$FindBin::Bin/templates"],
147             cache => 0,
148             layout => 'layouts/main.tx',
149             default_vars => {
150             app_name => '$name',
151             },
152             );
153              
154             # Routes
155             \$app->get('/', sub {
156             my (\$req, \$res) = \@_;
157            
158             # Home page is a complete HTML document, no layout needed
159             my \$html = \$template->render('home.tx', {
160             app_name => '$name',
161             no_layout => 1,
162             });
163            
164             return \$res->html(\$html);
165             });
166              
167             \$app->to_app;
168             EOF
169            
170 0         0 my $file = "$name/app.psgi";
171 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
172 0         0 print $fh $content;
173 0         0 close $fh;
174 0         0 chmod 0755, $file;
175            
176 0         0 say " 📄 Created: $file";
177             }
178              
179             sub create_cpanfile {
180 0     0 0 0 my ($self, $name) = @_;
181            
182 0         0 my $content = <<'EOF';
183             requires 'perl', '5.014';
184             requires 'Trickster';
185             requires 'Text::Xslate';
186              
187             on 'test' => sub {
188             requires 'Test::More', '0.98';
189             requires 'Plack::Test';
190             };
191             EOF
192            
193 0         0 my $file = "$name/cpanfile";
194 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
195 0         0 print $fh $content;
196 0         0 close $fh;
197            
198 0         0 say " 📄 Created: $file";
199             }
200              
201             sub create_gitignore {
202 0     0 0 0 my ($self, $name) = @_;
203            
204 0         0 my $content = <<'EOF';
205             *.swp
206             *.bak
207             *~
208             .DS_Store
209             local/
210             .carton/
211             EOF
212            
213 0         0 my $file = "$name/.gitignore";
214 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
215 0         0 print $fh $content;
216 0         0 close $fh;
217            
218 0         0 say " 📄 Created: $file";
219             }
220              
221             sub create_readme {
222 0     0 0 0 my ($self, $name) = @_;
223            
224 0         0 my $content = <<"EOF";
225             # $name
226              
227             A Trickster web application.
228              
229             ## Installation
230              
231             ```bash
232             cpanm --installdeps .
233             ```
234              
235             ## Running
236              
237             ```bash
238             plackup app.psgi
239             ```
240              
241             Visit http://localhost:5678
242              
243             ## Development
244              
245             ```bash
246             plackup -R lib,templates app.psgi
247             ```
248              
249             ## Testing
250              
251             ```bash
252             prove -l t/
253             ```
254             EOF
255            
256 0         0 my $file = "$name/README.md";
257 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
258 0         0 print $fh $content;
259 0         0 close $fh;
260            
261 0         0 say " 📄 Created: $file";
262             }
263              
264             sub create_layout {
265 0     0 0 0 my ($self, $name) = @_;
266            
267 0         0 my $content = <<'EOF';
268            
269            
270            
271            
272            
273             [% title || app_name %]
274            
275            
276            
277            
278            

[% app_name %]

279            
280            
281            
282             [% content %]
283            
284            
285            
286            

Powered by Trickster

287            
288            
289            
290             EOF
291            
292 0         0 my $file = "$name/templates/layouts/main.tx";
293 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
294 0         0 print $fh $content;
295 0         0 close $fh;
296            
297 0         0 say " 📄 Created: $file";
298             }
299              
300             sub create_home_template {
301 0     0 0 0 my ($self, $name) = @_;
302            
303 0         0 my $content = <<'EOF';
304            
305            
306            
307            
308             [% app_name %] • Powered by Trickster
309            
310            
380            
381            
382            
383            
🎩
384            

[% app_name %]

385            

Congratulations! Your Trickster app is running.

386            
387            
388            
Fast routing
389            
Zero deps
390            
Stateless sessions
391            
Built for 2025
392            
393            
394            

Get started:

395            

trickster generate controller Home

396            
397            
Powered by Trickster
398            
399            
400            
401             EOF
402            
403 0         0 my $file = "$name/templates/home.tx";
404 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
405 0         0 print $fh $content;
406 0         0 close $fh;
407            
408 0         0 say " 📄 Created: $file";
409             }
410              
411             sub create_test {
412 0     0 0 0 my ($self, $name) = @_;
413            
414 0         0 my $content = <<'EOF';
415             use strict;
416             use warnings;
417             use Test::More;
418             use Plack::Test;
419             use HTTP::Request::Common;
420              
421             require './app.psgi';
422             my $app = do './app.psgi';
423              
424             test_psgi $app, sub {
425             my $cb = shift;
426            
427             my $res = $cb->(GET '/');
428             is $res->code, 200, 'GET / returns 200';
429             like $res->content, qr/Welcome/, 'Home page contains welcome message';
430             };
431              
432             done_testing;
433             EOF
434            
435 0         0 my $file = "$name/t/01-basic.t";
436 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
437 0         0 print $fh $content;
438 0         0 close $fh;
439            
440 0         0 say " 📄 Created: $file";
441             }
442              
443             sub cmd_generate {
444 0     0 0 0 my ($self, $type, $name, @args) = @_;
445            
446 0 0 0     0 unless ($type && $name) {
447 0         0 say "Error: Type and name required";
448 0         0 say "Usage: trickster generate ";
449 0         0 say "Types: controller, model, template";
450 0         0 exit 1;
451             }
452            
453 0         0 my $method = "generate_$type";
454 0 0       0 if ($self->can($method)) {
455 0         0 $self->$method($name, @args);
456             } else {
457 0         0 say "Error: Unknown type '$type'";
458 0         0 say "Available types: controller, model, template";
459 0         0 exit 1;
460             }
461             }
462              
463             sub generate_controller {
464 0     0 0 0 my ($self, $name) = @_;
465            
466 0         0 my $app_name = $self->detect_app_name;
467            
468 0         0 my $content = <<"EOF";
469             package ${app_name}::Controller::${name};
470              
471             use strict;
472             use warnings;
473             use v5.14;
474              
475             sub new {
476             my (\$class) = \@_;
477             return bless {}, \$class;
478             }
479              
480             sub index {
481             my (\$self, \$req, \$res) = \@_;
482            
483             return \$res->json({ message => 'Hello from ${name} controller' });
484             }
485              
486             sub show {
487             my (\$self, \$req, \$res) = \@_;
488             my \$id = \$req->param('id');
489            
490             return \$res->json({ id => \$id });
491             }
492              
493             1;
494             EOF
495            
496 0         0 my $file = "lib/${app_name}/Controller/${name}.pm";
497 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
498 0         0 print $fh $content;
499 0         0 close $fh;
500            
501 0         0 say "✓ Created controller: $file";
502 0         0 say "";
503 0         0 say "Add to your app.psgi:";
504 0         0 say " use ${app_name}::Controller::${name};";
505 0         0 say " my \$${name}_controller = ${app_name}::Controller::${name}->new;";
506 0         0 say " \$app->get('/${name}', sub { \$${name}_controller->index(\@_) });";
507             }
508              
509             sub generate_model {
510 0     0 0 0 my ($self, $name) = @_;
511            
512 0         0 my $app_name = $self->detect_app_name;
513            
514 0         0 my $content = <<"EOF";
515             package ${app_name}::Model::${name};
516              
517             use strict;
518             use warnings;
519             use v5.14;
520              
521             sub new {
522             my (\$class, %opts) = \@_;
523            
524             return bless {
525             data => {},
526             %opts,
527             }, \$class;
528             }
529              
530             sub find {
531             my (\$self, \$id) = \@_;
532             return \$self->{data}{\$id};
533             }
534              
535             sub all {
536             my (\$self) = \@_;
537             return [values %{\$self->{data}}];
538             }
539              
540             sub create {
541             my (\$self, \$data) = \@_;
542            
543             my \$id = time . int(rand(1000));
544             \$self->{data}{\$id} = { id => \$id, %\$data };
545            
546             return \$self->{data}{\$id};
547             }
548              
549             sub update {
550             my (\$self, \$id, \$data) = \@_;
551            
552             return unless exists \$self->{data}{\$id};
553            
554             \$self->{data}{\$id} = { %{\$self->{data}{\$id}}, %\$data };
555            
556             return \$self->{data}{\$id};
557             }
558              
559             sub delete {
560             my (\$self, \$id) = \@_;
561             return delete \$self->{data}{\$id};
562             }
563              
564             1;
565             EOF
566            
567 0         0 my $file = "lib/${app_name}/Model/${name}.pm";
568 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
569 0         0 print $fh $content;
570 0         0 close $fh;
571            
572 0         0 say "✓ Created model: $file";
573 0         0 say "";
574 0         0 say "Use in your app:";
575 0         0 say " use ${app_name}::Model::${name};";
576 0         0 say " my \$${name}_model = ${app_name}::Model::${name}->new;";
577             }
578              
579             sub generate_template {
580 0     0 0 0 my ($self, $name) = @_;
581            
582 0         0 my $content = <<'EOF';
583            
584            

[% title %]

585            

Template content goes here.

586            
587             EOF
588            
589 0         0 my $file = "templates/${name}.tx";
590 0 0       0 open my $fh, '>', $file or die "Cannot create $file: $!";
591 0         0 print $fh $content;
592 0         0 close $fh;
593            
594 0         0 say "✓ Created template: $file";
595 0         0 say "";
596 0         0 say "Render in your route:";
597 0         0 say " my \$html = \$template->render('${name}.tx', { title => 'Page Title' });";
598             }
599              
600             sub detect_app_name {
601 1     1 0 6 my ($self) = @_;
602            
603 1         14 my $cwd = getcwd;
604 1         6 my $app_name = (split '/', $cwd)[-1];
605            
606             # Capitalize first letter
607 1         31 $app_name = ucfirst($app_name);
608            
609 1         5 return $app_name;
610             }
611              
612             sub cmd_server {
613 0     0 0   my ($self, @args) = @_;
614            
615 0           my $port = 5678;
616 0           my $host = '0.0.0.0';
617 0           my $reload = 0;
618            
619 0           GetOptionsFromArray(\@args,
620             'port|p=i' => \$port,
621             'host|h=s' => \$host,
622             'reload|r' => \$reload,
623             );
624            
625 0 0         unless (-f 'app.psgi') {
626 0           say "Error: app.psgi not found";
627 0           say "Run this command from your application directory";
628 0           exit 1;
629             }
630            
631 0           say "🎩 Starting Trickster development server...";
632 0           say "🌐 Listening on http://$host:$port";
633 0           say "⚡ Press Ctrl+C to stop";
634 0           say "";
635            
636 0           my @cmd = ('plackup', '--port', $port, '--host', $host);
637 0 0         push @cmd, '-R', 'lib,templates' if $reload;
638 0           push @cmd, 'app.psgi';
639            
640 0           exec @cmd;
641             }
642              
643             sub cmd_routes {
644 0     0 0   my ($self) = @_;
645            
646 0 0         unless (-f 'app.psgi') {
647 0           say "Error: app.psgi not found";
648 0           exit 1;
649             }
650            
651 0           say "Loading routes from app.psgi...";
652 0           say "";
653            
654             # This is a simplified version - in a real implementation,
655             # we'd need to parse the app.psgi file or load the app
656 0           say "Note: Route inspection requires loading the application.";
657 0           say "This feature will be enhanced in future versions.";
658 0           say "";
659 0           say "For now, check your app.psgi file for route definitions.";
660             }
661              
662             1;
663              
664             __END__