Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/SkillManager.pm view on Meta::CPAN
error => "Failed to merge skill Node dependencies into $target_root for $skill_path: $copy_error",
} if $copy_error ne '';
}
return {
success => 1,
stdout => $npm_stdout . $copy_stdout,
stderr => $npm_stderr . $copy_stderr,
};
}
# _install_skill_requirements_txt($skill_path)
# Installs Python dependencies declared by requirements.txt into the current
# user's Python environment through python -m pip --user.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_requirements_txt {
my ( $self, $skill_path ) = @_;
my $requirements = File::Spec->catfile( $skill_path, 'requirements.txt' );
return { success => 1, skipped => 1 } if !-f $requirements;
my $run = $self->_run_streaming_command(
command => [ $self->_python_dependency_command, '-m', 'pip', 'install', '--user', '--requirement', $requirements ],
cwd => $skill_path,
banner => "Installing Python dependencies for " . basename($skill_path) . " from $requirements",
);
my ( $stdout, $stderr, $exit ) = @{$run}{qw(stdout stderr exit)};
return {
error => "Failed to install skill Python dependencies for $skill_path: $stderr",
} if $exit != 0;
return {
success => 1,
stdout => $stdout,
stderr => $stderr,
};
}
# _python_dependency_command()
# Resolves the preferred Python executable for requirements.txt installs.
# Input: none.
# Output: executable path or command name string.
sub _python_dependency_command {
return command_in_path('python') || command_in_path('python3') || 'python';
}
# _package_json_dependency_specs($package_json)
# Extracts one deterministic dependency install list from package.json without
# asking npm to treat the skill checkout itself as an installable package.
# Input: absolute package.json path.
# Output: ordered list of npm install spec strings.
sub _package_json_dependency_specs {
my ( $self, $package_json ) = @_;
return () if !defined $package_json || !-f $package_json;
open my $fh, '<', $package_json or die "Unable to read $package_json: $!";
local $/;
my $content = <$fh>;
close $fh;
my $decoded = eval { decode_json($content) };
die "Unable to parse $package_json: $@" if !$decoded || $@;
my @specs;
for my $section ( qw(dependencies devDependencies optionalDependencies peerDependencies) ) {
my $entries = $decoded->{$section};
next if ref($entries) ne 'HASH';
for my $name ( sort keys %{$entries} ) {
my $version = $entries->{$name};
push @specs, defined $version && $version ne '' ? "$name\@$version" : $name;
}
}
return @specs;
}
# _copy_tree_contents($source_root, $target_root)
# Recursively copies the contents of one directory tree into another without
# relying on external shell utilities.
# Input: absolute source directory path and absolute target directory path.
# Output: true value after the copy succeeds, or dies on the first copy error.
sub _copy_tree_contents {
my ( $self, $source_root, $target_root ) = @_;
die "Missing source tree for copy\n" if !defined $source_root || $source_root eq '';
die "Missing target tree for copy\n" if !defined $target_root || $target_root eq '';
die "Source tree $source_root does not exist\n" if !-d $source_root;
make_path($target_root) if !-d $target_root;
find(
{
no_chdir => 1,
wanted => sub {
my $source = $File::Find::name;
return if $source eq $source_root;
my $relative = File::Spec->abs2rel( $source, $source_root );
my $target = File::Spec->catfile( $target_root, $relative );
if ( -d $source ) {
make_path($target) if !-d $target;
return;
}
my ( undef, $target_dir ) = File::Spec->splitpath($target);
make_path($target_dir) if defined $target_dir && $target_dir ne '' && !-d $target_dir;
copy( $source, $target ) or die "Unable to copy $source to $target: $!";
my $mode = ( stat $source )[2];
chmod( $mode & 07777, $target ) if defined $mode && -f $target;
},
},
$source_root
);
return 1;
}
# _install_manifest_file($manifest_path, %args)
# Installs every source listed in one explicit ddfile-style manifest into the
# requested skills root without skipping already-installed targets.
# Input: manifest file path plus manifest_name, skills_root, and operations array reference.
# Output: success hash ref when the manifest is absent or completes, or an error hash.
sub _install_manifest_file {
my ( $self, $manifest_path, %args ) = @_;
return { success => 1, skipped => 1 } if !defined $manifest_path || !-f $manifest_path;
my $manifest_name = $args{manifest_name} || basename($manifest_path);
lib/Developer/Dashboard/SkillManager.pm view on Meta::CPAN
my $docker_root = File::Spec->catdir( $skill_path, 'config', 'docker' );
my @services;
if ( -d $docker_root ) {
opendir( my $dh, $docker_root ) or die "Unable to read $docker_root: $!";
for my $entry (
sort grep {
$_ ne '.' && $_ ne '..' && -d File::Spec->catdir( $docker_root, $_ )
} readdir($dh)
)
{
my $service_root = File::Spec->catdir( $docker_root, $entry );
my @service_files;
for my $file ( $self->_sorted_files($service_root) ) {
push @service_files, File::Spec->catfile( $service_root, $file );
}
push @services, {
name => $entry,
root => $service_root,
files => \@service_files,
};
}
closedir($dh);
}
return \@services;
}
# _collector_details($repo_name, $skill_path)
# Enumerates collectors declared in one skill config and derives indicator metadata.
# Input: skill repo name and absolute skill root directory path.
# Output: array reference of collector metadata hashes.
sub _collector_details {
my ( $self, $repo_name, $skill_path ) = @_;
my $config = $self->_read_skill_config_file($skill_path);
my $collectors = $config->{collectors};
return [] if ref($collectors) ne 'ARRAY';
my @items;
for my $job ( @{$collectors} ) {
next if ref($job) ne 'HASH';
next if !defined $job->{name} || $job->{name} eq '';
my $qualified_name = $job->{name} =~ /^\Q$repo_name\E\./ ? $job->{name} : $repo_name . '.' . $job->{name};
my $indicator = ref( $job->{indicator} ) eq 'HASH' ? $job->{indicator} : {};
my $has_indicator = %{$indicator} ? JSON::XS::true() : JSON::XS::false();
my %item = (
name => $job->{name},
qualified_name => $qualified_name,
command => $job->{command},
cwd => $job->{cwd},
schedule => $job->{schedule},
interval => $job->{interval},
has_indicator => $has_indicator,
indicator => $indicator,
);
push @items, \%item;
}
return \@items;
}
# _read_skill_config_file($skill_path)
# Reads one skill config/config.json file directly.
# Input: absolute skill root directory path.
# Output: decoded hash reference or empty hash reference on invalid/missing JSON.
sub _read_skill_config_file {
my ( $self, $skill_path ) = @_;
my $config_file = File::Spec->catfile( $skill_path, 'config', 'config.json' );
return {} if !-f $config_file;
open my $fh, '<', $config_file or return {};
local $/;
my $json_text = <$fh>;
close $fh;
my $config = eval { decode_json($json_text) };
return ref($config) eq 'HASH' ? $config : {};
}
# _sorted_files($root)
# Lists plain files under one directory in deterministic sorted order.
# Input: directory path string.
# Output: sorted list of child file names.
sub _sorted_files {
my ( $self, $root ) = @_;
return () if !$root || !-d $root;
opendir( my $dh, $root ) or die "Unable to read $root: $!";
my @files = sort grep {
$_ ne '.'
&& $_ ne '..'
&& -f File::Spec->catfile( $root, $_ )
} readdir($dh);
closedir($dh);
return @files;
}
# _skill_disabled($skill_path)
# Checks whether one installed skill has been disabled locally.
# Input: absolute skill root directory path.
# Output: boolean true when the disabled marker exists.
sub _skill_disabled {
my ( $self, $skill_path ) = @_;
return -f $self->_disabled_marker_path($skill_path) ? 1 : 0;
}
# _disabled_marker_path($skill_path)
# Returns the filesystem path of the skill-disabled marker file.
# Input: absolute skill root directory path.
# Output: marker file path string.
sub _disabled_marker_path {
my ( $self, $skill_path ) = @_;
return File::Spec->catfile( $skill_path, '.disabled' );
}
1;
__END__
=pod
=head1 NAME
Developer::Dashboard::SkillManager - manage installed dashboard skills
=head1 SYNOPSIS
use Developer::Dashboard::SkillManager;
( run in 1.284 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )