Developer-Dashboard

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      publish fallback custom paths for `/app`, `/ajax`, `/js`, `/css`, and
      `/others` without breaking the smart longest-prefix parent routing
    - kept saved skill-page Ajax URL generation on the canonical custom path
      from `config/routes.json`, with Ajax custom routes defaulting to `json`
      unless an explicit route-level `type` is supplied
    - fixed browser `/system/status`, page-header status, and `dashboard ps1`
      indicator ordering so managed collector indicators now follow the
      collector array order from config instead of drifting alphabetically
    - fixed `dashboard serve logs -f` so a line appended immediately after
      the initial tail output is no longer skipped by a seek-to-end race
      before follow mode starts streaming new content

3.69  2026-05-13
    - added skill-local `dashboards/routes.json` metadata so installed skill
      Ajax handlers can publish canonical custom paths, optional fallback
      aliases, and default response types such as `json`, `html`, or raw
      mime strings without breaking the smart `/ajax/<repo>/...` resolver
    - fixed saved skill-page Ajax URL generation so `Ajax(file => ...)`
      emits the declared canonical custom route when route metadata exists,
      while the longest-prefix smart `/ajax/...` route remains the primary
      resolver and custom aliases are fallback-only before a normal `404`

Changes  view on Meta::CPAN

      window, forcing a visible operator follow-up instead of silent churn

3.64  2026-05-08
    - fixed macOS Terminal.app shell bootstrap to source /etc/bashrc_Apple_Terminal
      when update_terminal_cwd is missing, preventing "command not found"
      errors on fresh macOS bash sessions

3.63  2026-05-08
    - added dockerfile manifest support to skill installation, allowing skills
      to declare Docker images that get built automatically during install
    - fixed Progress.pm to support unlimited streaming output for long-running
      operations like Docker builds, with optional max_detail_lines cap
    - added has_dockerfile metadata to skill list and usage output
    - fixed macOS Terminal.app shell bootstrap to source /etc/bashrc_Apple_Terminal
      when update_terminal_cwd is missing, preventing "command not found"
      errors on fresh macOS bash sessions

3.62  2026-05-07
    - remove matching root ddfile skill source entries when uninstalling a skill

3.61  2026-05-06

Changes  view on Meta::CPAN

3.42  2026-05-05
    - fixed blank-mac `curl ... | sh` bootstrap so `install.sh` now
      bootstraps Homebrew automatically before reading the repo `brewfile`
      and continuing the normal macOS package flow
    - fixed Linux runtime lifecycle isolation so `dashboard restart` and
      `dashboard stop` no longer adopt or kill Developer Dashboard web and
      collector pids that belong to a different pid namespace such as a
      sibling Docker container
    - added Docker-style rolling detail lines to `dashboard skills install`
      so long-running dependency manifests keep the epic task board visible
      while streaming the newest manifest output under the active step
    - fixed tmux ticket-session prompt/status behavior so existing and new
      `dashboard ticket` tmux sessions suppress inline prompt indicators,
      move the full indicator strip into the first row of a two-line bottom
      tmux status block, keep tmux's normal indexed session/window row
      beneath it, preserve TT-backed percentage indicators and live
      collector values, refresh automatically through tmux status updates,
      and leave ordinary tmux sessions on the normal inline prompt path

3.40  2026-05-05
    - moved prompt indicators out of inline shell prompts and into tmux

README.md  view on Meta::CPAN

nested skill to the participating leaf service, preserving overwritten parent
keys under cumulative aliases such as `foo_VERSION` and
`foo_bar_VERSION` before the leaf value becomes the plain key. The resolver
also exports one skill-specific `<skill-name>_DDDC` variable for each
participating skill, using the leaf skill name with non-identifier characters
normalized to underscores and pointing that variable at the owning
`config/docker/` root. Nested skill services additionally export the full
cumulative skill path alias such as `foo_bar_zzz_DDDC` for the same compose
root, while the leaf alias stays available as `zzz_DDDC`.
When `--dry-run` is omitted, the dashboard hands off with `exec` so the
terminal sees the normal streaming output from `docker compose` itself
instead of a dashboard JSON wrapper.

## Prompt Integration

Render prompt text directly:

    dashboard ps1 --jobs 2

`dashboard ps1` now follows the original `~/bin/ps1` shape more closely: a
`(YYYY-MM-DD HH:MM:SS)` timestamp prefix, dashboard status and workspace info, a

bin/dashboard  view on Meta::CPAN

    my $stream = $cmd eq 'doctor' ? 0 : 1;

    my %results;
    for my $hook_root (@hook_roots) {
        opendir my $dh, $hook_root or next;
        for my $entry ( sort grep { $_ ne '.' && $_ ne '..' } readdir $dh ) {
            my $path = File::Spec->catfile( $hook_root, $entry );
            next if !is_runnable_file($path);
            next if $entry eq 'run';

            my $hook_result = _run_command_hook_streaming(
                $path,
                stream => $stream,
                argv   => \@argv,
            );
            my $result_key = exists $results{$entry} ? _command_hook_result_key($path) : $entry;
            $results{$result_key} = {
                stdout => $hook_result->{stdout},
                stderr => $hook_result->{stderr},
            };
            $results{$result_key}{exit_code} = $hook_result->{exit_code} if defined $hook_result->{exit_code};

bin/dashboard  view on Meta::CPAN

    require Developer::Dashboard::PathRegistry;
    $switchboard_paths = Developer::Dashboard::PathRegistry->new(
        cwd             => $switchboard_cwd,
        home            => $ENV{HOME},
        workspace_roots => [],
        project_roots   => [],
    );
    return $switchboard_paths;
}

# _run_command_hook_streaming($path, %args)
# Executes one hook file, streams its output live, and captures stdout/stderr so
# later hooks and the final command can inspect RESULT JSON.
# Input: executable hook path plus an optional stream flag and argv array ref.
# Output: hash reference containing stdout, stderr, and exit_code.
sub _run_command_hook_streaming {
    my ( $path, %args ) = @_;
    my $stream = exists $args{stream} ? $args{stream} : 1;
    my @argv = @{ $args{argv} || [] };
    open my $stdin, '<', File::Spec->devnull() or die "Unable to open " . File::Spec->devnull() . " for hook stdin: $!";
    my $stderr = gensym();
    my $stdout;
    my @command = command_argv_for_path($path);
    my $pid = open3( $stdin, $stdout, $stderr, @command, @argv );
    close $stdin;

doc/integration-test-plan.md  view on Meta::CPAN

- encoding: `dashboard encode`, `dashboard decode`
- indicators: `dashboard indicator set`, `dashboard indicator list`, `dashboard indicator refresh-core`
- collectors: `dashboard collector write-result`, `run`, `list`, `job`, `status`, `output`, `inspect`, `log`, `start`, `restart`, `stop`
- config: `dashboard config init`, `dashboard config show`
- auth: `dashboard auth add-user`, `list-users`, `remove-user`
- pages: `dashboard page new`, `save`, `list`, `show`, `encode`, `decode`, `urls`, `render`, `source`
- actions: `dashboard action run system-status paths`
- docker resolver: `dashboard docker compose --dry-run`
- web lifecycle: `dashboard serve`, `dashboard restart`, `dashboard stop`
- browser checks: headless Chromium editor, saved fake-project bookmark page, outsider bootstrap DOM verification, and helper-login DOM verification after helper-user enablement
- ajax streaming: installed long-running `/ajax/<file>` route timing, early-chunk verification, refresh-safe singleton replacement, `fetch_value()` / `stream_value()` DOM helper coverage, and browser pagehide cleanup coverage in unit tests
- windows verification assets: `integration/windows/run-strawberry-smoke.ps1` and `integration/windows/run-qemu-windows-smoke.sh`
- when the Windows checkout bootstrap changes, `integration/windows/run-strawberry-smoke.ps1` must be rerun with `-UseInstallBootstrap` so the guest exercises `install.ps1` through the streamed `Invoke-Expression` path

When a release changes the skills runtime, also run the focused host-side
skill regressions outside the blank-container harness:

- `prove -lv t/19-skill-system.t`
- `prove -lv t/20-skill-web-routes.t`
- `prove -lv t/09-runtime-manager.t`

doc/integration-test-plan.md  view on Meta::CPAN

17. Exercise page create/save/show/encode/decode/render/source flows inside the fake bookmark directory.
18. Exercise builtin action execution.
19. For Windows-targeted changes, run `integration/windows/run-strawberry-smoke.ps1 -UseInstallBootstrap -BootstrapScript <checkout install.ps1>` so the guest validates the same streamed `Invoke-Expression` bootstrap shape that operators use with `ir...
20. Exercise docker compose dry-run resolution against a temporary project.
21. Start the installed web service.
22. Confirm exact-loopback access reaches the editor page in Chromium.
23. Confirm the browser can render a saved fake-project bookmark page from the fake project bookmark directory.
24. Confirm the browser inserts sorted rendered `nav/*.tt` bookmark fragments between the top chrome and the main page body.
25. Confirm the browser top-right status strip shows configured collector icons, not collector names, that UTF-8 icons such as `🐳` and `💰` are visibly rendered, and that renamed collectors no longer leave stale managed indicators behind.
26. Confirm an installed saved bookmark page can declare `var endpoints = {};`, then use `fetch_value()` and `stream_value()` from `$(document).ready(...)` against saved `/ajax/<file>` routes without inline-script ordering failures or browser console...
27. Confirm an installed long-running saved `/ajax/<file>` route starts streaming the first output chunks promptly instead of buffering until the worker exits.
28. Confirm an installed skill page that ships `config/routes.json` emits the declared canonical custom ajax path, that the custom path resolves, that the smart `/ajax/<repo-name>/...` route still resolves for the same handler, and that a route-level...
29. Confirm non-loopback self-access returns `401` with an empty body and without a login form before any helper user exists in the active runtime.
30. Add a helper user for the outsider browser flow, then confirm non-loopback self-access reaches the helper login page in Chromium.
31. Log in as a helper through the HTTP helper flow.
32. Confirm helper page chrome shows `Logout`.
33. Log out and confirm the helper account is removed.
34. Restart the installed runtime from the extracted tarball tree and confirm the web service comes back.
35. Stop the runtime and confirm the web service is gone.

## Expected Results

- every covered command exits successfully except bare `dashboard`, which should
  return usage with a non-zero status
- `dashboard version` reports the installed release version
- `dashboard init` creates starter state without requiring manual setup
- `dashboard update` succeeds in the container from a user-provided fake-project `./.developer-dashboard/cli/update` command through the normal command-hook path
- the installed `dashboard` binary works without `perl -Ilib`
- the fake project's `./.developer-dashboard` tree becomes the active local runtime root with the home tree as fallback
- layered root-to-leaf `.env` and `.env.pl` files override in order, skill-local env files load only for skill execution paths, nested skill commands expand `foo -> foo.bar -> foo.bar.zzz` env files in order while preserving overwritten parent keys u...
- skill dependency installs follow `aptfile -> apkfile -> dnfile -> wingetfile -> brewfile -> package.json -> requirements.txt -> cpanfile -> cpanfile.local -> Makefile -> ddfile -> ddfile.local`, with `aptfile`, `apkfile`, and `dnfile` probing each ...
- long-running skill dependency steps keep the main epic checklist visible while streaming a Docker-style rolling ten-line detail window under the active task, collapse those detail lines when the task succeeds, and leave the captured detail visible ...
- explicit `dashboard skills install --ddfile` runs process `ddfile` first into the active layered skills root and then `ddfile.local` into the current directory's nested `./skills/` tree
- explicit `dashboard skills install <source> ...` runs can install one or more sources in command-line order, append each exact source to the home root `~/.developer-dashboard/ddfile` without duplicating existing non-comment entries, `dashboard skil...
- when the home runtime already has `.gitignore` or compatibility `.gitiignore`, explicit skill installs append `skills/<repo-name>/` without duplicates so cloned skill trees stay ignored by runtime Git checkouts
- `dashboard skill` is covered as the singular alias for `dashboard skills` management commands while installed command execution remains on the dotted `dashboard <skill>.<command>` path
- streamed `install.sh` runs such as `curl ... | sh` succeed without a local checkout by falling back to embedded `aptfile`, `apkfile`, `dnfile`, and `brewfile` manifests, shipping `tmux` in those bootstrap package sets because `dashboard workspace` ...
- the old-system-Perl Alpine rescue path keeps the locally bootstrapped `perlbrew` and `patchperl` tools on the private `~/perl5/lib/perl5` include path so `curl ... | sh` can still build `perl-5.38.5` instead of dying with missing `App::perlbrew` or...
- Debian-family streamed bootstrap also copes with third-party `nodejs` repositories that conflict with the distro `npm` package by installing `nodejs` first, checking whether `npm` and `npx` are already present, and only then attempting the distro `...
- Alpine streamed bootstrap installs the repo-root `apkfile` package set through `apk add --no-cache` and then proves the same post-install shell finish line as Debian-family hosts
- Debian-family streamed bootstrap uses `perlbrew --notest install perl-5.38.5` for the old-system-Perl rescue path so blank-machine bootstrap does not fail on upstream Perl core test noise before Developer Dashboard itself is installed
- `install.sh` prints a full progress board before it changes the system, then emits only per-step transitions instead of redrawing the whole board, explains any upcoming `sudo` prompt as an operating-system package-manager password request before th...

doc/testing.md  view on Meta::CPAN

  --expect-page-fragment "set_chain_value(endpoints,'status','/v1/status')" \
  --expect-ajax-path /v1/status \
  --expect-ajax-body '{"status":"ok"}'
```

For long-running saved bookmark Ajax handlers that would otherwise survive a
browser refresh, prefer `Ajax(..., singleton => 'NAME', ...)`. The runtime will
rename the Perl worker to `dashboard ajax: NAME`, terminate the older matching
Perl stream before it starts the refreshed one, and also tear down matching
singleton workers during `dashboard stop`, `dashboard restart`, and browser
`pagehide` cleanup beacons. For browser streaming checks, use `stream_data()`
or `stream_value()` against a finite saved Ajax handler and assert the final
DOM after incremental chunks land.

## Coverage

Install Devel::Cover in a local Perl library and generate the coverage report:

```bash
cpanm --notest --local-lib-contained ./.perl5 Devel::Cover
export PERL5LIB="$PWD/.perl5/lib/perl5${PERL5LIB:+:$PERL5LIB}"

doc/testing.md  view on Meta::CPAN

- shared `nav/*.tt` bookmark rendering between top chrome and the main page body in sorted filename order
- raw `nav/*.tt` TT/HTML fragment rendering between top chrome and the main page body, plus direct `/app/nav/<name>.tt` and `/source` coverage for those raw fragment files
- Template Toolkit conditional rendering for shared nav fragments and saved pages using `env.current_page` and `env.runtime_context.current_page`
- `/apps -> /app/index` compatibility
- top chrome rendering on edit and saved render pages
- denial of browser `token=` and `atoken=` execution for transient page and action payloads, plus transient `/ajax?token=...`, when the transient URL opt-in env var is absent
- absence of accidental project-local `.developer-dashboard` creation when `dashboard restart` runs inside a git repo that has not opted into a local dashboard root
- saved bookmark `Ajax file => ...` handlers through `/ajax/<file>?type=...`, including `dashboards/ajax/...` storage, direct process-backed streamed ajax execution for both `stdout` and `stderr`, and blank-env verification under the default deny pol...
- file-backed saved Ajax Perl wrappers with autoflushed `STDOUT` and `STDERR`, including a timing check that long-running `print` plus `sleep` handlers emit early chunks instead of buffering until exit
- skill install progress rendering that keeps the epic checklist visible while
  streaming a rolling ten-line detail window under the active manifest step,
  with `CLI::Progress` coverage guarding both the rolling window and the
  collapse-on-success redraw

## Blank Environment Integration

Run the host-built tarball integration flow with:

```bash
integration/blank-env/run-host-integration.sh
```

doc/testing.md  view on Meta::CPAN


The integration flow also:

- creates a fake project with its own `./.developer-dashboard` runtime tree
- creates that fake-project runtime tree only after `cpanm` completes, so the tarball's own test phase still runs against a clean runtime
- verifies installed CLI and saved bookmarks from that fake project's local runtime plus config collectors from that same runtime root
- verifies `dashboard version` reports the installed runtime version
- seeds a user-provided fake-project `./.developer-dashboard/cli/update` command plus `update.d` hooks inside the container and verifies `dashboard update` uses the same executable command-hook path as every other top-level subcommand, including late...
- verifies the installed web app denies `/?token=...` browser execution by default while saved bookmark routes still render
- uses headless Chromium to validate the editor, the saved fake-project bookmark page, and the helper login page
- verifies that an installed long-running saved `/ajax/...` route starts streaming visible output within the expected first seconds instead of buffering until process exit
- should be interpreted together with the tracked source-tree integration assets in `doc/integration-test-plan.md`, `doc/windows-testing.md`, and `integration/browser/run-bookmark-browser-smoke.pl`; source-tree tests now fail if those release/support...

## Windows Verification

For Windows-targeted changes, keep the verification layered:

- run the fast forced-Windows unit coverage in `t/`
- run the real Strawberry Perl smoke on a Windows host with `integration/windows/run-strawberry-smoke.ps1`
- when the checkout bootstrap changes, rerun that same smoke with `-UseInstallBootstrap` so the guest executes `install.ps1` through the streamed `Invoke-Expression` shape instead of only the file path
- run the full-system QEMU guest smoke with `integration/windows/run-host-windows-smoke.sh` before making a release-grade Windows compatibility claim

doc/update-and-release.md  view on Meta::CPAN

chmod +x ~/.developer-dashboard/cli/update.d/01-runtime
perl -Ilib bin/dashboard update
```

This executes ordered scripts from either `~/.developer-dashboard/cli/update`
or `~/.developer-dashboard/cli/update.d`:

1. sorted by filename
2. running any regular executable file
3. skipping non-executable files
4. streaming each hook file's stdout and stderr live while still accumulating `RESULT` JSON
5. rewriting `RESULT` after each hook so later hook files can react to earlier output
6. passing the final `RESULT` JSON to the real command

`dashboard update` has no special built-in path. If you want it, provide it as
a normal user command and let its hook files run through the same top-level
command-hook path as every other dashboard subcommand.

Perl hook scripts can use `Developer::Dashboard::Runtime::Result` to decode `RESULT` and read
structured hook output without hand-parsing the JSON blob. If the final Perl
command wants a compact summary after the hook chain finishes, it can call

doc/update-and-release.md  view on Meta::CPAN

- the local server adds CSP, frame-deny, nosniff, no-referrer, and no-store headers

The extension layer now includes:

- config-backed provider pages resolved through the page resolver
- action execution through the page action runner
- user CLI hook directories under `~/.developer-dashboard/cli`
- project-aware Docker Compose resolution through `dashboard docker compose`

Compose setup can now stay isolated in service folders under `./.developer-dashboard/config/docker/<service>/compose.yml` for the current project, with `~/.developer-dashboard/config/docker/<service>/compose.yml` as the fallback. The wrapper infers s...
Without `--dry-run`, the wrapper now hands off with `exec`, so terminal users see the normal streaming output from `docker compose` itself instead of a dashboard JSON wrapper.
Path aliases can now be managed from the CLI with `dashboard path add <name> <path>` and `dashboard path del <name>`. These commands persist user-defined aliases in the effective config root, using a project-local `./.developer-dashboard` tree first ...
Use `Developer::Dashboard::Folder` for runtime path helpers. It resolves the
same root-style names exposed by `dashboard paths`, including runtime,
bookmark, config, and configured alias names such as `docker`, without relying
on unscoped CPAN-global module names.
`dashboard init` now seeds the current dashboard-managed starter pages as editable saved bookmarks when those ids are missing. Re-running init keeps existing user config intact, creates `config.json` as `{}` only when it is missing, keeps dashboard-m...
`dashboard cpan <Module...>` now manages optional runtime Perl modules under `./.developer-dashboard/local` and appends matching requirements to `./.developer-dashboard/cpanfile`, while keeping the implementation in `bin/dashboard` and letting saved ...

## Release To PAUSE

integration/blank-env/run-integration.pl  view on Meta::CPAN

    }
    return {
        command   => $command,
        exit_code => $exit_code,
        stdout    => defined $stdout ? $stdout : '',
        stderr    => defined $stderr ? $stderr : '',
    };
}

# _capture_stream_prefix($label, $command, %opts)
# Runs one streaming shell command and records when expected stdout chunks first appear.
# Input: human label, shell command string, expected_chunks array ref, and optional timeout seconds.
# Output: hash reference with stdout, stderr, and matched event timing data.
sub _capture_stream_prefix {
    my ( $label, $command, %opts ) = @_;
    my $expected = $opts{expected_chunks} || [];
    my $timeout  = $opts{timeout} || 5;
    print "==> $label\n";
    print "    $command\n";
    my $stderr_fh = gensym();
    my $pid = open3( undef, my $stdout_fh, $stderr_fh, 'sh', '-lc', $command );

lib/Developer/Dashboard.pm  view on Meta::CPAN

nested skill to the participating leaf service, preserving overwritten parent
keys under cumulative aliases such as C<foo_VERSION> and
C<foo_bar_VERSION> before the leaf value becomes the plain key. The resolver
also exports one skill-specific C<E<lt>skill-nameE<gt>_DDDC> variable for each
participating skill, using the leaf skill name with non-identifier characters
normalized to underscores and pointing that variable at the owning
F<config/docker/> root. Nested skill services additionally export the full
cumulative skill path alias such as C<foo_bar_zzz_DDDC> for the same compose
root, while the leaf alias stays available as C<zzz_DDDC>.
When C<--dry-run> is omitted, the dashboard hands off with C<exec> so the
terminal sees the normal streaming output from C<docker compose> itself
instead of a dashboard JSON wrapper.

=head2 Prompt Integration

Render prompt text directly:

  dashboard ps1 --jobs 2

C<dashboard ps1> now follows the original F<~/bin/ps1> shape more closely: a
C<(YYYY-MM-DD HH:MM:SS)> timestamp prefix, dashboard status and workspace info, a

lib/Developer/Dashboard/PageRuntime.pm  view on Meta::CPAN


# _noop_writer(@parts)
# Accepts streamed output chunks when the caller does not need them.
# Input: zero or more ignored chunk parts.
# Output: empty string.
sub _noop_writer { return '' }

# _drain_saved_ajax_ready_handle(%args)
# Reads one ready saved-Ajax process pipe handle and forwards the chunk or error to the right writer.
# Input: ready fh, active select set, stdout fh, saved file path, and writer callbacks.
# Output: true value when streaming should continue, otherwise false when the client disconnected.
sub _drain_saved_ajax_ready_handle {
    my ( $self, %args ) = @_;
    my $fh            = $args{fh}            || die 'Missing ready handle';
    my $path          = $args{path}          || '';
    my $select        = $args{select}        || die 'Missing select set';
    my $stdout        = $args{stdout}        || die 'Missing stdout handle';
    my $stdout_writer = $args{stdout_writer} || \&_noop_writer;
    my $stderr_writer = $args{stderr_writer} || \&_noop_writer;
    my $chunk = '';
    my $bytes = $self->_stream_sysread( $fh, \$chunk );

lib/Developer/Dashboard/PageRuntime.pm  view on Meta::CPAN

    my $stdout_fileno = fileno($stdout);
    if ( defined $ready_fileno && defined $stdout_fileno && $ready_fileno == $stdout_fileno ) {
        my $continued = $stdout_writer->($chunk);
        return defined $continued ? $continued : 1;
    }
    my $continued = $stderr_writer->($chunk);
    return defined $continued ? $continued : 1;
}

# _close_saved_ajax_streams($select, @handles)
# Closes the saved-Ajax select set and any remaining pipe handles after streaming stops.
# Input: IO::Select object plus zero or more pipe handles.
# Output: true value.
sub _close_saved_ajax_streams {
    my ( $self, $select, @handles ) = @_;
    if ( $select && eval { $select->can('handles') } ) {
        for my $fh ( $select->handles ) {
            next if !defined fileno($fh);
            $select->remove($fh);
            close $fh;
        }

lib/Developer/Dashboard/PageRuntime/StreamHandle.pm  view on Meta::CPAN

=head1 METHODS

=head2 TIEHANDLE, PRINT, PRINTF, CLOSE

Implement the tied-handle contract used by streamed bookmark Ajax execution.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module is the small stream object used by page runtime and web streaming code. It presents one consistent write interface for incremental output so bookmark runtime code and server-side streaming can push chunks without depending on a specific P...

=head1 WHY IT EXISTS

It exists because streaming output is easier to test when the stream sink is a small object instead of a raw callback buried in transport code. That separation also keeps disconnect handling and chunk capture explicit.

=head1 WHEN TO USE

Use this file when changing streaming write semantics, buffering behavior, or tests around incremental page output and broken-pipe handling.

=head1 HOW TO USE

Construct it with the callback or sink expected by the caller, then pass it into the part of the runtime that wants to emit streaming content. Keep transport-neutral streaming behavior here rather than tying it to one web-server code path.

=head1 WHAT USES IT

It is used by page-runtime streaming helpers, by web response code that needs incremental output, and by coverage tests around streamed bookmark and Ajax behavior.

=head1 EXAMPLES

Example 1:

  perl -Ilib -MDeveloper::Dashboard::PageRuntime::StreamHandle -e 1

Do a direct compile-and-load check against the module from a source checkout.

Example 2:

lib/Developer/Dashboard/SkillDispatcher.pm  view on Meta::CPAN

    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 );

lib/Developer/Dashboard/SkillDispatcher.pm  view on Meta::CPAN

    );
    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);

lib/Developer/Dashboard/SkillDispatcher.pm  view on Meta::CPAN

        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;

lib/Developer/Dashboard/SkillDispatcher.pm  view on Meta::CPAN

            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} ) {

lib/Developer/Dashboard/SkillDispatcher.pm  view on Meta::CPAN

    }

    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} ) {

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

    $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;

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

    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' );

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

# _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 {

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

        @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},

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

        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},

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

        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},

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

# 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",
        );

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

# _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},

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN


# _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;

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN


# _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;

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN

            ? ( ['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}) {

lib/Developer/Dashboard/SkillManager.pm  view on Meta::CPAN


# _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,

lib/Developer/Dashboard/UpdateManager.pm  view on Meta::CPAN

Construct and execute dashboard updates.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module runs the ordered update hook chain for C<dashboard update>. It discovers executable update scripts, runs them in sorted order, streams their stdout and stderr, updates the structured C<RESULT> state between hooks, and coordinates collecto...

=head1 WHY IT EXISTS

It exists because update hooks are a first-class runtime workflow, not a one-off shell loop. The dashboard needs one module that owns ordering, streaming, structured hook results, and collector lifecycle around updates.

=head1 WHEN TO USE

Use this file when changing update hook discovery, update streaming behavior, RESULT propagation between update hooks, or the way updates stop and restart collectors.

=head1 HOW TO USE

Construct it with the file registry, path registry, and collector runner, then call its run method from the update command. Keep update hook execution policy in this module rather than in the command wrapper.

=head1 WHAT USES IT

It is used by the C<dashboard update> flow, by runtime bootstrap/update smoke tests, and by coverage that verifies update hook ordering and collector restart semantics.

=head1 EXAMPLES

lib/Developer/Dashboard/Web/DancerApp.pm  view on Meta::CPAN

    my ($index) = @_;
    my @parts = splat;
    @parts = @{ $parts[0] } if @parts == 1 && ref( $parts[0] ) eq 'ARRAY';
    return undef if !@parts;
    return $parts[$index];
}

# _response_from_result($result)
# Applies one backend response onto the active Dancer2 response object.
# Input: backend response array reference.
# Output: plain body or delayed streaming response suitable for Dancer2.
sub _response_from_result {
    my ($result) = @_;
    my ( $code, $type, $body, $headers ) = @{$result};
    my $backend = _current_backend();
    my %merged_headers = (
        %{ $backend->{default_headers} || {} },
        %{ $headers || {} },
    );

    if ( ref($body) eq 'HASH' && ref( $body->{stream} ) eq 'CODE' ) {

t/03-web-app.t  view on Meta::CPAN

  use Developer::Dashboard::DataHelper qw( j );
  print j { ok => 1 };
};
PAGE
$store->save_page($fetch_stream_page);
my ($fetch_stream_code, undef, $fetch_stream_body) = @{ $app->handle(path => '/app/fetch-stream-helpers', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($fetch_stream_code, 200, 'legacy bookmark with fetch_value and stream_value helpers renders');
like($fetch_stream_body, qr/function fetch_value\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes fetch_value helper');
like($fetch_stream_body, qr/function stream_value\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes stream_value helper');
like($fetch_stream_body, qr/function stream_data\(url, target, options, formatter\)/, 'legacy bookmark bootstrap exposes stream_data helper');
like($fetch_stream_body, qr/new XMLHttpRequest\(\)/, 'legacy bookmark streaming helper uses XMLHttpRequest for progressive browser updates');
like($fetch_stream_body, qr/xhr\.onprogress = function \(\)/, 'legacy bookmark streaming helper updates targets from incremental ajax progress events');
my $foo_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'foo','/ajax/foo?type=text'});
my $bar_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'bar','/ajax/bar?type=text&singleton=BAR'});
my $mike_bind_pos = index($fetch_stream_body, q{set_chain_value(endpoints,'mike','/ajax/mike?type=json'});
my $endpoints_decl_pos = index($fetch_stream_body, q{var endpoints = {};});
my $fetch_call_pos = index($fetch_stream_body, q{fetch_value(endpoints.foo, '#foo');});
ok($foo_bind_pos > -1 && $bar_bind_pos > -1 && $mike_bind_pos > -1, 'legacy bookmark render includes all saved Ajax endpoint bindings for fetch_value and stream_value');
ok($endpoints_decl_pos > -1, 'legacy bookmark render keeps the caller endpoint variable declaration');
ok($foo_bind_pos > $endpoints_decl_pos && $bar_bind_pos > $endpoints_decl_pos && $mike_bind_pos > $endpoints_decl_pos, 'saved Ajax endpoint bindings render after the caller declares the endpoint root object');
ok($fetch_call_pos > -1, 'legacy bookmark render keeps the inline fetch helper call');
like($fetch_stream_body, qr/dashboard_ajax_singleton_cleanup\('BAR'\)/, 'legacy bookmark render keeps singleton cleanup bindings for stream_value pages');

t/03-web-app.t  view on Meta::CPAN

    my $manual_ajax_token = uri_escape( encode_payload(q{print j { blocked => 1 };}) );
    my ($blocked_ajax_code, $blocked_ajax_type, $blocked_ajax_body) = @{ $app->handle(path => '/ajax', query => "token=$manual_ajax_token&type=json", remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
    is($blocked_ajax_code, 403, 'legacy ajax token route is denied when transient token URLs are disabled');
    like($blocked_ajax_type, qr/text\/plain/, 'legacy ajax token denial returns plain text');
    like($blocked_ajax_body, qr/Transient token URLs are disabled/, 'legacy ajax token denial explains the policy');
}
{
    local $ENV{DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS} = 1;
    my $manual_ajax_token = uri_escape( encode_payload(q{die "token ajax died\n";}) );
    my ($manual_ajax_error_code, $manual_ajax_error_type, $manual_ajax_error_body) = @{ $app->handle(path => '/ajax', query => "token=$manual_ajax_token&type=text", remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
    is($manual_ajax_error_code, 200, 'legacy ajax token runtime errors still return the streaming response shape');
    like($manual_ajax_error_type, qr/text\/plain/, 'legacy ajax token runtime errors keep the requested content type');
    like(drain_stream_body($manual_ajax_error_body), qr/token ajax died/, 'legacy ajax token runtime errors stream the runtime error text');
}

my $script_breakout_source = join "\n",
    'BOOKMARK: script-breakout',
    ':--------------------------------------------------------------------------------:',
    q{HTML: <script src="/js/jquery.js"></script>},
    q{<script>console.log("hello")</script>},
    ':--------------------------------------------------------------------------------:',

t/05-cli-smoke.t  view on Meta::CPAN

use strict;
use warnings;
print $ENV{RESULT} // '';
PL
close $custom_run_fh;
chmod 0755, $custom_run or die "Unable to chmod $custom_run: $!";
my ( $custom_stdout, $custom_stderr, $custom_exit ) = capture {
    system 'sh', '-c', "$perl -I'$lib' '$dashboard' inspect-result";
    return $? >> 8;
};
is( $custom_exit, 0, 'directory-backed custom command succeeds after hook streaming' );
like( $custom_stdout, qr/^custom-hook\n/s, 'directory-backed custom command streams hook stdout before the final RESULT json' );
like( $custom_stderr, qr/custom-hook-err\n/, 'directory-backed custom command streams hook stderr live' );
my ($custom_json) = $custom_stdout =~ /(\{[\s\S]*\})\s*\z/;
ok( defined $custom_json, 'directory-backed custom command leaves trailing RESULT json after streamed hook output' );
my $custom_result_data = json_decode($custom_json);
is( $custom_result_data->{'00-pre.pl'}{stdout}, "custom-hook\n", 'directory-backed custom commands receive RESULT JSON from their hook files' );
like( $custom_result_data->{'00-pre.pl'}{stderr}, qr/custom-hook-err/, 'directory-backed custom command RESULT keeps captured hook stderr' );

my $report_dir_root = File::Spec->catdir( $ENV{HOME}, '.developer-dashboard', 'cli', 'report-result' );
make_path($report_dir_root);

t/08-web-update-coverage.t  view on Meta::CPAN

}

{
    my ( $ajax_bad_file_code, $ajax_bad_file_type, $ajax_bad_file_body ) = @{ $app->handle( path => '/ajax', query => 'file=..%2Fbad&type=json', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    is( $ajax_bad_file_code, 400, 'legacy ajax route rejects invalid saved bookmark ajax file names cleanly' );
    like( $ajax_bad_file_type, qr/text\/plain/, 'legacy ajax invalid saved-file route returns plain text' );
    like( $ajax_bad_file_body, qr/invalid parent traversal/, 'legacy ajax invalid saved-file route returns the validation error text' );
}

{
    my $streaming_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-stream
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'stream.txt', code => q{
  print "first\n";
  print "second\n";
};
PAGE
    $store->save_page($streaming_page);
    my ( undef, undef, undef ) = @{ $app->handle( path => '/app/ajax-stream', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my ( $ajax_stream_code, $ajax_stream_type, $ajax_stream_body ) = @{ $app->handle( path => '/ajax/stream.txt', query => 'type=text', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    is( $ajax_stream_code, 200, 'legacy ajax saved-file route responds successfully for streaming output' );
    like( $ajax_stream_type, qr/text\/plain/, 'legacy ajax saved-file route keeps the requested content type for streaming output' );
    is( drain_stream_body($ajax_stream_body), "first\nsecond\n", 'legacy ajax saved-file route streams raw printed output without page buffering' );
}

{
    my $process_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-process
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'process-endpoint.json', code => q{

t/08-web-update-coverage.t  view on Meta::CPAN

print "$0\n";
};
PAGE
    $store->save_page($singleton_page);
    my ( undef, undef, $singleton_page_body ) = @{ $app->handle( path => '/app/ajax-singleton', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    like( $singleton_page_body, qr{/ajax/singleton-endpoint\.txt\?type=text&singleton=FOOBAR}, 'saved bookmark Ajax page emits the singleton query parameter in the generated ajax url' );
    like( $singleton_page_body, qr/dashboard_ajax_singleton_cleanup\('FOOBAR'\)/, 'saved bookmark Ajax page registers browser lifecycle cleanup for singleton-managed workers' );
    my ( $ajax_singleton_code, undef, $ajax_singleton_body ) = @{ $app->handle( path => '/ajax/singleton-endpoint.txt', query => 'type=text&singleton=FOOBAR', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
    my $ajax_singleton_output = drain_stream_body($ajax_singleton_body);
    is( $ajax_singleton_code, 200, 'legacy ajax saved-file route responds successfully for singleton-managed requests' );
    like( $ajax_singleton_output, qr/^dashboard ajax: FOOBAR$/m, 'legacy ajax saved-file route renames singleton-managed Perl workers before streaming output' );
}

{
    my @patterns;
    {
        no warnings 'redefine';
        local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub {
            my ( $self, $pattern ) = @_;
            push @patterns, $pattern;
            return 1;

t/08-web-update-coverage.t  view on Meta::CPAN

{
    my $res;
    Local::PSGITest::test_psgi $header_server->psgi_app, sub {
        my ($cb) = @_;
        $res = $cb->( POST 'http://127.0.0.1/login', [ username => 'helper', password => 'helper-pass-123' ] );
    };
    is( $res->header('Location'), '/login', 'server forwards custom Location headers from the app' );
    is( $res->header('Set-Cookie'), 'dashboard_session=abc', 'server forwards custom Set-Cookie headers from the app' );
}

my $streaming_app = bless {}, 'Local::StreamingApp';
{
    no warnings 'once';
    *Local::StreamingApp::handle = sub {
        return [
            200,
            'text/plain; charset=utf-8',
            {
                stream => sub {
                    my ($writer) = @_;
                    $writer->("alpha\n");
                    $writer->("beta\n");
                },
            },
            { 'X-Test' => 'streaming' },
        ];
    };
}
my $streaming_server = Developer::Dashboard::Web::Server->new( app => $streaming_app );
{
    my $res;
    Local::PSGITest::test_psgi $streaming_server->psgi_app, sub {
        my ($cb) = @_;
        $res = $cb->( GET 'http://127.0.0.1/ajax' );
    };
    is( $res->code, 200, 'streaming response path returns success' );
    like( $res->header('Content-Type'), qr/text\/plain/, 'streaming response keeps the content type header' );
    is( $res->header('X-Test'), 'streaming', 'streaming response keeps custom headers' );
    is( $res->content, "alpha\nbeta\n", 'streaming response writes streamed body chunks into the final response body' );
}

my $failing_stream_app = bless {}, 'Local::FailingStreamApp';
{
    no warnings 'once';
    *Local::FailingStreamApp::handle = sub {
        return [
            200,
            'text/plain; charset=utf-8',
            {

t/08-web-update-coverage.t  view on Meta::CPAN

        ];
    };
}
my $failing_stream_server = Developer::Dashboard::Web::Server->new( app => $failing_stream_app );
{
    my $res;
    Local::PSGITest::test_psgi $failing_stream_server->psgi_app, sub {
        my ($cb) = @_;
        $res = $cb->( GET 'http://127.0.0.1/ajax' );
    };
    is( $res->code, 200, 'streaming error responses keep the original success status' );
    like( $res->content, qr/alpha/, 'streaming error responses keep chunks written before the failure' );
    like( $res->content, qr/stream exploded/, 'streaming error responses append the streaming exception text' );
}

{
    no warnings 'redefine';
    my @chunks;
    local *Developer::Dashboard::Web::DancerApp::delayed = sub (&) { $_[0]->(); return 'delayed-ok' };
    local $Dancer2::Core::Route::RESPONDER = sub {
        my ($response) = @_;
        is( $response->[0], 200, 'disconnect coverage responder receives the original status code' );
        like( join( "\n", @{ $response->[1] || [] } ), qr/Content-Type\ntext\/plain/, 'disconnect coverage responder receives the content type header' );

t/15-release-metadata.t  view on Meta::CPAN

    like( $doc, qr/\bworkspace\b.*~\/\.developer-dashboard\/cli|~\/\.developer-dashboard\/cli.*\bworkspace\b/s, 'docs describe private workspace helper staging' );
    like( $doc, qr/dashboard jq/, 'docs describe the renamed jq subcommand' );
    like( $doc, qr/dashboard yq/, 'docs describe the renamed yq subcommand' );
    like( $doc, qr/dashboard tomq/, 'docs describe the renamed tomq subcommand' );
    like( $doc, qr/dashboard propq/, 'docs describe the renamed propq subcommand' );
    like( $doc, qr/dashboard of \. jq|jq\.js.*jquery\.js|jquery\.js.*jq\.js/s, 'docs describe the scoped open-file ranking behaviour' );
    like( $doc, qr/Ok\\\.js\$|ok\.json|case-insensitive regex/i, 'docs describe regex-based scoped open-file matching explicitly' );
    like( $doc, qr/javax\.jws\.WebService|Maven source jar|~\/\.developer-dashboard\/cache\/open-file/i, 'docs describe Java source lookup through archives and cached Maven downloads' );
    like( $doc, qr/vim -p|C<vim -p>/, 'docs describe vim tab mode for blank-enter open-all' );
    like( $doc, qr/stream_data\(url, target, options, formatter\)|C<stream_data\(url, target, options, formatter\)>/, 'docs describe the bookmark stream_data helper' );
    like( $doc, qr/XMLHttpRequest/, 'docs describe incremental browser streaming through XMLHttpRequest' );
    like( $doc, qr/request-specific\s+token\s+form|carry(?:ing)?\s+those\s+token\s+values\s+across\s+matching\s+placeholders|(?:`\{\{token\}\}`|C<\{\{token\}\}>|\{\{token\}\})\s+placeholders/s, 'docs describe the request-token carry-over workflow' );
    like( $doc, qr/below\s+the\s+response\s+`pre`|below\s+the\s+response\s+C<pre>/s, 'docs describe the response tabs below the response pre box' );
    like( $doc, qr/dashboard cpan(?: <Module\.\.\.>| E<lt>Module\.\.\.E<gt>)?|C<dashboard cpan E<lt>Module\.\.\.E<gt>>/, 'docs describe the runtime-local dashboard cpan command' );
    like( $doc, qr/ssl_subject_alt_names/, 'docs describe configured extra SSL SAN aliases and IPs' );
    like( $doc, qr/config\/api\.json|F<config\/api\.json>/, 'docs describe layered config/api.json machine auth for saved ajax routes' );
    like( $doc, qr/dashboard api(?: add| rm| ls)?/, 'docs describe the dashboard api management command for layered machine auth' );
    like( $doc, qr/X-DD-API-Key|x-dd-api-key/i, 'docs describe the API key header for saved ajax machine auth' );
    like( $doc, qr/X-DD-API-Secret|x-dd-api-secret/i, 'docs describe the API secret header for saved ajax machine auth' );
    like( $doc, qr/bin\/dashboard|dashboard entrypoint|C<dashboard> entrypoint/, 'docs describe the dashboard cpan implementation as entrypoint-local' );
    like( $doc, qr/config\/config\.json.*intact|preserves an existing .*config\/config\.json/s, 'docs describe non-destructive dashboard init reruns' );

t/21-refactor-coverage.t  view on Meta::CPAN

    my $layered_dispatcher = Developer::Dashboard::SkillDispatcher->new( paths => $layered_paths );
    my $layered_hooks = $layered_dispatcher->execute_hooks( 'shared-layer-skill', 'run-test' );
    ok(
        exists $layered_hooks->{hooks}{'00-pre.pl'},
        'execute_hooks keeps the first hook basename for the first matching layered hook',
    );
    ok(
        exists $layered_hooks->{hooks}{'run-test.d/00-pre.pl'},
        'execute_hooks namespaces duplicate layered hook basenames by hook directory leaf',
    );
    my $layered_stream_hooks = $layered_dispatcher->_execute_hooks_streaming(
        'shared-layer-skill',
        'run-test',
        [ $layered_manager->get_skill_path('shared-layer-skill'), File::Spec->catdir( $ENV{HOME}, 'skills-home', '.developer-dashboard', 'skills', 'shared-layer-skill' ) ],
    );
    ok(
        exists $layered_stream_hooks->{hooks}{'run-test.d/00-pre.pl'},
        '_execute_hooks_streaming namespaces duplicate layered hook basenames by hook directory leaf',
    );
    is_deeply(
        $layered_dispatcher->get_skill_config('shared-layer-skill'),
        {
            skill_name => 'shared-layer-skill',
            collectors => [
                { name => 'alpha', interval => 20 },
                { name => 'beta',  interval => 30 },
            ],
            providers => [

t/21-refactor-coverage.t  view on Meta::CPAN

    my $capture_fail_repo = File::Spec->catdir( $test_repos, 'capture-fail-npm-skill' );
    make_path($capture_fail_repo);
    _write_file(
        File::Spec->catfile( $capture_fail_repo, 'package.json' ),
        qq|{"name":"capture-fail-npm","version":"1.0.0","dependencies":{"capture-fail-runtime":"^1.0.0"}}\n|
    );
    my $cwd = getcwd();
    my $error;
    {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            die "synthetic npm streaming failure\n";
        };
        $error = eval { $manager->_install_skill_package_json($capture_fail_repo); 1 } ? '' : $@;
    }
    like(
        $error,
        qr/synthetic npm streaming failure/,
        '_install_skill_package_json surfaces streaming failures from the staged npm install workspace',
    );
    is(
        getcwd(),
        $cwd,
        '_install_skill_package_json restores the original cwd after a staged npm capture failure',
    );
}
{
    my $installed_dep = File::Spec->catdir( $skill_paths->skills_root, 'shared-skill' );
    make_path($installed_dep);

t/21-refactor-coverage.t  view on Meta::CPAN

    like( $missing_skill->{error}, qr/\n\nDid you mean:\n/, 'missing-skill dispatch guidance includes suggestion heading' );
}
{
    my $missing_exec = $dispatcher->exec_command( 'missing-skill', 'run-test' );
    like( $missing_exec->{error}, qr/\ASkill 'missing-skill' not found\./, 'exec_command rejects missing skills' );
    like( $missing_exec->{error}, qr/\n\nDid you mean:\n/, 'missing-skill exec guidance includes suggestion heading' );
}
is_deeply( $dispatcher->execute_hooks( '', 'run-test' ), { hooks => {}, result_state => {} }, 'execute_hooks returns an empty result for missing skill names' );
is_deeply( $dispatcher->execute_hooks( 'dep-skill', '' ), { hooks => {}, result_state => {} }, 'execute_hooks returns an empty result for missing command names' );
is_deeply( $dispatcher->execute_hooks( 'missing-skill', 'run-test' ), { hooks => {}, result_state => {} }, 'execute_hooks returns an empty result for missing skills' );
is_deeply( $dispatcher->_execute_hooks_streaming( '', 'run-test', [] ), { hooks => {}, result_state => {} }, '_execute_hooks_streaming returns an empty payload for missing skill names' );
is_deeply( $dispatcher->_execute_hooks_streaming( 'dep-skill', '', [] ), { hooks => {}, result_state => {} }, '_execute_hooks_streaming returns an empty payload for missing command names' );
is_deeply( $dispatcher->_execute_hooks_streaming( 'dep-skill', 'run-test', [] ), { hooks => {}, result_state => {} }, '_execute_hooks_streaming returns an empty payload when no skill layers participate' );
ok( !$manager->disable('dep-skill')->{error}, 'disable succeeds for an installed skill' );
ok( !$manager->is_enabled('dep-skill'), 'is_enabled reports false once a skill is disabled' );
my @enabled_skill_roots = $skill_paths->installed_skill_roots;
my @all_skill_roots = $skill_paths->installed_skill_roots( include_disabled => 1 );
ok( !grep( { $_ eq $dep_skill_root } @enabled_skill_roots ), 'installed_skill_roots excludes disabled skills by default' );
ok( grep( { $_ eq $dep_skill_root } @all_skill_roots ), 'installed_skill_roots can still enumerate disabled skills when requested' );
is( $manager->get_skill_path('dep-skill'), undef, 'get_skill_path hides disabled skills from normal runtime lookup' );
ok( $manager->get_skill_path( 'dep-skill', include_disabled => 1 ), 'get_skill_path can still resolve disabled skills when explicitly requested' );
is_deeply(
    $dispatcher->dispatch( 'dep-skill', 'run-test' ),

t/21-refactor-coverage.t  view on Meta::CPAN

    local *Developer::Dashboard::SkillDispatcher::execute_hooks = sub {
        return { error => 'hook failure' };
    };
    is_deeply(
        $dispatcher->dispatch( 'dep-skill', 'run-test' ),
        { error => 'hook failure' },
        'dispatcher returns hook execution errors before launching the main skill command',
    );
}
{
    my $streaming_dir = tempdir( CLEANUP => 1 );
    my $streaming_script = File::Spec->catfile( $streaming_dir, 'stream-child.pl' );
    _write_file(
        $streaming_script,
        <<'PERL',
#!/usr/bin/env perl
use strict;
use warnings;
$| = 1;
print "stream-out:$ENV{SKILL_COMMAND}\n";
print STDERR "stream-err\n";
exit 7;
PERL
        0755,
    );
    my ( $stdout, $stderr, $result ) = capture {
        $dispatcher->_run_child_command_streaming(
            command      => [ $^X, $streaming_script ],
            args         => [],
            env          => { SKILL_COMMAND => 'run-test' },
            skill_layers => [$dep_skill_root],
            result_state => {},
            last_result  => {},
            stdin_mode   => 'null',
        );
    };
    is( $stdout, "stream-out:run-test\n", '_run_child_command_streaming mirrors child stdout while using null stdin for hooks' );
    is( $stderr, "stream-err\n", '_run_child_command_streaming mirrors child stderr while using null stdin for hooks' );
    is( $result->{stdout}, "stream-out:run-test\n", '_run_child_command_streaming captures child stdout for RESULT handoff' );
    is( $result->{stderr}, "stream-err\n", '_run_child_command_streaming captures child stderr for RESULT handoff' );
    is( $result->{exit_code}, 7, '_run_child_command_streaming captures the child exit code' );
}
{
    my $streaming_dir = tempdir( CLEANUP => 1 );
    my $streaming_script = File::Spec->catfile( $streaming_dir, 'stream-last-result.pl' );
    _write_file(
        $streaming_script,
        <<'PERL',
#!/usr/bin/env perl
use strict;
use warnings;
print "stream-last-result\n";
exit 0;
PERL
        0755,
    );
    local *Developer::Dashboard::Runtime::Result::set_last_result = sub {
        my ( $payload ) = @_;
        $main::dd_last_result_payload = $payload;
        return;
    };
    local $main::dd_last_result_payload;
    my ( $stdout, $stderr, $result ) = capture {
        $dispatcher->_run_child_command_streaming(
            command      => [ $^X, $streaming_script ],
            args         => [],
            env          => {},
            skill_layers => [$dep_skill_root],
            result_state => {},
            last_result  => { file => '/tmp/previous-hook', exit => 0, STDOUT => "old\n", STDERR => '' },
            stdin_mode   => 'null',
        );
    };
    is( $stdout, "stream-last-result\n", '_run_child_command_streaming still mirrors stdout when a prior RESULT payload exists' );
    is( $stderr, '', '_run_child_command_streaming keeps stderr empty when a prior RESULT payload exists and the child emits no stderr' );
    is_deeply(
        $main::dd_last_result_payload,
        { file => '/tmp/previous-hook', exit => 0, STDOUT => "old\n", STDERR => '' },
        '_run_child_command_streaming reloads the previous RESULT payload before launching the child',
    );
    is( $result->{exit_code}, 0, '_run_child_command_streaming preserves successful exit codes while restoring the previous RESULT payload' );
}
{
    my $stdin_dir = tempdir( CLEANUP => 1 );
    my $stdin_script = File::Spec->catfile( $stdin_dir, 'stdin-child.pl' );
    _write_file(
        $stdin_script,
        <<'PERL',
#!/usr/bin/env perl
use strict;
use warnings;

t/21-refactor-coverage.t  view on Meta::CPAN

    my $stdin_text = File::Spec->catfile( $stdin_dir, 'stdin.txt' );
    _write_file( $stdin_text, "hello-from-stdin\n" );
    open my $saved_stdin, '<&', \*STDIN or die "Unable to duplicate original STDIN: $!";
    open my $saved_stdout, '>&', \*STDOUT or die "Unable to duplicate original STDOUT: $!";
    open my $saved_stderr, '>&', \*STDERR or die "Unable to duplicate original STDERR: $!";
    my $stdout_path = File::Spec->catfile( $stdin_dir, 'stdout.txt' );
    my $stderr_path = File::Spec->catfile( $stdin_dir, 'stderr.txt' );
    open STDIN, '<', $stdin_text or die "Unable to open stdin fixture file: $!";
    open STDOUT, '>', $stdout_path or die "Unable to redirect stdout fixture file: $!";
    open STDERR, '>', $stderr_path or die "Unable to redirect stderr fixture file: $!";
    my $result = $dispatcher->_run_child_command_streaming(
        command      => [ $^X, $stdin_script ],
        args         => [],
        env          => {},
        skill_layers => [$dep_skill_root],
        result_state => {},
        last_result  => {},
        stdin_mode   => 'inherit',
    );
    open STDIN, '<&', $saved_stdin or die "Unable to restore original STDIN: $!";
    open STDOUT, '>&', $saved_stdout or die "Unable to restore original STDOUT: $!";

t/21-refactor-coverage.t  view on Meta::CPAN

    my $stdout = do {
        open my $fh, '<', $stdout_path or die "Unable to read captured stdout fixture file: $!";
        local $/;
        <$fh>;
    };
    my $stderr = do {
        open my $fh, '<', $stderr_path or die "Unable to read captured stderr fixture file: $!";
        local $/;
        <$fh>;
    };
    is( $stdout, "stdin:hello-from-stdin\n", '_run_child_command_streaming preserves interactive stdin when requested' );
    is( $stderr, '', '_run_child_command_streaming keeps stderr empty when the child emits no stderr' );
    is( $result->{stdout}, "stdin:hello-from-stdin\n", '_run_child_command_streaming captures inherited-stdin child stdout' );
    is( $result->{stderr}, '', '_run_child_command_streaming captures an empty stderr stream when nothing is emitted' );
    is( $result->{exit_code}, 0, '_run_child_command_streaming captures a successful inherited-stdin exit code' );
}
{
    my $hook_dir = File::Spec->catdir( $dep_skill_root, 'cli', 'streaming-hook.d' );
    make_path($hook_dir);
    my $hook_script = File::Spec->catfile( $hook_dir, '00-stream.pl' );
    _write_file(
        $hook_script,
        <<'PERL',
#!/usr/bin/env perl
use strict;
use warnings;
$| = 1;
print "hook-stream-out\n";
print STDERR "hook-stream-err\n";
exit 4;
PERL
        0755,
    );
    my ( $stdout, $stderr, $result ) = capture {
        $dispatcher->_execute_hooks_streaming( 'dep-skill', 'streaming-hook', [$dep_skill_root] );
    };
    is( $stdout, "hook-stream-out\n", '_execute_hooks_streaming mirrors hook stdout live' );
    is( $stderr, "hook-stream-err\n", '_execute_hooks_streaming mirrors hook stderr live' );
    is_deeply(
        $result->{hooks}{'00-stream.pl'},
        {
            stdout    => "hook-stream-out\n",
            stderr    => "hook-stream-err\n",
            exit_code => 4,
        },
        '_execute_hooks_streaming captures hook stdout, stderr, and exit code',
    );
    is_deeply(
        $result->{last_result},
        {
            file   => $hook_script,
            exit   => 4,
            STDOUT => "hook-stream-out\n",
            STDERR => "hook-stream-err\n",
        },
        '_execute_hooks_streaming records the last streaming hook result for downstream RESULT consumers',
    );
}
{
    no warnings 'redefine';
    local %ENV = %ENV;
    my %seen;
    local *Developer::Dashboard::SkillDispatcher::_execute_hooks_streaming = sub {
        return {
            hooks        => { pre => { stdout => "hook\n", stderr => '', exit_code => 0 } },
            result_state => { pre => { stdout => "hook\n", stderr => '', exit_code => 0 } },
            last_result  => { file => '/tmp/hook', exit => 0, STDOUT => "hook\n", STDERR => '' },
        };
    };
    local *Developer::Dashboard::SkillDispatcher::_exec_resolved_command = sub {
        my ( $self, $cmd_path, $command, $args ) = @_;
        $seen{cmd_path} = $cmd_path;
        $seen{command} = [ @{$command} ];

t/21-refactor-coverage.t  view on Meta::CPAN

    is_deeply( $exec_result->{skill_layers}, [$dep_skill_root], 'exec_command loads the participating skill layers before exec' );
    is( $exec_result->{runtime_layers_loaded}, 1, 'exec_command reloads runtime env layers before replacing the helper process' );
    is_deeply(
        $exec_result->{last},
        { file => '/tmp/hook', exit => 0, STDOUT => "hook\n", STDERR => '' },
        'exec_command forwards the last hook RESULT payload into Runtime::Result',
    );
}
{
    no warnings 'redefine';
    local *Developer::Dashboard::SkillDispatcher::_execute_hooks_streaming = sub { return { error => 'stream hook failure' } };
    is_deeply(
        $dispatcher->exec_command( 'dep-skill', 'run-test' ),
        { error => 'stream hook failure' },
        'exec_command stops before exec when streaming hooks report an error',
    );
}
{
    my ( undef, undef, $exec_error ) = capture {
        local *Developer::Dashboard::SkillDispatcher::_exec_replacement = sub { return 'mock exec failure'; };
        $dispatcher->_exec_resolved_command( '/no/such/path', [ '/definitely/missing-skill-command' ], [] );
    };
    like( $exec_error->{error}, qr/\AUnable to exec \/no\/such\/path: mock exec failure/, '_exec_resolved_command reports direct exec failures clearly' );
}
{
    no warnings 'redefine';
    local *Developer::Dashboard::SkillDispatcher::_execute_hooks_streaming = sub {
        return {
            hooks        => {},
            result_state => {},
        };
    };
    local *Developer::Dashboard::SkillDispatcher::_exec_resolved_command = sub {
        my $last_result = Developer::Dashboard::Runtime::Result::last_result();
        return {
            success     => 1,
            last_result => $last_result,

t/21-refactor-coverage.t  view on Meta::CPAN

        [ 'running', 'failed' ],
        '_install_manifest_file emits running and failed progress events for a broken source',
    );
}
{
    my $default_fail_skill = _create_skill_repo( $test_repos, 'make-default-fail', with_cpanfile => 0 );
    _write_file( File::Spec->catfile( $default_fail_skill, 'Makefile' ), "install:\n\t\@true\n" );
    my $default_make_failure;
    {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            my ( undef, %args ) = @_;
            my $target = @{ $args{command} } > 1 ? join( ' ', @{ $args{command} }[ 1 .. $#{ $args{command} } ] ) : 'default';
            return {
                stdout => '',
                stderr => "synthetic make failure for $target\n",
                exit   => 1,
            };
        };
        $default_make_failure = $manager->_install_skill_makefile($default_fail_skill);
    }

t/21-refactor-coverage.t  view on Meta::CPAN

    like(
        $default_make_failure->{error},
        qr/^Failed to run skill Makefile target 'default' for \Q$default_fail_skill\E: /,
        '_install_skill_makefile names the failing default target in its error message',
    );
}
{
    my $install_target_make_failure;
    {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            my ( undef, %args ) = @_;
            my $target = @{ $args{command} } > 1 ? join( ' ', @{ $args{command} }[ 1 .. $#{ $args{command} } ] ) : 'default';
            return {
                stdout => '',
                stderr => $target eq 'install' ? "synthetic make failure for $target\n" : '',
                exit   => $target eq 'install' ? 1 : 0,
            };
        };
        $install_target_make_failure = $manager->_install_skill_makefile($dep_skill_root);
    }

t/40-install-bootstrap.t  view on Meta::CPAN

    like( $install_ps_text, qr/win-arm64-zip/, 'install.ps1 supports the official Node.js ARM64 ZIP fallback for Windows ARM hosts' );
    like( $install_ps_text, qr/function Install-PortableNodeZip/, 'install.ps1 defines a portable Node.js ZIP installer for Windows ARM fallback installs' );
    like( $install_ps_text, qr/Expand-Archive -LiteralPath \$ZipPath -DestinationPath \$extractRoot -Force/, 'install.ps1 extracts the official Node.js ZIP fallback under the user-space install root on Windows ARM hosts' );
    like( $install_ps_text, qr/Add-ProcessPathSegment -PathSegment \$nodeBin/, 'install.ps1 activates the portable Node.js fallback by prepending its runtime directory to PATH' );
    unlike( $install_ps_text, qr/Invoke-RestMethod\s+-Uri\s+'https:\/\/strawberryperl\.com\/releases\.json'/, 'install.ps1 no longer hard-codes Invoke-RestMethod for the Strawberry Perl release feed' );
    unlike( $install_ps_text, qr/Invoke-RestMethod\s+-Uri\s+'https:\/\/nodejs\.org\/dist\/index\.json'/, 'install.ps1 no longer hard-codes Invoke-RestMethod for the Node.js release index' );
    unlike( $install_ps_text, qr/Invoke-WebRequest\s+-Uri\s+'https:\/\/github\.com\/git-for-windows\/git\/releases\/latest\/download\/Git-64-bit\.exe'/, 'install.ps1 no longer hard-codes Invoke-WebRequest for the stale Git fallback installer download...
    unlike( $install_ps_text, qr/Invoke-WebRequest\s+-Uri\s+'https:\/\/cpanmin\.us\/'/, 'install.ps1 no longer hard-codes Invoke-WebRequest for the cpanm bootstrap script download' );
    like( $install_ps_text, qr/msiexec\.exe/, 'install.ps1 uses msiexec for MSI-based Windows fallback installers' );
    like( $install_ps_text, qr/2>&1 \| ForEach-Object \{ Write-Host \$\_ \}/, 'install.ps1 streams native command output to the host instead of leaking it into helper return values' );
    like( $install_ps_text, qr/\$previousErrorActionPreference\s*=\s*\$ErrorActionPreference/s, 'install.ps1 snapshots PowerShell error handling before streaming native command stderr' );
    like( $install_ps_text, qr/\$ErrorActionPreference\s*=\s*'Continue'/s, 'install.ps1 downgrades native stderr records while streaming command output so git and winget progress do not abort the bootstrap' );
    like( $install_ps_text, qr/Refresh-ProcessPathFromEnvironment/, 'install.ps1 refreshes the current PATH after winget installs new tools' );
    like( $install_ps_text, qr/cpanmin\.us/, 'install.ps1 bootstraps cpanm for Windows installs from the standalone cpanmin.us script' );
    unlike( $install_ps_text, qr/'local::lib'\s*,\s*'App::cpanminus'/, 'install.ps1 no longer tries to self-install App::cpanminus while the downloaded cpanm script is still running on Windows' );
    like( $install_ps_text, qr{https://github\.com/manif3station/developer-dashboard\.git}, 'install.ps1 knows the canonical GitHub repository for the streamed Windows bootstrap source' );
    like( $install_ps_text, qr/git\s+clone/s, 'install.ps1 clones the current GitHub master source for the default streamed Windows install target' );
    like( $install_ps_text, qr/Push-Location\s+\$effectiveCpanTarget/s, 'install.ps1 installs the default cloned Windows checkout from inside the local checkout directory' );
    like( $install_ps_text, qr/Sync-LocalLibEnvironmentFromPerl/, 'install.ps1 delegates local::lib environment setup to perl so Windows install paths stay canonical' );
    like(
        $install_ps_text,
        qr/'--notest',\s*'--local-lib-contained',\s*\$InstallRoot,\s*'local::lib',\s*'File::ShareDir::Install'/s,

t/43-explicit-coverage-qa.t  view on Meta::CPAN

    my $cpan_root = tempdir( CLEANUP => 1 );
    my $cpanfile = File::Spec->catfile( $cpan_root, 'cpanfile' );
    open my $cpan_fh, '>', $cpanfile or die $!;
    print {$cpan_fh} "requires 'Test::More';\n";
    close $cpan_fh;
    my $original_cwd = Cwd::getcwd();
    my $cpan_error = do {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_shared_perl_root = sub { return File::Spec->catdir( $cpan_root, 'perl5-shared' ) };
        local *Developer::Dashboard::SkillManager::_ensure_perl_root = sub { return $_[1] };
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            return {
                stdout => '',
                stderr => "cpan boom\n",
                exit   => 1,
            };
        };
        eval { $manager->_install_skill_cpanfile($cpan_root); 1 } ? '' : $@;
    };
    is( $cpan_error, '', '_install_skill_cpanfile reports streaming failures as structured errors instead of throwing raw exceptions' );
    my $cpan_failure = do {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_shared_perl_root = sub { return File::Spec->catdir( $cpan_root, 'perl5-shared' ) };
        local *Developer::Dashboard::SkillManager::_ensure_perl_root = sub { return $_[1] };
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            return {
                stdout => '',
                stderr => "cpan boom\n",
                exit   => 1,
            };
        };
        $manager->_install_skill_cpanfile($cpan_root);
    };
    like( $cpan_failure->{error}, qr/cpan boom/, '_install_skill_cpanfile surfaces the streamed cpanm failure text in the structured error result' );
    is( Cwd::getcwd(), $original_cwd, '_install_skill_cpanfile restores the original cwd after an in-flight failure' );

    my $cpan_local_root = tempdir( CLEANUP => 1 );
    my $cpanfile_local = File::Spec->catfile( $cpan_local_root, 'cpanfile.local' );
    open my $cpan_local_fh, '>', $cpanfile_local or die $!;
    print {$cpan_local_fh} "requires 'Test::More';\n";
    close $cpan_local_fh;
    my $cpan_local_error = do {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_skill_local_perl_root = sub { return File::Spec->catdir( $cpan_local_root, 'perl5-local' ) };
        local *Developer::Dashboard::SkillManager::_ensure_perl_root = sub { return $_[1] };
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            return {
                stdout => '',
                stderr => "cpan local boom\n",
                exit   => 1,
            };
        };
        eval { $manager->_install_skill_cpanfile_local($cpan_local_root); 1 } ? '' : $@;
    };
    is( $cpan_local_error, '', '_install_skill_cpanfile_local reports streaming failures as structured errors instead of throwing raw exceptions' );
    my $cpan_local_failure = do {
        no warnings 'redefine';
        local *Developer::Dashboard::SkillManager::_skill_local_perl_root = sub { return File::Spec->catdir( $cpan_local_root, 'perl5-local' ) };
        local *Developer::Dashboard::SkillManager::_ensure_perl_root = sub { return $_[1] };
        local *Developer::Dashboard::SkillManager::_run_streaming_command = sub {
            return {
                stdout => '',
                stderr => "cpan local boom\n",
                exit   => 1,
            };
        };
        $manager->_install_skill_cpanfile_local($cpan_local_root);
    };
    like( $cpan_local_failure->{error}, qr/cpan local boom/, '_install_skill_cpanfile_local surfaces the streamed cpanm failure text in the structured error result' );
    is( Cwd::getcwd(), $original_cwd, '_install_skill_cpanfile_local restores the original cwd after an in-flight failure' );

t/45-skills-progress-platform.t  view on Meta::CPAN

The skills install progress board became noisy and misleading because it showed
every package-manager step regardless of the current operating system. This
test exists to stop regressions where Linux users see irrelevant Brew or
Winget progress rows, macOS users see Apt rows, or dependency output floods
the whole terminal instead of staying capped.

=head1 WHEN TO USE

Use this test when changing the skills install progress task list, the
platform-detection logic behind package-manager filtering, or the detail-line
limit used while streaming dependency installation progress.

=head1 HOW TO USE

Run this file directly with C<prove -lv t/45-skills-progress-platform.t> when
iterating on the progress board behaviour, or let it run through the full test
suite to confirm the regression stays covered across the release gates. The
test forces synthetic Linux and macOS host markers through environment
variables and inspects the captured progress object produced by the skills
helper.

t/lib/Local/PSGITest.pm  view on Meta::CPAN

        SERVER_PORT     => defined $port ? $port : ( $scheme eq 'https' ? 443 : 80 ),
        SERVER_PROTOCOL => 'HTTP/1.1',
        REMOTE_ADDR     => defined $host ? $host : '127.0.0.1',
        'psgi.version'      => [ 1, 1 ],
        'psgi.url_scheme'   => $scheme,
        'psgi.input'        => $input,
        'psgi.errors'       => IO::Handle->new_from_fd( fileno(STDERR), 'w' ),
        'psgi.multithread'  => 0,
        'psgi.multiprocess' => 0,
        'psgi.run_once'     => 0,
        'psgi.streaming'    => 1,
        'psgi.nonblocking'  => 0,
    );

    my $content_type = $headers->header('Content-Type');
    my $content_length = length $content;
    $env{CONTENT_TYPE}   = $content_type if defined $content_type;
    $env{CONTENT_LENGTH} = $content_length if $content_length;

    for my $field ( $headers->header_field_names ) {
        next if !defined $field;

t/lib/Local/PSGITest.pm  view on Meta::CPAN

C<Test::SharedFork> dependency chain on platforms such as Windows.

=head1 WHAT IT IS

This is a repository-owned test helper module under C<t/lib/>. It is not part
of the public runtime API and exists only to support the shipped test suite.

=head1 WHAT IT IS FOR

It provides a stable, local PSGI harness so the repository can test PSGI
routes, delayed responses, and streaming bodies without adding install-time
test dependency chains to the public distribution metadata.

=head1 PURPOSE

Keep the repository PSGI tests self-contained while preventing end-user
installers from pulling C<Plack::Test>, C<Test::TCP>, and
C<Test::SharedFork> through packaged prerequisite metadata.

=head1 FUNCTIONS

t/lib/Local/PSGITest.pm  view on Meta::CPAN

=item * C<$callback> - coderef receiving a request callback
=back

Expected output:
Returns whatever C<$callback> returns.

=head2 request($app, $request)

What it does:
Converts an C<HTTP::Request> object into a PSGI environment, runs the PSGI
application, resolves delayed or streaming responses, and returns an
C<HTTP::Response>.

Input arguments:
=over 4
=item * C<$app> - PSGI application coderef
=item * C<$request> - C<HTTP::Request> instance
=back

Expected output:
An C<HTTP::Response> object containing the PSGI status, headers, and body.



( run in 0.966 second using v1.01-cache-2.11-cpan-995e09ba956 )