Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/SkillDispatcher.pm view on Meta::CPAN
# dispatch($skill_name, $command, @args)
# Executes a command from an installed skill.
# Input: skill repo name, command name, and command arguments.
# Output: command output or error hash.
sub dispatch {
my ( $self, $skill_name, $command, @args ) = @_;
return { error => 'Missing skill name' } if !$skill_name;
return { error => 'Missing command name' } if !$command;
my $skill_path = $self->{manager}->get_skill_path( $skill_name, include_disabled => 1 );
my $suggest = Developer::Dashboard::CLI::Suggest->new(
paths => $self->{manager}{paths},
manager => $self->{manager},
);
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$skill_path;
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$self->{manager}->is_enabled($skill_name);
my $command_spec = $self->_command_spec( $skill_name, $command );
my $cmd_path = $command_spec ? $command_spec->{cmd_path} : undef;
my $command_skill_path = $command_spec ? $command_spec->{skill_path} : undef;
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$cmd_path;
my $hook_result = $self->execute_hooks( $skill_name, $command, @args );
return $hook_result if $hook_result->{error};
my @skill_layers = $command_spec ? @{ $command_spec->{skill_layers} || [] } : $self->_skill_layers($skill_name);
my %env = $self->_skill_env(
skill_name => $skill_name,
skill_path => $command_skill_path || $skill_path,
skill_layers => \@skill_layers,
command => $command_spec ? $command_spec->{command_name} : $command,
result_state => $hook_result->{result_state} || {},
);
my @command = command_argv_for_path($cmd_path);
my ( $stdout, $stderr, $exit ) = capture {
local %ENV = ( %ENV, %env );
Developer::Dashboard::Runtime::Result::set_current( $hook_result->{result_state} || {} );
if ( ref( $hook_result->{last_result} ) eq 'HASH' && %{ $hook_result->{last_result} } ) {
Developer::Dashboard::Runtime::Result::set_last_result( $hook_result->{last_result} );
}
else {
Developer::Dashboard::Runtime::Result::clear_last_result();
}
Developer::Dashboard::EnvLoader->load_runtime_layers( paths => $self->{manager}{paths} );
Developer::Dashboard::EnvLoader->load_skill_layers( skill_layers => \@skill_layers );
system( @command, @args );
};
my $hook_stdout = join '', map { defined $_->{stdout} ? $_->{stdout} : '' } values %{ $hook_result->{hooks} || {} };
my $hook_stderr = join '', map { defined $_->{stderr} ? $_->{stderr} : '' } values %{ $hook_result->{hooks} || {} };
return {
stdout => $hook_stdout . $stdout,
stderr => $hook_stderr . $stderr,
exit_code => $exit,
hooks => $hook_result->{hooks} || {},
};
}
# exec_command($skill_name, $command, @args)
# Executes one skill command by streaming hooks first and then replacing the
# current helper process with the resolved skill command so interactive stdin,
# stdout, and stderr behave exactly like a direct invocation.
# Input: skill repo name, command name, and command arguments.
# Output: never returns on success; otherwise returns an error hash.
sub exec_command {
my ( $self, $skill_name, $command, @args ) = @_;
return { error => 'Missing skill name' } if !$skill_name;
return { error => 'Missing command name' } if !$command;
my $skill_path = $self->{manager}->get_skill_path( $skill_name, include_disabled => 1 );
my $suggest = Developer::Dashboard::CLI::Suggest->new(
paths => $self->{manager}{paths},
manager => $self->{manager},
);
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$skill_path;
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$self->{manager}->is_enabled($skill_name);
my $command_spec = $self->_command_spec( $skill_name, $command );
my $cmd_path = $command_spec ? $command_spec->{cmd_path} : undef;
my $command_skill_path = $command_spec ? $command_spec->{skill_path} : undef;
return { error => $suggest->unknown_skill_command_message( $skill_name, $command ) } if !$cmd_path;
my @skill_layers = $command_spec ? @{ $command_spec->{skill_layers} || [] } : $self->_skill_layers($skill_name);
my $hook_result = $self->_execute_hooks_streaming( $skill_name, $command_spec ? $command_spec->{command_name} : $command, \@skill_layers, @args );
return $hook_result if $hook_result->{error};
my %env = $self->_skill_env(
skill_name => $skill_name,
skill_path => $command_skill_path || $skill_path,
skill_layers => \@skill_layers,
command => $command_spec ? $command_spec->{command_name} : $command,
result_state => $hook_result->{result_state} || {},
);
my @command = command_argv_for_path($cmd_path);
%ENV = ( %ENV, %env );
Developer::Dashboard::Runtime::Result::set_current( $hook_result->{result_state} || {} );
if ( ref( $hook_result->{last_result} ) eq 'HASH' && %{ $hook_result->{last_result} } ) {
Developer::Dashboard::Runtime::Result::set_last_result( $hook_result->{last_result} );
}
else {
Developer::Dashboard::Runtime::Result::clear_last_result();
}
Developer::Dashboard::EnvLoader->load_runtime_layers( paths => $self->{manager}{paths} );
Developer::Dashboard::EnvLoader->load_skill_layers( skill_layers => \@skill_layers );
return $self->_exec_resolved_command( $cmd_path, \@command, \@args );
}
# execute_hooks($skill_name, $command, @args)
# Executes hook files from skill's cli/<command>.d/ directory before main command.
# Input: skill repo name, command name, and command arguments.
# Output: hash with hook results and environment.
sub execute_hooks {
my ( $self, $skill_name, $command, @args ) = @_;
return { hooks => {}, result_state => {} } if !$skill_name || !$command;
my $skill_path = $self->{manager}->get_skill_path( $skill_name, include_disabled => 1 );
return { hooks => {}, result_state => {} } if !$skill_path;
return { hooks => {}, result_state => {} } if !$self->{manager}->is_enabled($skill_name);
my $command_spec = $self->_command_spec( $skill_name, $command );
my @skill_layers = $command_spec ? @{ $command_spec->{skill_layers} || [] } : $self->_skill_layers($skill_name);
return { hooks => {}, result_state => {} } if !@skill_layers;
my $resolved_command = $command_spec ? $command_spec->{command_name} : $command;
my %results;
my $last_result = {};
for my $layer_path (@skill_layers) {
my $hooks_dir = File::Spec->catdir( $layer_path, 'cli', "$resolved_command.d" );
next if !-d $hooks_dir;
opendir( my $dh, $hooks_dir ) or die "Unable to read $hooks_dir: $!";
for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir($dh) ) {
my $hook_path = File::Spec->catfile( $hooks_dir, $entry );
next unless is_runnable_file($hook_path);
my %env = $self->_skill_env(
skill_name => $skill_name,
skill_path => $layer_path,
skill_layers => \@skill_layers,
command => $resolved_command,
result_state => \%results,
);
my @command = command_argv_for_path($hook_path);
my ( $stdout, $stderr, $exit ) = capture {
local %ENV = ( %ENV, %env );
Developer::Dashboard::Runtime::Result::set_current( \%results );
if (%{$last_result}) {
Developer::Dashboard::Runtime::Result::set_last_result($last_result);
}
else {
Developer::Dashboard::Runtime::Result::clear_last_result();
}
Developer::Dashboard::EnvLoader->load_runtime_layers( paths => $self->{manager}{paths} );
Developer::Dashboard::EnvLoader->load_skill_layers( skill_layers => \@skill_layers );
system( @command, @args );
};
my $result_key = $entry;
if ( exists $results{$entry} ) {
my $leaf = basename( dirname($hook_path) );
$result_key = $leaf . '/' . basename($hook_path);
}
$results{$result_key} = {
stdout => $stdout,
stderr => $stderr,
exit_code => $exit,
};
$last_result = {
file => $hook_path,
exit => $exit,
STDOUT => $stdout,
STDERR => $stderr,
};
}
closedir($dh);
}
my %payload = (
hooks => \%results,
result_state => \%results,
);
$payload{last_result} = $last_result if %{$last_result};
return \%payload;
}
# _execute_hooks_streaming($skill_name, $command, $skill_layers, @args)
# Executes skill hook files while preserving live stdio so interactive hooks
# and later main commands can still read from the caller's stdin and print
# prompts without buffering surprises.
# Input: skill repo name, resolved command name, array reference of skill layer
# paths, and command arguments.
# Output: hash reference containing hook captures, result_state, and
# last_result.
sub _execute_hooks_streaming {
my ( $self, $skill_name, $command, $skill_layers, @args ) = @_;
return { hooks => {}, result_state => {} } if !$skill_name || !$command;
my @skill_layers = @{ $self->_arrayref_or_empty($skill_layers) };
return { hooks => {}, result_state => {} } if !@skill_layers;
my %results;
my $last_result = {};
for my $layer_path (@skill_layers) {
my $hooks_dir = File::Spec->catdir( $layer_path, 'cli', "$command.d" );
next if !-d $hooks_dir;
opendir( my $dh, $hooks_dir ) or die "Unable to read $hooks_dir: $!";
for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir($dh) ) {
my $hook_path = File::Spec->catfile( $hooks_dir, $entry );
next unless is_runnable_file($hook_path);
my %env = $self->_skill_env(
skill_name => $skill_name,
skill_path => $layer_path,
skill_layers => \@skill_layers,
command => $command,
result_state => \%results,
);
my @hook_command = command_argv_for_path($hook_path);
my $run = $self->_run_child_command_streaming(
command => \@hook_command,
args => \@args,
env => \%env,
skill_layers => \@skill_layers,
result_state => \%results,
last_result => $last_result,
stdin_mode => 'null',
);
my $result_key = $entry;
if ( exists $results{$entry} ) {
my $leaf = basename( dirname($hook_path) );
$result_key = $leaf . '/' . basename($hook_path);
}
$results{$result_key} = {
stdout => $run->{stdout},
stderr => $run->{stderr},
exit_code => $run->{exit_code},
};
$last_result = {
file => $hook_path,
exit => $run->{exit_code},
STDOUT => $run->{stdout},
STDERR => $run->{stderr},
};
}
closedir($dh);
}
my %payload = (
hooks => \%results,
result_state => \%results,
);
$payload{last_result} = $last_result if %{$last_result};
return \%payload;
}
# _run_child_command_streaming(%args)
# Launches one child command with inherited stdin, streams stdout and stderr
# live, and still captures both streams for RESULT-aware callers.
# Input: hash containing command array ref, args array ref, env hash ref,
# skill_layers array ref, result_state hash ref, optional last_result hash
# ref, and optional stdin_mode string.
# Output: hash reference containing stdout, stderr, and exit_code.
sub _run_child_command_streaming {
my ( $self, %args ) = @_;
my @command = @{ $self->_arrayref_or_empty( $args{command} ) };
my @argv = @{ $self->_arrayref_or_empty( $args{args} ) };
my %env = %{ $self->_hashref_or_empty( $args{env} ) };
my @skill_layers = @{ $self->_arrayref_or_empty( $args{skill_layers} ) };
my $result_state = $self->_hashref_or_empty( $args{result_state} );
my $last_result = $args{last_result};
my $stdin_mode = $self->_defined_or_default( $args{stdin_mode}, 'inherit' );
my $stdin_spec = '<&STDIN';
my $stdin_fh;
if ( $stdin_mode eq 'null' ) {
open $stdin_fh, '<', File::Spec->devnull() or die "Unable to open " . File::Spec->devnull() . " for streaming skill hook stdin: $!";
$stdin_spec = '<&' . fileno($stdin_fh);
}
my $stderr = gensym();
my $stdout;
my ( $stdout_text, $stderr_text ) = ( '', '' );
my $pid;
{
local %ENV = ( %ENV, %env );
Developer::Dashboard::Runtime::Result::set_current($result_state);
if ( ref($last_result) eq 'HASH' && %{$last_result} ) {
Developer::Dashboard::Runtime::Result::set_last_result($last_result);
}
else {
Developer::Dashboard::Runtime::Result::clear_last_result();
}
Developer::Dashboard::EnvLoader->load_runtime_layers( paths => $self->{manager}{paths} );
Developer::Dashboard::EnvLoader->load_skill_layers( skill_layers => \@skill_layers );
$pid = open3( $stdin_spec, $stdout, $stderr, @command, @argv );
}
close $stdin_fh if $stdin_fh;
my $selector = IO::Select->new( $stdout, $stderr );
my $stdout_fd = fileno($stdout);
my $stderr_fd = fileno($stderr);
local $| = 1;
STDOUT->autoflush(1);
STDERR->autoflush(1);
while ( my @ready = $selector->can_read ) {
for my $fh (@ready) {
my $buffer = '';
my $read = sysread( $fh, $buffer, 8192 );
if ( !defined $read || $read == 0 ) {
$selector->remove($fh);
close $fh;
next;
}
if ( fileno($fh) == $stdout_fd ) {
print STDOUT $buffer;
$stdout_text .= $buffer;
next;
}
if ( fileno($fh) == $stderr_fd ) {
print STDERR $buffer;
$stderr_text .= $buffer;
next;
}
}
}
waitpid( $pid, 0 );
return {
stdout => $stdout_text,
stderr => $stderr_text,
exit_code => $? >> 8,
};
}
( run in 0.510 second using v1.01-cache-2.11-cpan-39bf76dae61 )