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 )