Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/SkillManager.pm view on Meta::CPAN
return "$label (error: $error)";
}
return "$label from $path" if -f $path;
return $label;
}
# _progress_error_text($error)
# Compacts one install error into a single visible task-board line.
# Input: raw error string from one dependency or source install step.
# Output: single-line error summary string.
sub _progress_error_text {
my ( $self, $error ) = @_;
return 'unknown failure' if !defined $error || $error eq '';
$error =~ s/\s+/ /g;
$error =~ s/\A\s+//;
$error =~ s/\s+\z//;
return $error;
}
# _progress_emit($event)
# Sends one skill-install progress event to the optional progress callback.
# Input: event hash reference.
# Output: true value.
sub _progress_emit {
my ( $self, $event ) = @_;
my $progress = $self->{progress};
return 1 if !$progress || ref($progress) ne 'CODE';
$progress->($event);
return 1;
}
# _active_dependency_task_id()
# Returns the dependency-task id currently being executed so lower-level
# helpers can stream detail lines into the visible progress board.
# Input: none.
# Output: task id string or undef.
sub _active_dependency_task_id {
my ($self) = @_;
return $self->{_active_dependency_task_id};
}
# _progress_detail_line($line, %args)
# Streams one compact detail line into the active dependency task.
# Input: raw text line plus optional explicit task_id.
# Output: true value.
sub _progress_detail_line {
my ( $self, $line, %args ) = @_;
my $task_id = $args{task_id} || $self->_active_dependency_task_id || return 1;
return 1 if !defined $line;
$line =~ s/\r//g;
$line =~ s/\s+\z//;
return 1 if $line eq '';
return $self->_progress_emit(
{
task_id => $task_id,
detail_line => $line,
}
);
}
# _run_streaming_command(%args)
# Runs one external dependency command while streaming a rolling output
# snapshot into the active progress task and collecting the full transcript.
# Input: command array reference plus optional cwd and banner line.
# Output: hash reference with stdout, stderr, and exit fields.
sub _run_streaming_command {
my ( $self, %args ) = @_;
my $command = $args{command} || die "Missing command for streaming execution\n";
die "Streaming command must be an array reference\n" if ref($command) ne 'ARRAY' || !@{$command};
my $cwd = $args{cwd};
my $banner = $args{banner};
my $env = $args{env};
die "Streaming command env must be a hash reference\n" if defined $env && ref($env) ne 'HASH';
$self->_progress_detail_line($banner) if defined $banner && $banner ne '';
my $stdout_handle;
my $stderr_handle = gensym;
my $stdin_handle = gensym;
my $pid;
my $stdout = '';
my $stderr = '';
my %target_for;
my $launcher = sub {
local @ENV{ keys %{$env} } = values %{$env} if $env;
$pid = open3( $stdin_handle, $stdout_handle, $stderr_handle, @{$command} );
};
if ( defined $cwd && $cwd ne '' ) {
my $orig = Cwd::getcwd();
chdir $cwd or die "Unable to chdir to $cwd for command launch: $!";
my $ok = eval { $launcher->(); 1 };
my $error = $@;
chdir $orig or die "Unable to chdir back to $orig after command launch: $!";
die $error if !$ok;
}
else {
$launcher->();
}
close $stdin_handle if $stdin_handle;
%target_for = (
fileno($stdout_handle) => \$stdout,
fileno($stderr_handle) => \$stderr,
);
my $selector = IO::Select->new();
$selector->add($stdout_handle) if $stdout_handle;
$selector->add($stderr_handle) if $stderr_handle;
while ( my @ready = $selector->can_read ) {
for my $handle (@ready) {
my $chunk = '';
my $read = sysread( $handle, $chunk, 8192 );
if ( !defined $read || $read == 0 ) {
$selector->remove($handle);
close $handle;
next;
}
my $slot = $target_for{ fileno($handle) };
${$slot} .= $chunk if $slot;
for my $line ( split /\n/, $chunk ) {
$self->_progress_detail_line($line);
}
}
}
waitpid $pid, 0;
return {
lib/Developer/Dashboard/SkillManager.pm view on Meta::CPAN
chdir $skills_root or die "Unable to chdir to $skills_root for $manifest_name dependency install: $!";
( $stdout, $stderr, $status ) = capture {
system( 'dashboard', 'skills', 'install', $dependency );
};
chdir $cwd or die "Unable to chdir back to $cwd after $manifest_name dependency install: $!";
1;
};
my $eval_error = $@;
if ($eval_error) {
chdir $cwd if Cwd::getcwd() ne $cwd;
die $eval_error;
}
( $stdout, $stderr, $status );
};
return {
error => "Failed to install dependent skills for $skill_path via $manifest_name: $step_stderr",
} if $exit != 0;
push @stdout, $step_stdout if defined $step_stdout && $step_stdout ne '';
push @stderr, $step_stderr if defined $step_stderr && $step_stderr ne '';
}
return { success => 1, skipped => 1 } if !@stdout && !@stderr;
return {
success => 1,
stdout => join( '', @stdout ),
stderr => join( '', @stderr ),
};
}
# _install_skill_package_json($skill_path)
# Installs Node dependencies declared by package.json into the dashboard home
# node_modules directory by running npx-wrapped npm inside a private staging
# workspace and then merging the resulting dependency tree into HOME/node_modules.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_package_json {
my ( $self, $skill_path ) = @_;
my $package_json = File::Spec->catfile( $skill_path, 'package.json' );
return { success => 1, skipped => 1 } if !-f $package_json;
my @specs = $self->_package_json_dependency_specs($package_json);
return { success => 1, skipped => 1 } if !@specs;
die "Unable to chdir to " . $self->{paths}->home . " for package.json dependency install: No such file or directory\n"
if !-d $self->{paths}->home;
my $workspace_parent = File::Spec->catdir( $self->{paths}->home_runtime_root, 'cache', 'node-package-installs' );
my $target_root = File::Spec->catdir( $self->{paths}->home, 'node_modules' );
make_path($workspace_parent) if !-d $workspace_parent;
make_path($target_root) if !-d $target_root;
my $workspace = tempdir( 'npm-install-XXXXXX', DIR => $workspace_parent, CLEANUP => 1 );
my $workspace_package_json = File::Spec->catfile( $workspace, 'package.json' );
open my $workspace_fh, '>', $workspace_package_json or die "Unable to write $workspace_package_json: $!";
print {$workspace_fh} encode_json(
{
name => 'developer-dashboard-skill-runtime',
version => '1.0.0',
private => JSON::XS::true(),
}
);
close $workspace_fh;
my $run = $self->_run_streaming_command(
command => [ 'npx', '--yes', 'npm', 'install', @specs ],
cwd => $workspace,
banner => "Installing Node dependencies for " . basename($skill_path) . " from $package_json: " . join( ' ', @specs ),
);
my ( $npm_stdout, $npm_stderr, $npm_exit ) = @{$run}{qw(stdout stderr exit)};
return {
error => "Failed to install skill Node dependencies for $skill_path: $npm_stderr",
} if $npm_exit != 0;
my $workspace_modules = File::Spec->catdir( $workspace, 'node_modules' );
my ( $copy_stdout, $copy_stderr, $copy_exit ) = ( '', '', 0 );
if ( -d $workspace_modules ) {
my $copy_error = eval {
$self->_copy_tree_contents( $workspace_modules, $target_root );
1;
} ? '' : "$@";
$copy_error =~ s/\s+\z// if defined $copy_error;
return {
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 {
lib/Developer/Dashboard/SkillManager.pm view on Meta::CPAN
$self->_progress_emit( { task_id => $task_id, status => 'running', label => "Install/update $source" } )
if $args{progress};
my $result = $self->_install_to_skills_root( $source, $skills_root );
if ( $result->{error} ) {
$self->_progress_emit( { task_id => $task_id, status => 'failed', label => "Install/update $source" } )
if $args{progress};
return $result;
}
push @{$operations}, {
manifest => $manifest_name,
source => $source,
repo_name => $result->{repo_name},
path => $result->{path},
version_before => $result->{version_before},
version_after => $result->{version_after},
status => $result->{status},
changed => $result->{changed},
} if ref($operations) eq 'ARRAY';
$self->_progress_emit( { task_id => $task_id, status => 'done', label => $self->_install_result_progress_label($source, $result) } )
if $args{progress};
$idx++;
}
return { success => 1 };
}
# _install_result_progress_label($source, $result)
# Builds a concise source-level completion label for an install result.
# Input: source string and install result hash reference.
# Output: operator-facing progress label with status and version transition.
sub _install_result_progress_label {
my ( $self, $source, $result ) = @_;
my $repo = ref($result) eq 'HASH' && $result->{repo_name} ? $result->{repo_name} : $source;
my $status = ref($result) eq 'HASH' && $result->{status} ? $result->{status} : 'done';
my $before = ref($result) eq 'HASH' && defined $result->{version_before} ? $result->{version_before} : '-';
my $after = ref($result) eq 'HASH' && defined $result->{version_after} ? $result->{version_after} : '-';
return "$repo $status ($before -> $after)";
}
# _install_skill_aptfile($skill_path)
# Installs aptfile packages on Debian-like hosts after printing the requested
# package list.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_aptfile {
my ( $self, $skill_path ) = @_;
my @apt_packages = $self->_skill_apt_packages($skill_path);
return { success => 1, skipped => 1 } if !@apt_packages || !$self->_is_debian_like;
my @missing_packages = $self->_packages_missing(
sub { $self->_apt_package_is_installed( $_[0] ) },
@apt_packages
);
return {
success => 1,
skipped => 1,
skip_reason => 'all aptfile packages already installed',
} if !@missing_packages;
my $aptfile = File::Spec->catfile( $skill_path, 'aptfile' );
my @runner_prefix = $self->_skill_package_runner_prefix;
my $run = $self->_run_streaming_command(
command => [ @runner_prefix, 'apt-get', 'install', '-y', @missing_packages ],
banner => "Installing apt packages for " . basename($skill_path) . " from $aptfile: " . join( ' ', @missing_packages ),
);
return {
error => "Failed to install skill apt dependencies for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
return {
success => 1,
stdout => $run->{stdout},
stderr => $run->{stderr},
};
}
# _install_skill_apkfile($skill_path)
# Installs apkfile packages on Alpine hosts after printing the requested
# package list.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_apkfile {
my ( $self, $skill_path ) = @_;
my $apkfile = File::Spec->catfile( $skill_path, 'apkfile' );
my @packages = $self->_dependency_file_lines($apkfile);
return { success => 1, skipped => 1 } if !@packages || !$self->_is_alpine;
my @missing_packages = $self->_packages_missing(
sub { $self->_apk_package_is_installed( $_[0] ) },
@packages
);
return {
success => 1,
skipped => 1,
skip_reason => 'all apkfile packages already installed',
} if !@missing_packages;
my @runner_prefix = $self->_skill_package_runner_prefix;
my $run = $self->_run_streaming_command(
command => [ @runner_prefix, 'apk', 'add', '--no-cache', @missing_packages ],
banner => "Installing apk packages for " . basename($skill_path) . " from $apkfile: " . join( ' ', @missing_packages ),
);
return {
error => "Failed to install skill apk dependencies for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
return {
success => 1,
stdout => $run->{stdout},
stderr => $run->{stderr},
};
}
# _install_skill_dnfile($skill_path)
# Installs dnfile packages on Fedora hosts after printing the requested
# package list.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_dnfile {
my ( $self, $skill_path ) = @_;
my $dnfile = File::Spec->catfile( $skill_path, 'dnfile' );
my @packages = $self->_dependency_file_lines($dnfile);
return { success => 1, skipped => 1 } if !@packages || !$self->_is_fedora;
my @missing_packages = $self->_packages_missing(
sub { $self->_dnf_package_is_installed( $_[0] ) },
@packages
);
return {
success => 1,
skipped => 1,
skip_reason => 'all dnfile packages already installed',
} if !@missing_packages;
my @runner_prefix = $self->_skill_package_runner_prefix;
my $run = $self->_run_streaming_command(
command => [ @runner_prefix, 'dnf', 'install', '-y', @missing_packages ],
banner => "Installing dnf packages for " . basename($skill_path) . " from $dnfile: " . join( ' ', @missing_packages ),
);
return {
error => "Failed to install skill dnf dependencies for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
return {
success => 1,
stdout => $run->{stdout},
stderr => $run->{stderr},
};
}
# _install_skill_wingetfile($skill_path)
# Installs wingetfile packages on Windows hosts after printing the requested
# package list.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_wingetfile {
my ( $self, $skill_path ) = @_;
my $wingetfile = File::Spec->catfile( $skill_path, 'wingetfile' );
my @packages = $self->_dependency_file_lines($wingetfile);
return { success => 1, skipped => 1 } if !@packages || !$self->_is_windows;
my @stdout;
my @stderr;
for my $package (@packages) {
my $run = $self->_run_streaming_command(
command => [
'winget', 'install',
'--id', $package,
'--exact',
'--accept-package-agreements',
'--accept-source-agreements',
'--disable-interactivity',
],
banner => "Installing winget packages for " . basename($skill_path) . " from $wingetfile: $package",
);
return {
error => "Failed to install skill winget dependencies for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
push @stdout, $run->{stdout} if defined $run->{stdout} && $run->{stdout} ne '';
push @stderr, $run->{stderr} if defined $run->{stderr} && $run->{stderr} ne '';
}
return {
success => 1,
stdout => join( '', @stdout ),
stderr => join( '', @stderr ),
};
}
# _skill_package_runner_prefix()
# Returns the command prefix used for privileged package-manager installs.
# Input: none.
# Output: list containing 'sudo' for non-root users, or an empty list for root.
sub _skill_package_runner_prefix {
my ($self) = @_;
return () if ( $> || 0 ) == 0;
return ('sudo');
}
# _install_skill_brewfile($skill_path)
# Installs brewfile packages on macOS after printing the requested package list.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_brewfile {
my ( $self, $skill_path ) = @_;
my $brewfile = File::Spec->catfile( $skill_path, 'brewfile' );
my @packages = $self->_dependency_file_lines($brewfile);
return { success => 1, skipped => 1 } if !@packages || $self->_current_os ne 'darwin';
my $run = $self->_run_streaming_command(
command => [ 'brew', 'install', @packages ],
banner => "Installing brew packages for " . basename($skill_path) . " from $brewfile: " . join( ' ', @packages ),
);
return {
error => "Failed to install skill brew dependencies for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
return {
success => 1,
stdout => $run->{stdout},
stderr => $run->{stderr},
};
}
# _install_skill_cpanfile($skill_path)
# Installs shared Perl dependencies from cpanfile into HOME perl5.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_cpanfile {
my ( $self, $skill_path ) = @_;
my $cpanfile = File::Spec->catfile( $skill_path, 'cpanfile' );
return { success => 1, skipped => 1 } if !-f $cpanfile;
my $shared_root = $self->_ensure_perl_root( $self->_shared_perl_root );
my $run = $self->_run_streaming_command(
command => [ 'cpanm', '--notest', '-L', $shared_root, '--cpanfile', $cpanfile, '--installdeps', '.' ],
cwd => $skill_path,
env => $self->_skill_cpanm_env,
banner => "Installing Perl dependencies for " . basename($skill_path) . " from $cpanfile",
);
my ( $stdout, $stderr, $exit ) = @{$run}{qw(stdout stderr exit)};
return {
error => "Failed to install skill dependencies for $skill_path: $stderr",
} if $exit != 0;
return {
success => 1,
stdout => $stdout,
stderr => $stderr,
};
}
# _install_skill_cpanfile_local($skill_path)
# Installs skill-local Perl dependencies from cpanfile.local into ./perl5.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_cpanfile_local {
my ( $self, $skill_path ) = @_;
my $cpanfile_local = File::Spec->catfile( $skill_path, 'cpanfile.local' );
return { success => 1, skipped => 1 } if !-f $cpanfile_local;
my $local_root = $self->_ensure_perl_root( $self->_skill_local_perl_root($skill_path) );
my $run = $self->_run_streaming_command(
command => [ 'cpanm', '--notest', '-L', $local_root, '--cpanfile', $cpanfile_local, '--installdeps', '.' ],
cwd => $skill_path,
env => $self->_skill_cpanm_env,
banner => "Installing local Perl dependencies for " . basename($skill_path) . " from $cpanfile_local",
);
my ( $stdout, $stderr, $exit ) = @{$run}{qw(stdout stderr exit)};
return {
error => "Failed to install skill local dependencies for $skill_path: $stderr",
} if $exit != 0;
return {
success => 1,
stdout => $stdout,
stderr => $stderr,
};
}
# _skill_cpanm_env()
# Returns the non-interactive CPAN environment required for skill dependency installs.
# Input: none.
# Output: hash reference of environment variables for cpanm child processes.
sub _skill_cpanm_env {
my ($self) = @_;
return {
PERL_MM_USE_DEFAULT => 1,
NONINTERACTIVE_TESTING => 1,
PERL_CANARY_STABILITY_NOPROMPT => 1,
};
}
# _install_skill_makefile($skill_path)
# Runs the optional skill Makefile command chain before ddfile processing.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_makefile {
my ( $self, $skill_path ) = @_;
my $makefile = File::Spec->catfile( $skill_path, 'Makefile' );
return { success => 1, skipped => 1 } if !-f $makefile;
my $make = command_in_path('make') || 'make';
my %targets = map { $_ => 1 } $self->_makefile_targets($makefile);
my @commands = (
[],
( $self->{skip_tests}
? ()
: $targets{test}
? ( ['test'] )
: $targets{tests}
? ( ['tests'] )
: () ),
['install'],
( $targets{clean} ? ( ['clean'] ) : () ),
);
my ( @stdout, @stderr );
my $result = eval {
for my $args (@commands) {
my $target_name = @{$args} ? join( ' ', @{$args} ) : 'default';
my $run = $self->_run_streaming_command(
command => [ $make, @{$args} ],
cwd => $skill_path,
banner => "Running make $target_name for " . basename($skill_path) . " from $makefile",
);
my ( $stdout, $stderr, $exit ) = @{$run}{qw(stdout stderr exit)};
push @stdout, $stdout if defined $stdout && $stdout ne '';
push @stderr, $stderr if defined $stderr && $stderr ne '';
if ( $exit != 0 ) {
my $target = 'default';
if (@{$args}) {
$target = join( ' ', @{$args} );
}
my $detail = $stderr ne '' ? $stderr : $stdout;
die "Failed to run skill Makefile target '$target' for $skill_path: $detail";
}
}
return {
success => 1,
stdout => join( '', @stdout ),
stderr => join( '', @stderr ),
};
};
my $error = $@;
return { error => $error } if !$result;
return $result;
}
# _install_skill_dockerfile($skill_path)
# Builds Docker images declared by dockerfile in the skill root.
# Input: absolute skill root directory path.
# Output: result hash reference with success or error state.
sub _install_skill_dockerfile {
my ( $self, $skill_path ) = @_;
my $dockerfile = File::Spec->catfile( $skill_path, 'dockerfile' );
return { success => 1, skipped => 1 } if !-f $dockerfile;
my $run = $self->_run_streaming_command(
command => [ 'docker', 'build', '-t', lc(basename($skill_path)), '-f', $dockerfile, $skill_path ],
cwd => $skill_path,
banner => "Building Docker image for " . basename($skill_path) . " from $dockerfile",
);
return {
error => "Failed to build Docker image for $skill_path: $run->{stderr}",
} if $run->{exit} != 0;
return {
success => 1,
stdout => $run->{stdout},
stderr => $run->{stderr},
};
}
# _makefile_targets($makefile)
# Extracts simple top-level target names from one skill Makefile.
# Input: absolute Makefile path.
# Output: list of target name strings.
sub _makefile_targets {
my ( $self, $makefile ) = @_;
return () if !defined $makefile || !-f $makefile;
open my $fh, '<', $makefile or die "Unable to read $makefile: $!";
my %seen;
my @targets;
while ( my $line = <$fh> ) {
next if $line =~ /^\s/;
next if $line =~ /^\#/;
next if $line =~ /^\./;
next if $line !~ /^([^:=]+)\s*:(?![=])/;
for my $target ( split /\s+/, $1 ) {
next if !defined $target || $target eq '';
next if $seen{$target}++;
push @targets, $target;
}
}
close $fh or die "Unable to close $makefile: $!";
return @targets;
}
# _skill_metadata($repo_name, $skill_path)
# Summarizes the isolated filesystem and command surface for one installed skill.
# Input: repo name string and absolute skill root directory path.
# Output: metadata hash reference.
sub _skill_metadata {
my ( $self, $repo_name, $skill_path ) = @_;
my $cli_root = File::Spec->catdir( $skill_path, 'cli' );
my @commands = map { $_->{name} } @{ $self->_cli_command_details($skill_path) };
my $docker_root = File::Spec->catdir( $skill_path, 'config', 'docker' );
my @docker_services = map { $_->{name} } @{ $self->_docker_service_details($skill_path) };
my $pages = $self->_page_details($skill_path);
my $collectors = $self->_collector_details( $repo_name, $skill_path );
my $indicators_count = 0;
for my $collector ( @{$collectors} ) {
$indicators_count++ if $collector->{has_indicator};
}
my $enabled = $self->_skill_disabled($skill_path) ? JSON::XS::false() : JSON::XS::true();
my $has_ddfile = -f File::Spec->catfile( $skill_path, 'ddfile' ) ? JSON::XS::true() : JSON::XS::false();
my $has_aptfile = -f File::Spec->catfile( $skill_path, 'aptfile' ) ? JSON::XS::true() : JSON::XS::false();
( run in 0.713 second using v1.01-cache-2.11-cpan-995e09ba956 )