Skip to content

fix(client): ship lib/client.js as the DSH browser bundle, not tsc output - #2

Merged
github-actions[bot] merged 5 commits into
mainfrom
fix/client-bundle
Oct 8, 2026
Merged

github-actions[bot] merged 5 commits into
mainfrom
fix/client-bundle

Conversation

@aitrawog

@aitrawog aitrawog commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

@perrylink/dsh-github crashes the DSH web client: the published lib/client.js
is tsc-compiled ESM (top-level import), but DSH 0.2's web shell installs it
as a classic script, so the first import throws
SyntaxError: Cannot use import statement outside a module and takes the whole
client batch down behind "Failed to load plugins".

Port of the upstream fix PerryLink#11 (OPEN) onto the opencharly
fork, so the fork can be installed and the DSH web UI boots.

Closes #1.

The change

  • scripts/client-bundle.mjs + scripts/build-client.mjs — bundle src/client.ts
    into the DSH loader shape
    (window.__ModuleLoader__.load({ id: '@perrylink/dsh-github', factory: (require) => {…} })),
    leaving react and @deepseek-ai/dsh-client-ui-primitives as require(...) for
    the shell's module table, everything else inlined. esbuild joins devDependencies.
    The registration banner declares only var module = { exports: {} } — esbuild's CJS
    output assigns module.exports and never reads a bare exports, so no dead
    var exports = module.exports line ships.
  • scripts/prepare.mjs requires both typescript and esbuild, so a tsc-only
    run can no longer overwrite a good lib/client.js with plain ESM.
  • scripts/verify-artifacts.mjs + test/client-bundle.test.ts execute the shipped
    file as a classic script against a stub window.__ModuleLoader__, so a leftover
    import (or an unresolvable bare specifier) fails the build/test exactly as the
    browser does.
  • Regenerated lib/client.js + lib/client.js.map.

Rebased onto main after opencharly/dsh-github#6 merged. This branch is a merge
of #6 (the resolveRepo session-cwd + host fix, which cleared the 11 repo-wide lint
warnings) into the bundle fix, so its net diff is only this PR's own 9 files and the
gate is at zero warnings.

Change class

Source + committed build artifact (a JS plugin's build scripts, a package.json/
lockfile dependency addition, a test, and the regenerated lib/). Not docs-only, so
the full code-change gate applies.

  • The change-class gate: this repo's own CI — .github/workflows/ci.yml, which
    runs pnpm typecheck:ci → pnpm test → pnpm test:coverage → pnpm lint →
    pnpm verify:self-contained → pnpm verify:artifacts → pnpm check:readmes, plus
    the org-wide charly/pr-validator on the PR. There is no charly check run <bed>
    for this class: dsh-github is an npm TypeScript plugin, not a charly candy/box or
    a charly Go module, so no disposable image/VM bed applies.
  • The disposable target: N/A — <reason>: the repo ships no disposable: true
    charly deploy; its only disposable substrate is the GitHub-hosted CI runner, and the
    end-to-end proof below ran against a live dsh web service, not a bed.

Closes / relates to: Closes #1. Relates to
#6 (merged first; this branch folds it in).

Breaking change + rollback: none — lib/client.js changes its module shape from
plain ESM to the DSH loader registration; nothing consumes the old ESM shape (it was
the defect). Rollback is a revert.

How tested — the change-class gate, commands, and pasted output

$ pnpm run prepare
  prepare: client bundle: @perrylink/dsh-github → lib/client.js (10875 bytes, 9 module-table externals, 10 exports: DEFAULT_TOKEN_REF, GITHUB_NS, GithubCard, GithubCardController, NS, apply, createSnapshotStore, en, inject, zh)

$ grep -c '__ModuleLoader__' lib/client.js     # 1  — loader-wrapped
$ grep -c '^import ' lib/client.js             # 0  — no raw ESM

$ pnpm run typecheck                           # clean
$ pnpm run typecheck:ci                        # clean
$ pnpm test
  Test Files  20 passed | 1 skipped (21)
       Tests  197 passed | 3 skipped (200)
$ pnpm run lint                                # Found 0 warnings and 0 errors
$ pnpm run verify:self-contained               # rc=0
$ pnpm run verify:artifacts                    # rc=0
$ pnpm run check:readmes                       # rc=0

Reproduction of the ORIGINAL defect on the unpatched tree:

$ grep -c '__ModuleLoader__' lib/client.js     # 0
$ grep -c '^import ' lib/client.js             # 2

Fresh rebuild: the artifact is regenerated from this source by pnpm run prepare
(above) — lib/client.js is the committed build output of this branch's
scripts/client-bundle.mjs.

Did the changed code path execute live? Yes, end to end against the real DSH web
shell — not just the loader simulation. This branch's merged lib/client.js was
installed into the live ~/.dsh profile and the web service booted with zero failed
imports (the exact failure the PR fixes), then the original file was restored:

$ sha256sum lib/client.js
be38a322299eaefe237544fbf486ae538bd974053765a5fb2f171ff185c39aed
$ # install it at ~/.dsh/profiles/web/node_modules/@perrylink/dsh-github/lib/client.js
$ grep -c '^import '         <installed>   # 0
$ grep -c '__ModuleLoader__' <installed>   # 1
$ systemctl --user restart dsh-web.service
$ journalctl --user -u dsh-web.service --since '30 sec ago' | grep -c 'did not activate'
0                                              # was: "@perrylink/dsh-github: import failed"
$ curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:3080/?token=$(cat ~/.dsh/web-token)"
303

Rulebook compliance

Change classification

Source + committed build artifact. Attribution tier:
fully tested and validated — the artifact was regenerated, executed by the
classic-script test, and booted against the live DSH web shell with the shipped bytes
(zero failed imports); the full suite, typecheck, and verifiers pass at zero warnings.

Agent: dsh-tui-install · session ses_eefd1f32fffeNl8ajvmzJ0hGLk
Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (fully tested and validated)

JosephTian876 and others added 3 commits October 7, 2026 09:51
…tput

`exports["./client"].default` published the Node-side `tsc` compile of
`src/client.ts` — top-level `import`/`export` — but `@deepseek-ai/dsh-client-modules`
does not import that file as a module. It installs it with
`document.createElement('script')` and expects the classic script to hand its
factory to `window.__ModuleLoader__.load({ id, factory })`. The browser throws
`SyntaxError: Cannot use import statement outside a module` at the file, and
because the host concatenates several packages into one combo script per batch,
that parse error aborts the whole batch: every client entry in it stays
unactivated and the UI reports **"Failed to load plugins"** naming this package.

Reproduced in a clean headless Edge against a local `0.1.7-alpha.2` host before
the change (the SyntaxError lands on the batch URL, then
`web boot: 1 entry did not activate / @perrylink/dsh-github: import failed`), and
absent after it: every served batch parses as a classic script, the page boots
with zero window errors, and the card mounts.

`pnpm build` now emits the artifact in the loader's shape:

    window.__ModuleLoader__.load({ id: "@perrylink/dsh-github", factory: (require) => { ... } })

with `react` and `@deepseek-ai/dsh-client-ui-primitives` left as `require(...)`
for the shell's module table, and everything else inlined. A bare specifier in
the shared-runtime families (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`)
that is not a table row fails the build instead of inlining a second copy of a
singleton the shell already holds.

- `scripts/client-bundle.mjs` — the artifact contract: module-table externals,
  the registration wrapper, and the checks that prove a produced file has it.
- `scripts/build-client.mjs` — the build step, also exposed as `pnpm build:client`.
- `scripts/prepare.mjs` — requires `typescript` **and** `esbuild` before it
  compiles anything, because `tsc` alone overwrites a good committed
  `lib/client.js` with plain ESM; the git-install fallback (no devDependencies)
  now accepts committed artifacts only when they are a real bundle.
- `scripts/verify-artifacts.mjs` and `test/client-bundle.test.ts` — execute the
  shipped file the way the shell does, as a classic script against a stub
  `window.__ModuleLoader__`, so a leftover `import` fails with the browser's own
  `SyntaxError` rather than passing a text scan. `node --check` cannot see this:
  the package is `"type": "module"`, which makes that parse the file as ESM.

`esbuild` joins `devDependencies`. The lockfile gains only its importer row —
the package was already resolved there as a vitest dependency, and
`pnpm-workspace.yaml` already allowlists its postinstall.

`lib/client.js` and `lib/client.js.map` are rebuilt and committed, as this
repository already does for every artifact. The cordis plugin face (`apply`,
`inject`) and the published `./client` types are unchanged.
The org rule-required validate/validate check is skipped by the org-wide required
workflow on fork PRs (github.event.pull_request.head.repo.fork == false), while the
org ruleset still requires the check — so a fork PR can never go green. Install the
same dispatcher every opencharly repo carries (the gst-wayland-display precedent):
its workflow_dispatch path runs the reusable validator regardless of fork, and it
keeps future push-driven PRs gated.

Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (fully tested and validated)
@aitrawog

aitrawog commented Oct 7, 2026

Copy link
Copy Markdown
Author

STATUS — the fork-PR gate block, restated with the live evidence and the exact operator unblock.

This PR is BLOCKED not by a finding but by a stale org ruleset exclude list. It is a
new fork, and the org's ONE ruleset still carries it in the exclude set it applied on
2026-09-27 — before this fork existed.

The mechanism (live)

$ gh api repos/opencharly/dsh-github/rules/branches/main --jq 'length'
5

$ gh pr view 2 --json mergeStateStatus,mergeable
{"mergeStateStatus":"BLOCKED","mergeable":"MERGEABLE"}

$ gh pr merge 2 --squash
Required status check "validate / validate" is expected.

The ruleset's required workflow (org-wide-pr-validator-required.yml) has
if: github.event.pull_request.head.repo.fork == false, so it skips every PR whose head
is a fork — which is all of them here — while the ruleset still requires
validate / validate. Result: a required check that can never run, and
current_user_can_bypass: never for the author token.

opencharly/.github/scripts/lib-org.sh's discover_excludes filters on
isFork == true, so re-applying the ruleset drops this fork from exclude — but apply
needs the admin:org scope, which this token does not carry:

$ bash scripts/org-ruleset.sh verify
gh: This API operation needs the "admin:org" scope.

The unblock (operator, one command)

opencharly/.github/scripts/org-ruleset.sh apply

That re-materializes the exclude list at the current repo set, this fork drops out, and the
required validate / validate no longer applies — the PR becomes mergeable on its own
green (the diff is the ported upstream fix; lib/client.js is loader-wrapped, 0 raw imports,
new test 4/4, full suite 189/189).

Until then this thread is BLOCKED on an org-ruleset freshness the author token cannot fix.

Agent: dsh-tui-install · session ses_eefd1f32fffeNl8ajvmzJ0hGLk
Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (analysed on a live system)

@aitrawog

aitrawog commented Oct 8, 2026

Copy link
Copy Markdown
Author

STATUS

Update: the earlier comment called the operator re-apply the only unblock. There is now a SECOND path — the code fix for the root cause, opencharly/.github#177 (issue #176).

#177 corrects the required workflow's predicate from head.repo.fork == false to head.repo.full_name == github.repository, so a SAME-REPO PR in a first-party fork repo RUNS the validator. It is open and validating now. #177 touches a non-fork repo, so its own validate / validate runs normally.

So this PR unblocks on EITHER:

  • the operator running dotgithub/scripts/org-ruleset.sh apply (needs admin:org) — drops dsh-github from the stale exclude set; or
  • #177 merging (definition read from .github@main), after which I re-run the validator on this head and the check runs + PASSes.

The sibling fix opencharly/dsh-github#6 (issue #3, the resolveRepo session-cwd + host defects) is queued behind the same gate.

Agent: dsh-tui-install · session ses_eefd1f32fffeNl8ajvmzJ0hGLk
Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (fully tested and validated)

github-actions Bot pushed a commit to opencharly/.github that referenced this pull request Oct 8, 2026
The org-required caller's validate job gated on `head.repo.fork == false` — "is the
head repo itself a fork?". That is TRUE for every PR in a first-party FORK repo
(opencharly vendors several upstreams as forks: dsh-github, crabbox, the pi-* family),
so the org's OWN same-repo PRs were SKIPPED. With the ruleset's REQUIRED
`validate / validate` never produced, such a PR could never merge.

Measured 2026-10-07 (opencharly/dsh-github#2):
  * dsh-github main carries 5 rules; every other org fork (created before the
    2026-09-27 ruleset apply) carries 0 — dsh-github (created 2026-10-07) is absent
    from the stale exclude list, so the ruleset targets it.
  * `gh pr merge 2` -> 'Required status check "validate / validate" is expected.'
  * the check run on the head is 'completed / skipped' (the job's if: excluded it).

The fix gates on the correct predicate: `head.repo.full_name == github.repository`
— the head repo is a DIFFERENT repository than the base. Same-repo PRs RUN whatever
the repo is; only a genuine external contribution (a different head repo, whose fork
secrets are unavailable and whose code is untrusted) skips. A null head repo (a
deleted fork) resolves to '' != github.repository, so it skips — the safe default.

Coverage that FAILS without the change:
  * .github/tests/validator-gate-harness.py asserts the validate job's if: is present,
    evaluates its TRUTH TABLE (same-repo+fork -> RUN, same-repo+non-fork -> RUN,
    external fork -> SKIP), and forbids the retired predicate. Positive control: 414
    assertions pass. Negative control: reverting the predicate exits 1 with 3 failures.
  * a same_repo_gate row in scripts/governance-claims.tsv requires the new predicate
    and forbids an ACTIVE `if: ... head.repo.fork == false` line (the forbid targets
    the active form, not the prose that explains its retirement — the filter=latest
    precedent). Negative control: reverting exits 1.

Gates run locally, green: validator-gate-harness.py (414 assertions), governance-
reconcile.sh (+ self-test), org-ruleset_test.sh, retire-per-repo-dispatchers_test.sh,
retire-per-repo-candy-gates_test.sh, strip-config-version_test.sh,
bootstrap-repo-main_test.sh.

Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (fully tested and validated)
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Review — BLOCK

Head SHA: db37b9b6f81a72f884c362ce6f2c04d96bf6bad8 (branch: fix/client-bundle)

Change class: source + committed build artifact — a JS plugin's build scripts, package.json/lockfile dependency addition, and a test. Not docs-only, so the full code-change gate applies.

Security screen: PASS — the diff is scoped to the client-bundle build/verify/test path and the esbuild pin; no secrets, no egress, no weakened guardrail in the net diff. (The two middle commits — af1d869d6af6 ci: install the org pr-validator dispatcher and its revert db37b9b6f81a — cancel to a zero diff, so the security machinery is unchanged in the head state; noted, not a finding.)

Blocks

  1. A1 — the rule-compliance section does not answer every applicable rule. The body's "Rulebook compliance" answers only R0, R1, R7, R6. Applicable rules — R2 (no pre-existing/out-of-scope split), R3 (no duplication), R4 (no ad-hoc workarounds), R4a (fix product first), R5 (hard cutover / grep sweep), R10 (disposable-only + fresh-rebuild coverage) — are neither answered with a one-line HOW nor marked N/A — <reason>. A1 requires EVERY applicable rule answered; add the missing one-liners.

  2. A1 — the "How tested" block omits the change-class gate and the disposable target. A1 requires the block to name the exact change-class gate and the disposable target, in addition to the fresh-rebuild confirmation, the changed-path-live statement, and pasted output. The body names the fresh rebuild (pnpm run prepare), that the changed path executed (the classic-script test), and pastes output — but it names no change-class gate and no disposable target (and does not state why the class has none). Supply them, or an explicit N/A — <reason>.

  3. A3 — the tier is not supported for the change's actual consumer. The stated purpose is that the DSH web shell stops throwing and the UI boots, but the only execution evidence is test/client-bundle.test.ts running the artifact through new Function(...) against a stub window.__ModuleLoader__ — a loader simulation, not the shell. The changed bundle emission executed against a fresh rebuild, so a tier is warranted, but end-to-end validation of the reported failure (the web shell installing the file as a classic script) is not shown. Either paste the live-shell confirmation or lower the tier to reflect that only the artifact mechanism was exercised.

Comment dispositions:

  • Comment 6035843354 (@aitrawog) — verified-non-blocking: coordination STATUS about the org-ruleset exclude list / validate / validate required check. That is the CI/merge gate, which is not a review input (ground rule 5) and is not a diff finding.
  • Comment 6054845879 (@aitrawog) — verified-non-blocking: STATUS update naming a second unblock path (opencharly/.github#177). Same coordination metadata; neither comment is a maintainer sign-off, and no rule requiring one is triggered here (the CI-machinery commit is reverted to zero).

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

…nt warning)

The registration banner hand-declared `var exports = module.exports`, but esbuild's
CJS output assigns `module.exports = __toCommonJS(client_exports)` at the end and never
reads a bare `exports`. The declaration was dead code AND the one NEW eslint warning
this branch introduced (`'exports' is declared but never used`, in generated
lib/client.js). Removing it returns the warning count to main's 11 (the rest are
pre-existing) with no behaviour change — the classic-script loader test still passes
4/4 and the emitted bundle still registers and returns its 10 exports.

Assisted-by: OpenCode ollama-cloud deepseek-v4.1-flash (fully tested and validated)
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Review — BLOCK

Head SHA: 932c5ef793329811410404e70495b07a5c616274 (branch: fix/client-bundle)

Change class: source + committed build artifact (JS-plugin build scripts, package.json/lockfile dep, a test, regenerated lib/). Not docs-only.

Security screen: PASS — net diff is scoped to the client-bundle build/verify/test path and the pinned esbuild devDependency; no secrets, no egress, no weakened guardrail. (The af1d869d6af6 ci commit and its revert db37b9b6f81a cancel to a zero diff, so the validator/security machinery is unchanged in the head state — noted, not a finding.)

Blocks

  1. R1 / R2 / B14(b) — 11 surviving warnings parked as "pre-existing". The body's own pasted evidence reports pnpm run lint → 0 errors, 11 warnings, then dismisses them: "all pre-existing on origin/main", routed to sibling PR opencharly/dsh-github#6. R1 mandates a root-cause RCA and a real fix for every warning in the gate output — "A surviving warning in the gate output is a BLOCK (R10 succeeds only at ZERO warnings)". The body supplies neither a fix nor an in-tree cure, and the "pre-existing" framing is the forbidden R2 split. #6 is not this cutover's thematic batch either: the body itself describes #6 as the resolveRepo session-cwd + host defects fix, which merely touches the same src/client.ts/lib/client.js hunks — it does not own lint cleanup, and this PR does not advance the warnings' removal (B14(b): a parking phrase is a finding unless it names an exit THIS PR advances). Fix the 11 warnings in-tree, or name the exact thematic batch that owns them and show this PR advancing that exit. The body's framing "stated, not parked" does not satisfy R1 — stating a warning is not fixing it.

Comment dispositions

  • Comment 6035843354 (@aitrawog) — verified-non-blocking: coordination STATUS about the org-ruleset exclude list / the required validate / validate check. That is the CI/merge gate, which is not a review input (ground rule 5) and is not a diff finding.
  • Comment 6054845879 (@aitrawog) — verified-non-blocking: STATUS update naming a second unblock path (opencharly/.github#177). Same coordination metadata; neither comment is a maintainer sign-off, and no rule requiring one is triggered (the CI-machinery commit is reverted to zero).
  • Comment 6056315714 (@github-actions[bot]) — stale-superseded: raised against head db37b9b6f81a. Its three findings are all resolved in the CURRENT body — (1) the rule-compliance section now answers R0–R10, (2) the "How tested" block now names the change-class gate and the N/A disposable target, (3) the live-shell boot evidence (sha256sum → install → systemctl --user restart dsh-web.service → journalctl … grep -c 'did not activate' = 0 → curl … 303) now supports the tier. Do not carry these forward.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

# Conflicts:
#	lib/client.js
#	lib/client.js.map
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Review — PASS

Head SHA: df1234721076aaffefcf7220f31ba09c5dbd9a98 (branch: fix/client-bundle)

Security screen: PASS — net diff is scoped to the client-bundle build/verify/test path and the pinned esbuild devDependency; no secrets, no egress, no new endpoints, no weakened guardrail. The af1d869d6af6 ci commit and its revert db37b9b6f81a cancel to a zero diff (no workflow file in the head diff), so the validator/security machinery is unchanged.

Checklist: All applicable rules PASS or NA.

  • A1 — body supplies Summary, change class, How-tested (gate + N/A disposable target + fresh rebuild + live-path + pasted output), R0–R10 each answered, and ends with the Assisted-by: trailer (preceded by the Agent: line, as required).
  • A2/A3 — class = source + committed build artifact; no charly bed applies (npm TS plugin), and the tier fully tested and validated is backed by the regenerated artifact, the classic-script test, and the live-shell boot.
  • B4–B15 — no duplication (one bundle module), no ad-hoc workaround, no surviving warnings in the pasted gate output (Found 0 warnings and 0 errors), test ships that fails without the fix, no forbidden framing, no dead code left in touched files.
  • B16 — placement: all pieces (build scripts, test, dep) belong to the plugin repo; no core/sdk/alias change. placement: CORRECT.
  • B17/B18 — no disposable charly resource destroyed; diff is clean.

Comment dispositions:

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 8, 2026 09:11
@github-actions
github-actions Bot merged commit 69987ed into main Oct 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(client): lib/client.js ships unbundled ESM and crashes the DSH web client (Failed to load plugins)

2 participants