| 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 |
||||
| 50 | 0 | 0 | say ""; | ||||
| 51 | 0 | 0 | say "Commands:"; | ||||
| 52 | 0 | 0 | say " new |
||||
| 53 | 0 | 0 | say " generate |
||||
| 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 | |
||||||
| 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 | |
||||||
| 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 |
|
||||||
| 396 | |||||||
| 397 | |||||||
| 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__ |