NAME
Punk::Command - the punk command line: registry, dispatcher and commands
SYNOPSIS
punk new MyApp # generate a new application
punk routes # the compiled routing table
punk api sync # stubs for operations added to the spec
punk config check # resolve configuration and every secret
punk doctor # versions, C ABIs, application health
punk console # a REPL with the application compiled
punk dev # Hyperman with restart-on-change
punk serve ./public # a directory of files, no application
DESCRIPTION
Punk resolves and freezes everything at to_app, which makes a running application fast and opaque in equal measure. These commands open it up - what the router holds, what the specification maps to, what the configuration resolved to, and which C ABIs are in play.
Except for new and serve, each finds the application by walking up from the current directory looking for app.psgi, and takes the class from that file's Class->to_app.
bin/punk is two lines over Punk::Command->main(@ARGV). Commands are specs in a registry; option parsing, generated help and the exit-code contract (0 success, 1 application or environment failure, 2 misuse) live in the dispatcher, so punk help X, punk X --help and every misuse path read one declaration and cannot drift.
THE REGISTRY
Punk::Command->register(greet => {
abstract => 'one line for the command list',
usage => '[options]',
desc => 'a paragraph for the help page',
options => [ { spec => 'port=i', arg => 'N',
doc => 'listen port', default => 5000 } ],
commands => { sync => \%subspec }, # nested verbs
code => sub { my ($opt, @args) = @_; ...; return 0 },
}, __PACKAGE__);
Two owners claiming one name croak naming both. Registration order is display order. A command body signals misuse with die { usage_error => $msg } (message, usage, exit 2); any other die becomes punk <cmd>: msg and exit 1.
An unknown command X gets one attempt at require Punk::Command::<ucfirst X>, whose load-time job is to call register - that is how a plugin distribution adds punk queue ... when it is installed, with no scanning and no cost on the common path.
register_doctor($label => \&probe) adds a row to the doctor report the same way.
Options resolved at run time
extra_options => sub {
my (@argv) = @_;
return ( { spec => 'shout', doc => '...', owner => $class,
section => 'kit options (diy)' } );
},
A command that cannot know all its options at registration - new, whose --kit names a generator with options of its own - declares them through a callback, run over the raw argv before Getopt. What it returns parses and appears in help like the command's own, under the heading its section gives, which is what keeps generated help matching what is actually accepted. An option that collides with one the command declared croaks naming both.
LOADING THE APPLICATION
my $app = Punk::Command->load_app(dir => $opt->{dir}, chdir => 1);
my $users = $app->{registrar}->model_instance('User');
For a command in another distribution that needs the application itself and not just its files. Loads it through its own app.psgi - to_app and all, so the compiled tables are there - and returns root, class, psgi and registrar. Dies with one line on failure, which a command body's die already turns into a prefixed message and exit 1.
app.psgi changes directory to the application root, and everything relative in punk.yml is written for that. chdir => 1 leaves the process there, which is what anything going on to open a relative dsn needs; without it the caller's directory is restored.
TESTING
local $Punk::Command::OUT = $out_fh; # replaces STDOUT
local $Punk::Command::ERR = $err_fh; # replaces STDERR
my $code = Punk::Command->main(@argv);
main returns the exit code and honours the two filehandle globals, so a test gets code, stdout and stderr with no process spawn.
COMMANDS
new
Generates a running application; see Punk::Generate. With --sqitch ENGINE it also initialises a Sqitch project for the schema through Punk::Command::Sqitch (the Punk-Sqitch distribution), so the first model's table can be punk sqitch add users --model User; the engine is asked for because the generated punk.yml ships its database block commented out.
--kit NAME generates through Punk::Kit::<Name> instead of the basic skeleton, the same probe by name the command seam above uses. A kit declares its own options, so punk new --kit NAME --help lists them under the command's own; "KITS" in Punk::Generate is how one is written. A kit that writes its own sqitch.plan makes --sqitch a no-op rather than having Sqitch refuse a project that is already correct.
generate controller / generate model
Adds a controller or a Punk::Model class to an existing application - new scaffolds once, these add to it. The application class is read from app.psgi without executing anything, so a broken controller cannot stop you generating the fix. An existing file is refused without --force.
test
Runs prove -lr in the application root; extra arguments go to prove verbatim. --env sets PUNK_ENV for the run.
secret
One line of random key material (url-safe base64, or --hex) - the bytes punk.yml's session secret wants behind its $env reference. Nothing else is printed, so it pipes.
version
What punk --version prints: $Punk::VERSION, the only version there is.
routes
Prints method, path, target and guard count for every route, including API operations (from their mount, since the specification is the routing table there), websocket routes and mounted applications. Targets are recovered from the compiled coderef, so what you see is what the route resolved to rather than what was declared.
api sync
Adds a stub for every operation the specification declares that the controllers do not implement, and leaves everything else alone. Methods with no matching operation are reported, never removed. --dry-run reports without writing.
config check
Resolves config/punk.yml for an environment, reporting every $env, $file and $exec reference independently - so one missing secret does not hide the others - then loads it properly to apply the guardrail. Exits non-zero when anything failed, which makes it usable as a deployment gate.
doctor
Versions, C ABI resolution and, if run inside an application, its route and configuration counts. Plugin rows join via "THE REGISTRY"'s register_doctor.
console
A REPL with $app (the registrar), $psgi and $c (a throwaway context) in scope.
dev
Runs the application under Hyperman and restarts it when anything under lib/, config/ or root/templates/ changes. A compiled-at-boot application cannot hot-reload - there is no live structure to patch - so restarting is the honest implementation.
serve
punk serve
punk serve ./public --port 3000
punk serve --host 0.0.0.0 --list
Serves a directory of files under Hyperman, on 127.0.0.1:8000 by default. dev's twin, and its opposite: this is the one command besides new that never looks for an application. The whole server is Punk::Static->app, so what it serves and how is Punk::Static - content types, ETag, If-Modified-Since, ranges, precompressed .gz siblings and the traversal guard, all of it.
A directory resolves to its index.html (--index NAME for another name, --no-index for the plain 404), and --list renders a listing for a directory that has none. Hyperman's C access log goes to stderr unless --quiet.
There is no watch-and-restart loop, unlike dev: a file is read on the request that asks for it, so there is nothing compiled to invalidate.
SEE ALSO
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)