Developer-Dashboard

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      resolver lookup, toggle writes, and exported `DDDC` paths
4.09  2026-06-06
    - keep smart-routed skill page browser edits on their canonical
      `/app/<skill>` and nested `/app/<skill>/<sub-skill>` aliases by
      teaching the edit/source routes and Play handoff to load skill index
      pages through the same smart resolver instead of collapsing to the
      underlying `BOOKMARK: index` id
4.08  2026-06-06
    - split the Web UI bookmark editor into Jupyter-like section blocks while
      keeping the saved bookmark file format canonical, including block-aware
      syntax overlays, Tab-created sections, hidden source recomposition, and
      Play-mode save-and-render handoff
    - standardize operator-facing built-in CLI output so path, file, api, and
      skill inventory or mutation commands default to readable table summaries
      while `-o json` returns the full raw machine payload
4.04  2026-06-05
    - restore the public switchboard helper path contract for dotted
      `dashboard <skill>.<command>` dispatch, keep Windows core-backed
      built-ins routed through the shared staged `_dashboard-core` helper,
      and fix the staged private core import drift that broke `dashboard init`
      and later helper refreshes

README.md  view on Meta::CPAN

- bookmark `CODE*` execution with captured `STDOUT` rendered into the page and
captured `STDERR` rendered as visible errors
- per-page sandpit isolation so one bookmark run can share runtime
variables across `CODE*` blocks without leaking them into later page runs
- old-style root editor behavior with a free-form bookmark textarea when no path is provided
- file-backed collectors and indicators
- prompt rendering for `PS1` and the PowerShell `prompt` function
- project/path discovery helpers
- a lightweight local web interface
- action execution with trusted and safer page boundaries
- config-backed providers, path aliases, and compose overlays
- update scripts and installable runtime packaging

Managed runtime children are expected to clean up after themselves. Detached
web startup helpers, collector loops, the collector watchdog supervisor, the
SSL frontend connection workers, and background page actions now reap the
direct children they own instead of leaving zombie processes behind on hosts
such as macOS and WSL. Collector loops and the collector watchdog supervisor
also reap those children immediately on `SIGCHLD`, so long-interval
collectors and orphaned watchdogs do not leave visible `<defunct>`
dashboard processes behind until some later housekeeping pass. Managed

README.md  view on Meta::CPAN


- Update Manager

    `Developer::Dashboard::UpdateManager` runs ordered update scripts and
    restarts validated collector loops when needed, giving the runtime a
    controlled bootstrap and upgrade path.

- Docker Compose Resolver

    `Developer::Dashboard::DockerCompose` resolves project-aware compose files,
    explicit overlay layers, services, addons, modes, env injection, and the
    final `docker compose` command so container workflows can live inside the
    same dashboard ecosystem instead of in separate wrapper scripts.

## Environment Variables

The distribution supports these compatibility-style customization variables:

- `DEVELOPER_DASHBOARD_BOOKMARKS`

    Override the saved page root.

README.md  view on Meta::CPAN

Bookmark documents use the original separator-line format with directive
headers such as `TITLE:`, `STASH:`, `HTML:`, and `CODE1:`.

Posting a bookmark document with `BOOKMARK: some-id` back through the root
editor now saves it to the bookmark store so `/app/some-id` resolves it
immediately.

The browser editor now loads one visible block per bookmark section while it
keeps the canonical separator-based bookmark source in a hidden form field for
saves and play requests. Each visible block still uses the syntax-highlighted
overlay viewport that follows the real textarea scroll position by transform
instead of a second scrollbox, so long lines, full-text selection, and caret
placement stay aligned with the real editor. Pressing `Tab` inside one block
starts the next section block, and the browser recomposes those blocks back
into the original separator-line bookmark document before it posts or plays
the page. The directive assist still understands `:---` when you paste older
single-textarea workflows into one block, expanding it to the full separator
line and seeding the next sensible directive.

Edit and source views preserve raw Template Toolkit placeholders inside
`HTML:` sections, so values such as `[% title %]` are kept in the bookmark

README.md  view on Meta::CPAN

During compose execution the dashboard exports `DDDC` as the runtime
`config/docker` directory for the current runtime, so compose YAML can keep using
`${DDDC}` paths inside the YAML itself. Project-local isolated services are
discovered from that same
`./.developer-dashboard/config/docker/<service>/...` tree. Wrapper flags such as
`--service`, `--addon`, `--mode`, `--project`, and `--dry-run` are
consumed first, and all remaining docker compose flags such as `-d` and
`--build` pass straight through to the real `docker compose` command.
If one resolved service comes from an installed skill docker root, the
resolver also loads that skill's `<skill-root>/.env` file into the compose
environment before docker-config, addon, and mode env overlays are applied.
Only skills whose compose service files actually participate are included,
disabled skills are skipped, and `<skill-root>/.env.pl` is not executed from
this compose path. Nested skill services expand their env chain from the root
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

README.md  view on Meta::CPAN


- `DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS`

    Allows browser execution of transient `/?token=...`, `/action?atoken=...`,
    and older `/ajax?token=...` payloads. The default is off, so the web UI only
    executes saved bookmark files unless this is set to a truthy value such as
    `1`, `true`, `yes`, or `on`.

Collector definitions come only from dashboard configuration JSON, so config
remains the single source of truth for path aliases, providers, collectors,
and Docker compose overlays.

## Testing And Coverage

Run the test suite:

    prove -lr t

Measure library coverage with Devel::Cover:

    cpanm --no-wget --notest --local-lib-contained ./.perl5 Devel::Cover

doc/architecture.md  view on Meta::CPAN

  Owns the explicit Dancer2 HTTP route table, normalizes requests, enforces
  protected-route authorization, and forwards work into the web-app service.

- `Developer::Dashboard::Web::Server`
  PSGI web server wrapper for the Dancer2 app, defaulting to bind `0.0.0.0:7890`.

- `Developer::Dashboard::UpdateManager`
  Runs ordered update scripts, stops running collectors, and restarts them afterward.

- `Developer::Dashboard::DockerCompose`
  Resolves compose base files plus explicit project, service, addon, and mode overlays, env injection, and the final `docker compose` command.
  It discovers isolated service folders from layered `config/docker` roots,
  including project-local `./.developer-dashboard/config/docker`, and exports `DDDC` for
  compose-time references to the runtime `config/docker` directory inside YAML.

## Runtime Model

The architecture follows a producer/consumer pattern:

- collectors prepare data in the background
- indicators and pages read cached state

doc/architecture.md  view on Meta::CPAN

- home-runtime helper staging is non-destructive: `dashboard init` may add or update dashboard-managed built-in helpers under `~/.developer-dashboard/cli/dd/`, but it must preserve the separate user command and hook space under `~/.developer-dashboar...
- dashboard-managed helper and starter-page refreshes are MD5-aware inside Perl, so `dashboard init` skips rewriting a dashboard-managed helper or shipped starter page when the existing file already matches the shipped content digest
- starter-page refreshes are safe across upgrades too: `dashboard init` records the md5 of the last dashboard-managed shipped starter-page copy under the active runtime config tree, refreshes only pages that still match that recorded or bridged histo...
- `dashboard init` seeds the current dashboard-managed starter pages as normal editable saved bookmarks, but rerunning it preserves an existing `~/.developer-dashboard/config/config.json` instead of overwriting user config; if the file is missing, in...
- the public `dashboard` entrypoint stays thin for all built-in commands: the shipped starter bookmark source plus helper script source live under `share/seeded-pages/` and `share/private-cli/`, dedicated helper bodies cover `jq`/`yq`/`of`/`open-file...
- `dashboard cpan <Module...>` installs optional runtime modules into `./.developer-dashboard/local` and appends them to `./.developer-dashboard/cpanfile`, while keeping that support in the `dashboard` entrypoint and having saved Ajax workers infer `...
- `dashboard serve logs` exposes the combined Dancer2 and Starman runtime log stored in the dashboard log file, with `-n N` tailing and `-f` follow mode
- `dashboard serve workers N` persists the default Starman worker count in config and auto-starts the web service when it is currently stopped; `--host HOST` and `--port PORT` steer that auto-start path, while `dashboard serve --workers N` and `dashb...
- the editor view auto-submits once focus leaves the whole editor form instead of relying on a visible update button
- the editor splits bookmark sections into separate visible blocks but still recomposes the canonical separator-based source into a hidden `instruction` field before save and play submits
- each visible editor block keeps a plain escaped source overlay with wrapping disabled so the visible text geometry stays identical to the real textarea during long bookmark edits

## Environment Overrides

The core supports compatibility-style environment overrides for project customization:

- `DEVELOPER_DASHBOARD_BOOKMARKS`
  Saved page/bookmark root.

- `DEVELOPER_DASHBOARD_CHECKERS`
  Filter for enabled collector/checker names using colon-separated values.

doc/testing.md  view on Meta::CPAN

built distribution with the packaged-tree and blank-environment gates.
The installer guardrails in `t/40-install-bootstrap.t` also treat the Unix
bootstrap target as a compatibility contract: checkout or extracted-tarball
runs must install `.` locally, streamed `curl ... | sh` runs with no checkout
must clone the current GitHub `master` checkout instead of silently falling
back to a stale CPAN release, and the shipped bootstrap package manifests must
carry `tmux` because `dashboard workspace` is a first-party tmux workflow.

Branch and condition reports are still generated and should be used to drive new edge-case tests, especially when adding new runtime modules.

Frontend editor changes should also be checked in a real browser route, not just from HTML output. In particular, the split bookmark editor must load one visible block per section while still recomposing the canonical separator-based source into the ...

JSON behavior is exercised through the shared `Developer::Dashboard::JSON` wrapper, which now uses `JSON::XS`.
Release metadata checks also verify that built tarball runtime prerequisites
explicitly include `JSON::XS`.
When a code change introduces a new runtime Perl module, declare it in all
three release metadata sources in the same change: `Makefile.PL`, `cpanfile`,
and `dist.ini`. The release metadata guardrail fails if a required non-core
runtime module is missing from one of those files, so dependency drift is
caught before `dzil build`, blank-environment installs, or CI releases.
The blank-container `cpanm` gate is an install-verification pass against the

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

=item *

a lightweight local web interface

=item *

action execution with trusted and safer page boundaries

=item *

config-backed providers, path aliases, and compose overlays

=item *

update scripts and installable runtime packaging

=back

Managed runtime children are expected to clean up after themselves. Detached
web startup helpers, collector loops, the collector watchdog supervisor, the
SSL frontend connection workers, and background page actions now reap the

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


=item * Update Manager

C<Developer::Dashboard::UpdateManager> runs ordered update scripts and
restarts validated collector loops when needed, giving the runtime a
controlled bootstrap and upgrade path.

=item * Docker Compose Resolver

C<Developer::Dashboard::DockerCompose> resolves project-aware compose files,
explicit overlay layers, services, addons, modes, env injection, and the
final C<docker compose> command so container workflows can live inside the
same dashboard ecosystem instead of in separate wrapper scripts.

=back

=head2 Environment Variables

The distribution supports these compatibility-style customization variables:

=over 4

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

Bookmark documents use the original separator-line format with directive
headers such as C<TITLE:>, C<STASH:>, C<HTML:>, and C<CODE1:>.

Posting a bookmark document with C<BOOKMARK: some-id> back through the root
editor now saves it to the bookmark store so C</app/some-id> resolves it
immediately.

The browser editor now loads one visible block per bookmark section while it
keeps the canonical separator-based bookmark source in a hidden form field for
saves and play requests. Each visible block still uses the syntax-highlighted
overlay viewport that follows the real textarea scroll position by transform
instead of a second scrollbox, so long lines, full-text selection, and caret
placement stay aligned with the real editor. Pressing C<Tab> inside one block
starts the next section block, and the browser recomposes those blocks back
into the original separator-line bookmark document before it posts or plays
the page. The directive assist still understands C<:---> when you paste older
single-textarea workflows into one block, expanding it to the full separator
line and seeding the next sensible directive.

Edit and source views preserve raw Template Toolkit placeholders inside
C<HTML:> sections, so values such as C<[% title %]> are kept in the bookmark

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

During compose execution the dashboard exports C<DDDC> as the runtime
C<config/docker> directory for the current runtime, so compose YAML can keep using
C<${DDDC}> paths inside the YAML itself. Project-local isolated services are
discovered from that same
C<./.developer-dashboard/config/docker/E<lt>serviceE<gt>/...> tree. Wrapper flags such as
C<--service>, C<--addon>, C<--mode>, C<--project>, and C<--dry-run> are
consumed first, and all remaining docker compose flags such as C<-d> and
C<--build> pass straight through to the real C<docker compose> command.
If one resolved service comes from an installed skill docker root, the
resolver also loads that skill's F<E<lt>skill-rootE<gt>/.env> file into the compose
environment before docker-config, addon, and mode env overlays are applied.
Only skills whose compose service files actually participate are included,
disabled skills are skipped, and F<E<lt>skill-rootE<gt>/.env.pl> is not executed from
this compose path. Nested skill services expand their env chain from the root
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

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


Allows browser execution of transient C</?token=...>, C</action?atoken=...>,
and older C</ajax?token=...> payloads. The default is off, so the web UI only
executes saved bookmark files unless this is set to a truthy value such as
C<1>, C<true>, C<yes>, or C<on>.

=back

Collector definitions come only from dashboard configuration JSON, so config
remains the single source of truth for path aliases, providers, collectors,
and Docker compose overlays.

=head2 Testing And Coverage

Run the test suite:

  prove -lr t

Measure library coverage with Devel::Cover:

  cpanm --no-wget --notest --local-lib-contained ./.perl5 Devel::Cover

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

    my ( $class, %args ) = @_;
    my $config = $args{config} || die 'Missing config';
    my $paths  = $args{paths}  || die 'Missing path registry';
    return bless {
        config => $config,
        paths  => $paths,
    }, $class;
}

# resolve(%args)
# Resolves the effective docker compose context and overlay stack.
# Input: optional project_root, addons, modes, services, and compose args.
# Output: hash reference describing files, env, layers, precedence, and final command.
sub resolve {
    my ( $self, %args ) = @_;
    my $project_root = $args{project_root} || $self->{paths}->current_project_root || cwd();
    my $docker_cfg  = $self->{config}->docker_config;
    my $docker_root = $self->_docker_config_root;
    my @passthrough = @{ $args{args} || [] };
    my @compose_files = ();
    my @layers;

    my @base = $self->_discover_base_files($project_root);
    push @compose_files, @base;
    push @layers, { name => 'base', files => [@base] };

    my @project_overlays = ( @{ $docker_cfg->{files} || [] }, @{ $docker_cfg->{project_overlays} || [] } );
    push @compose_files, @project_overlays;
    push @layers, { name => 'project', files => [@project_overlays] } if @project_overlays;

    my @addons = @{ $args{addons} || [] };
    my @modes  = @{ $args{modes}  || [] };
    my @services = @{ $args{services} || [] };

    my %addon_map = (
        %{ $docker_cfg->{addons} || {} },
    );
    my %mode_map = (
        %{ $docker_cfg->{modes} || {} },

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

            project_root => $args{project_root},
            service      => $_,
        )
    } @services;
}

# _resolve_skill_service_env(%args)
# Loads .env files from installed skill roots whose config/docker/<service>
# folder contributes one compose file to the effective service stack.
# Input: project_root and ordered service list array reference.
# Output: hash reference with loaded env file list and env overlay hash.
sub _resolve_skill_service_env {
    my ( $self, %args ) = @_;
    my $project_root = $args{project_root} || cwd();
    my @services     = @{ $args{services} || [] };
    return { files => [], env => {} } if !@services;

    my @skill_layers;
    my %env;
    my %seen;
    for my $service (@services) {

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

    return $class->_load_skill_layer_specs(
        specs => $class->_skill_layer_specs( @{ $args{skill_layers} || [] } ),
    );
}

# load_skill_layers_into_hash(%args)
# Loads the ordered nested skill env chain into an isolated temporary
# environment and returns only the added or changed keys.
# Input: hash with skill_layers => array reference of skill root paths and
# optional base_env => hash reference of starting environment values.
# Output: hash reference with loaded file list and env overlay hash.
sub load_skill_layers_into_hash {
    my ( $class, %args ) = @_;
    my $base_env = ref( $args{base_env} ) eq 'HASH' ? { %{ $args{base_env} } } : { %ENV };
    my %before = %{$base_env};

    local %ENV = %{$base_env};
    local $ENV{DEVELOPER_DASHBOARD_ENV_AUDIT};
    local %Developer::Dashboard::EnvAudit::AUDIT;

    my $loaded = $class->_load_skill_layer_specs(
        specs => $class->_skill_layer_specs( @{ $args{skill_layers} || [] } ),
    );
    my %overlay;
    for my $key ( sort keys %ENV ) {
        next
          if exists $before{$key}
          && (
               ( !defined $before{$key} && !defined $ENV{$key} )
            || ( defined $before{$key} && defined $ENV{$key} && $before{$key} eq $ENV{$key} )
          );
        $overlay{$key} = $ENV{$key};
    }

    return {
        files => $loaded,
        env   => \%overlay,
    };
}

# load_files(%args)
# Loads a specific ordered list of .env and .env.pl files, updating both %ENV
# and the shared EnvAudit inventory.
# Input: hash with files => array reference of candidate file paths.
# Output: ordered array reference of the env files that were actually loaded.
sub load_files {
    my ( $class, %args ) = @_;

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

        push @loaded, $file;
    }
    return \@loaded;
}

# load_files_into_hash(%args)
# Loads a specific ordered list of env files into an isolated temporary
# environment and returns only the added or changed keys.
# Input: hash with files => array reference of candidate file paths and
# optional base_env => hash reference of starting environment values.
# Output: hash reference with loaded file list and env overlay hash.
sub load_files_into_hash {
    my ( $class, %args ) = @_;
    my $base_env = ref( $args{base_env} ) eq 'HASH' ? { %{ $args{base_env} } } : { %ENV };
    my %before = %{$base_env};

    local %ENV = %{$base_env};
    local $ENV{DEVELOPER_DASHBOARD_ENV_AUDIT};
    local %Developer::Dashboard::EnvAudit::AUDIT;

    my $loaded = $class->load_files( files => $args{files} );
    my %overlay;
    for my $key ( sort keys %ENV ) {
        next
          if exists $before{$key}
          && (
               ( !defined $before{$key} && !defined $ENV{$key} )
            || ( defined $before{$key} && defined $ENV{$key} && $before{$key} eq $ENV{$key} )
          );
        $overlay{$key} = $ENV{$key};
    }

    return {
        files => $loaded,
        env   => \%overlay,
    };
}

# _plain_directory_env_files($paths)
# Builds the env file list contributed by ancestor directories from the active
# root toward the current working directory.
# Input: path registry object.
# Output: ordered list of plain directory env file paths.
sub _plain_directory_env_files {
    my ( $class, $paths ) = @_;

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

  <style>
    body { margin: 0; font-family: Georgia, serif; background: #f5efe2; color: #1f2a2e; }
    main { max-width: 980px; margin: 32px auto; background: #fffef9; border: 1px solid #ddd3c2; padding: 24px; }
    .editor-stack {
      position: relative;
      min-height: 520px;
      border: 1px solid #2a2f36;
      background: #1f2328;
      overflow: hidden;
    }
    .editor-overlay,
    .instruction-block-editor {
      display: block;
      width: 100%;
      min-height: 520px;
      box-sizing: border-box;
      margin: 0;
      padding: 12px;
      font-family: Menlo, Consolas, "Courier New", monospace;
      font-size: 14px;
      line-height: 21px;
      white-space: pre;
      word-break: normal;
      overflow-wrap: normal;
      overflow: auto;
      tab-size: 4;
      letter-spacing: 0;
    }
    .editor-overlay-viewport {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      background: transparent;
    }
    .editor-overlay {
      position: absolute;
      top: 0;
      left: 0;
      min-width: 100%;
      color: #e6edf3;
      background: transparent;
      unicode-bidi: plaintext;
      direction: ltr;
      will-change: transform;
    }

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

  const label = document.createElement('div');
  label.className = 'editor-block-label';
  label.textContent = ddBlockLabel(text, index);
  wrapper.appendChild(label);

  const stack = document.createElement('div');
  stack.className = 'editor-stack';
  wrapper.appendChild(stack);

  const viewport = document.createElement('div');
  viewport.className = 'editor-overlay-viewport';
  viewport.setAttribute('aria-hidden', 'true');
  stack.appendChild(viewport);

  const highlight = document.createElement('pre');
  highlight.className = 'editor-overlay';
  viewport.appendChild(highlight);

  const editor = document.createElement('textarea');
  editor.className = 'instruction-block-editor';
  editor.wrap = 'off';
  editor.spellcheck = false;
  editor.autocapitalize = 'off';
  editor.autocomplete = 'off';
  editor.autocorrect = 'off';
  editor.value = text;

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

if (ddPlayButton) {
  ddPlayButton.addEventListener('click', function() {
    ddMode.value = 'render';
    ddComposeInstruction();
    ddForm.submit();
  });
}
window.addEventListener('resize', function() {
  Array.prototype.slice.call(ddBlocks.querySelectorAll('.editor-block')).forEach(function(block) {
    const editor = block.querySelector('.instruction-block-editor');
    const highlight = block.querySelector('.editor-overlay');
    ddAutoResizeEditor(editor);
    ddSyncEditorOverlay(editor, highlight);
  });
});
ddSource.value = __SOURCE_JSON__;
ddLoadBlocks(ddSource.value);
</script>
</body>
</html>
HTML

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

# _highlight_instruction_html($source)
# Generates the initial syntax-coloured editor HTML from bookmark source text.
# Input: canonical bookmark instruction text.
# Output: highlighted HTML string for the editable source area.
sub _highlight_instruction_html {
    my ( $self, $source ) = @_;
    my %state = ( section => '', html_mode => '' );
    return join "\n", map { $self->_highlight_editor_line( $_, \%state ) } split /\n/, ( $source // '' ), -1;
}

# _editor_overlay_html($source)
# Generates the browser overlay HTML while preserving the textarea's final blank line geometry.
# Input: canonical bookmark instruction text.
# Output: highlighted HTML string with a trailing sentinel when the source ends in a newline.
sub _editor_overlay_html {
    my ( $self, $source ) = @_;
    my $html = $self->_highlight_instruction_html($source);
    $html .= ' ' if defined $source && $source =~ /\n\z/;
    return $html;
}

# _highlight_editor_line($line, $state)
# Highlights a single bookmark editor line while preserving exact layout.
# Input: raw line text and mutable parser state hash reference.
# Output: highlighted HTML fragment for that line.

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

    path        => '/',
    method      => 'POST',
    body        => 'instruction=TITLE%3A%20Sample%20Dashboard%0A%3A--------------------------------------------------------------------------------%3A%0ABOOKMARK%3A%20index%0A%3A------------------------------------------------------------------------...
    remote_addr => '127.0.0.1',
    headers     => { host => '127.0.0.1' },
) };
is($code1d_tt, 200, 'posted TT bookmark instruction route ok');
like($body1d_tt, qr/\[% title %\]/, 'editor preserves TT placeholders in the posted source view');
like($body1d_tt, qr/HTML:\s*&lt;h1&gt;\[% title %\]&lt;\/h1&gt;/s, 'editor textarea keeps TT placeholders inside HTML sections');
like($body1d_tt, qr/ddSource\.value = "[^"]*\[% title %\][^"]*"\s*;\s*ddLoadBlocks\(ddSource\.value\);/s, 'editor boot script keeps TT placeholders in the browser-loaded hidden instruction source before it splits blocks');
like($app->_editor_overlay_html("HTML: <h1>[% title %]</h1>\n"), qr/<span class="tok-directive">HTML:<\/span>\s*<span class="tok-tag">&lt;h1<\/span><span class="tok-tag">&gt;<\/span><span class="tok-note">\[% title %\]<\/span>/s, 'editor syntax highl...
my ($code1d_tt_source, $type1d_tt_source, $body1d_tt_source) = @{ $app->handle(
    path        => '/app/index/source',
    query       => '',
    remote_addr => '127.0.0.1',
    headers     => { host => '127.0.0.1' },
) };
is($code1d_tt_source, 200, 'saved TT bookmark source route ok');
like($type1d_tt_source, qr/text\/plain/, 'saved TT bookmark source route returns plain text');
like($body1d_tt_source, qr/^HTML:\s+<h1>\[% title %\]<\/h1> \[% stash\.foo %\]$/m, 'saved TT bookmark source route preserves raw TT placeholders');
my ($play_url_tt) = $body1d_tt =~ m{<button type="button" class="chrome-button" id="play-button" data-play-url="([^"]+)">Play</button>};

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

    '';
my ($code1e, undef, $body1e) = @{ $app->handle(
    path        => '/',
    method      => 'POST',
    body        => 'instruction=' . uri_escape($highlight_source),
    remote_addr => '127.0.0.1',
    headers     => { host => '127.0.0.1' },
) };
is($code1e, 200, 'highlight demo route ok');
like($body1e, qr/wrap="off"/, 'editor textarea disables soft wrapping so long bookmark lines keep exact geometry');
like($body1e, qr/white-space:\s*pre;/, 'editor stack keeps preformatted line geometry instead of wrapping overlay lines differently from the textarea');
like($body1e, qr/viewport\.className = 'editor-overlay-viewport';/, 'editor route builds a clipped overlay viewport for each visible block');
like($body1e, qr/function ddSyncEditorOverlay\(editor, highlight\)/, 'editor route exposes a dedicated overlay sync helper for one block overlay');
like($body1e, qr/function ddAutoResizeEditor\(editor\)/, 'editor route exposes a dedicated auto-resize helper for each block textarea');
like($body1e, qr/editor\.style\.height = 'auto';\s*editor\.style\.height = Math\.max\(editor\.scrollHeight, 48\) \+ 'px';/s, 'editor route grows each block textarea to match its content height');
like($body1e, qr/highlight\.style\.transform = 'translate\('/, 'editor route syncs each block overlay position through transforms instead of a second scrollbox');
like($body1e, qr/function ddCreateEditorBlock\(/, 'editor route builds visible block editors dynamically from bookmark sections');
like($body1e, qr/function ddRenderEditor\(editor, highlight\) \{\s*highlight\.innerHTML = ddOverlayHtml\(editor\.value\);\s*ddAutoResizeEditor\(editor\);\s*ddSyncEditorOverlay\(editor, highlight\);/s, 'editor route auto-resizes a block before syncing...
like($body1e, qr/window\.addEventListener\('resize', function\(\) \{\s*Array\.prototype\.slice\.call\(ddBlocks\.querySelectorAll\('\.editor-block'\)\)\.forEach\(function\(block\) \{\s*const editor = block\.querySelector\('\.instruction-block-editor'\...
my $demo_overlay = $app->_editor_overlay_html($highlight_source);
like($demo_overlay, qr/<span class="tok-directive">HTML:<\/span>/, 'editor overlay highlights bookmark directives');
like($demo_overlay, qr/<span class="tok-tag">&lt;style<\/span>/, 'editor overlay highlights HTML tag names');
like($demo_overlay, qr/<span class="tok-js">const<\/span> run = 1;/, 'editor overlay highlights JavaScript keywords');
like($demo_overlay, qr/<span class="tok-note">\[% stash\.name %\]<\/span>/, 'editor overlay highlights TT placeholders inside HTML sections');

my $broken_editor_source = <<'BOOKMARK';
BOOKMARK: test
:--------------------------------------------------------------------------------:
HTML: <script src="/js/jquery.js"></script>
<script>var foo = {};
$(document).ready(function () {
    let lastLength = 0;

    $.ajax({

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

BOOKMARK
my ( $broken_editor_code, undef, $broken_editor_body ) = @{ $app->handle(
    path        => '/',
    method      => 'POST',
    body        => 'instruction=' . uri_escape($broken_editor_source),
    remote_addr => '127.0.0.1',
    headers     => { host => '127.0.0.1' },
) };
is( $broken_editor_code, 200, 'exact bookmark editor repro route ok' );
like( $broken_editor_body, qr/Stream error:/, 'exact bookmark editor repro keeps the original bookmark text visible in the editor route' );
my $broken_editor_overlay = $app->_editor_overlay_html($broken_editor_source);
like( $broken_editor_overlay, qr/<span class="tok-js">let<\/span> lastLength = 0;/, 'exact bookmark editor repro keeps the JavaScript source text visible in the editor overlay' );
like( $broken_editor_overlay, qr/<span class="tok-string">'Stream error:'<\/span>/, 'exact bookmark editor repro highlights JavaScript string text in the overlay' );
like( $broken_editor_overlay, qr/<span class="tok-string">'GET'<\/span>/, 'exact bookmark editor repro highlights JavaScript string literals without leaking markup text' );
unlike( $broken_editor_overlay, qr/class=&quot;tok-string&quot;&gt;GET/, 'exact bookmark editor repro no longer leaks span attribute text into the visible editor overlay' );
unlike( $broken_editor_overlay, qr/\x1EHL\d+\x1E/, 'exact bookmark editor repro does not leak placeholder markers into the overlay output' );

my ($code2, $type2, $body2) = @{ $app->handle(path => '/app/welcome', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code2, 200, 'saved page route ok');
like($body2, qr/Welcome/, 'saved page rendered');
unlike($body2, qr{<h1>\s*Welcome\s*</h1>}, 'page title is not injected into the page body');
like($body2, qr{<title>Welcome</title>}, 'page title is still rendered in the head title element');
unlike($body2, qr/id="logout-url"/, 'admin route does not render logout link');

my ($code2b, undef, $body2b) = @{ $app->handle(path => '/app/welcome', query => 'name=Michael', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' }) };
is($code2b, 200, 'saved page with query state route ok');

t/07-core-units.t  view on Meta::CPAN

    my $paths = Developer::Dashboard::PathRegistry->new( home => $bad_home );
    dies_like(
        sub { Developer::Dashboard::EnvLoader->load_runtime_layers( paths => $paths ) },
        qr/did not return a true value/,
        'EnvLoader propagates .env.pl execution failures instead of hiding them',
    );
    chdir $previous_cwd or die "Unable to chdir back to $previous_cwd: $!";
}

{
    my $overlay_dir = tempdir( CLEANUP => 1 );
    my $env_file = File::Spec->catfile( $overlay_dir, '.env' );
    my $env_pl_file = File::Spec->catfile( $overlay_dir, '.env.pl' );
    open my $env_fh, '>:raw', $env_file or die "Unable to write overlay env file: $!";
    print {$env_fh} "VERSION=leaf\nNEW_ONLY=from-file\n";
    close $env_fh or die "Unable to close overlay env file: $!";
    open my $env_pl_fh, '>:raw', $env_pl_file or die "Unable to write overlay env.pl file: $!";
    print {$env_pl_fh} "\$ENV{PL_ONLY} = \"\$ENV{VERSION}-pl\";\n1;\n";
    close $env_pl_fh or die "Unable to close overlay env.pl file: $!";

    my $overlay = Developer::Dashboard::EnvLoader->load_files_into_hash(
        files    => [ undef, $env_file, $env_pl_file, $env_file ],
        base_env => {
            KEEP    => 'keep',
            VERSION => 'base',
        },
    );
    is_deeply(
        $overlay->{files},
        [ $env_file, $env_pl_file ],
        'load_files_into_hash reports only unique existing files in load order',
    );
    is_deeply(
        $overlay->{env},
        {
            DEVELOPER_DASHBOARD_ENV_AUDIT => json_encode(
                {
                    NEW_ONLY => {
                        envfile => $env_file,
                        value   => 'from-file',
                    },
                    PL_ONLY => {
                        envfile => $env_pl_file,
                        value   => 'leaf-pl',

t/10-extension-action-docker.t  view on Meta::CPAN

open my $compose_fh, '>', File::Spec->catfile( $repo, 'compose.yaml' ) or die $!;
print {$compose_fh} "services:\n  app:\n    image: perl:latest\n";
close $compose_fh;
open my $override_fh, '>', File::Spec->catfile( $repo, 'compose.dev.yaml' ) or die $!;
print {$override_fh} "services:\n  app:\n    environment:\n      MODE: dev\n";
close $override_fh;
open my $repo_cfg, '>', File::Spec->catfile( $repo, '.developer-dashboard.json' ) or die $!;
print {$repo_cfg} <<'JSON';
{
  "docker": {
    "project_overlays": ["compose.project.yaml"],
    "services": {
      "worker": {
        "files": ["compose.worker.yaml"]
      }
    },
    "addons": {
      "mailhog": {
        "files": ["compose.mailhog.yaml"],
        "env": { "MAILHOG_ENABLED": "1" }
      }

t/10-extension-action-docker.t  view on Meta::CPAN

        ]
      }
    }
  ]
}
JSON
close $repo_cfg;
open my $addon_fh, '>', File::Spec->catfile( $repo, 'compose.mailhog.yaml' ) or die $!;
print {$addon_fh} "services:\n  mailhog:\n    image: mailhog/mailhog\n";
close $addon_fh;
open my $project_overlay_fh, '>', File::Spec->catfile( $repo, 'compose.project.yaml' ) or die $!;
print {$project_overlay_fh} "services:\n  app:\n    environment:\n      PROJECT_LAYER: 1\n";
close $project_overlay_fh;
open my $service_overlay_fh, '>', File::Spec->catfile( $repo, 'compose.worker.yaml' ) or die $!;
print {$service_overlay_fh} "services:\n  worker:\n    image: perl:latest\n";
close $service_overlay_fh;
my $global_docker_root = File::Spec->catdir( $home, '.developer-dashboard', 'config', 'docker', 'green' );
make_path($global_docker_root);
open my $global_green_fh, '>', File::Spec->catfile( $global_docker_root, 'compose.yml' ) or die $!;
print {$global_green_fh} "services:\n  green:\n    extra_hosts:\n      - host.docker.internal:host-gateway\n";
close $global_green_fh;
open my $global_green_dev_fh, '>', File::Spec->catfile( $global_docker_root, 'development.compose.yml' ) or die $!;
print {$global_green_dev_fh} "services:\n  green:\n    environment:\n      GREEN_DEV: 1\n";
close $global_green_dev_fh;
my $global_blue_root = File::Spec->catdir( $home, '.developer-dashboard', 'config', 'docker', 'blue' );
make_path($global_blue_root);

t/10-extension-action-docker.t  view on Meta::CPAN

    );
    my $resolved = $docker->resolve(
        addons => [ 'mailhog' ],
        args   => [ 'config', 'green' ],
        modes  => ['dev'],
        services => [ 'worker', 'orange' ],
    );
    chdir $old or die $!;
    is_same_path( $resolved->{project_root}, $repo, 'docker compose resolver uses current project root' );
    ok( grep( { /compose\.yaml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes discovered base file' );
    ok( grep( { /compose\.project\.yaml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes project overlay' );
    ok( grep( { /compose\.worker\.yaml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes service overlay' );
    ok( grep( { /compose\.dev\.yaml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes mode overlay' );
    ok( grep( { /compose\.mailhog\.yaml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes config addon overlay' );
    ok( grep( { /green\/development\.compose\.yml$/ } @{ $resolved->{files} } ), 'docker compose resolver includes isolated development compose files automatically for selected services' );
    ok( !grep( { /skills\/beta-skill\/config\/docker\/green\/compose\.yml$/ } @{ $resolved->{files} } ), 'docker compose resolver excludes docker roots contributed by disabled skills' );
    is( $resolved->{env}{APP_MODE}, 'dev', 'docker compose resolver merges mode env' );
    is_same_path(
        $resolved->{env}{alpha_skill_DDDC},
        File::Spec->catdir( $home, '.developer-dashboard', 'skills', 'alpha-skill', 'config', 'docker' ),
        'docker compose resolver exports one skill-specific DDDC variable for each participating skill docker root',
    );
    is( $resolved->{env}{ORANGE_SKILL_ENV}, 'orange-skill', 'docker compose resolver loads .env from participating skill docker roots' );
    is( $resolved->{env}{SKILL_ENV_REF}, 'skill-base', 'docker compose resolver expands participating skill .env values against the current process environment' );
    ok( !exists $resolved->{env}{DISABLED_SKILL_ENV}, 'docker compose resolver skips .env from disabled skill docker roots' );
    ok( grep( { /skills\/alpha-skill\/\.env$/ } @{ $resolved->{env_files} } ), 'docker compose resolver reports participating skill .env files in dry-run data' );
    is_same_path( $resolved->{env}{DDDC}, File::Spec->catdir( $repo, '.developer-dashboard', 'config', 'docker' ), 'docker compose resolver exports DDDC as the effective project-local docker config root' );
    is( $resolved->{env}{MAILHOG_ENABLED}, '1', 'docker compose resolver merges addon env' );
    is_deeply( [ @{ $resolved->{command} }[0,1] ], [ 'docker', 'compose' ], 'docker compose resolver produces docker compose command' );
    is_deeply( $resolved->{precedence}, [ qw(base project service addon mode) ], 'docker compose resolver exposes overlay precedence' );
    is_same_path(
        ( grep { /green\/development\.compose\.yml$/ } @{ $resolved->{files} } )[-1],
        File::Spec->catfile( $repo, '.developer-dashboard', 'config', 'docker', 'green', 'development.compose.yml' ),
        'docker compose resolver leaves the deepest project-local config/docker service folder as the last overriding development compose layer',
    );
    ok( grep( { $_ eq 'green' } @{ $resolved->{services} } ), 'docker compose resolver infers service names from passthrough docker compose args' );
    is( $resolved->{command}[-1], 'green', 'docker compose resolver preserves passthrough docker compose service args' );
}

{

t/11-coverage-closure.t  view on Meta::CPAN

  "collectors": [
    { "name": "repo.collector", "command": "printf repo", "cwd": "home", "interval": 5 },
    { "name": "cfg.collector", "command": "printf cfg", "cwd": "home", "interval": 7 }
  ],
  "providers": [
    { "id": "cfg-provider", "title": "Config Provider", "body": "cfg page body" },
    { "id": "shared-provider", "page": { "id": "shared-provider", "title": "Shared Provider", "layout": { "body": "shared body" } } }
  ],
  "docker": {
    "files": ["compose.dev.yaml", "compose.test.yaml"],
    "project_overlays": ["compose.test.yaml"],
    "services": {
      "worker": { "files": ["compose.worker.yaml"] }
    },
    "addons": {
      "debug": {
        "files": ["compose.debug.yaml"],
        "modes": ["dev"],
        "env": { "DEBUG_ENABLED": "1" }
      },
      "extra": {

t/11-coverage-closure.t  view on Meta::CPAN

    config  => $config,
    paths   => $paths,
);
my $resolved = $docker->resolve(
    project_root => $repo,
    addons       => ['debug'],
    modes        => [],
    services     => ['worker'],
    args         => ['config'],
);
ok( scalar( grep { $_ =~ /compose\.debug\.yaml$/ } @{ $resolved->{files} } ), 'docker resolve includes addon overlays' );
ok( scalar( grep { $_ eq 'dev' } @{ $resolved->{modes} } ), 'docker resolve pulls addon-provided modes into the resolution' );

my $docker_run = $docker->run(
    project_root => $repo,
    addons       => ['debug'],
    services     => ['worker'],
    args         => ['config'],
);
is( $docker_run->{exit_code}, 0, 'docker compose wrapper executes stub docker successfully' );
like( $docker_run->{stdout}, qr/DEBUG:1/, 'docker compose wrapper injects addon environment into command execution' );



( run in 1.150 second using v1.01-cache-2.11-cpan-df04353d9ac )