@react-ui-org/react-ui 0.63.1 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude/agents/code-reviewer.md +122 -0
  2. package/.claude/rules/code.md +22 -0
  3. package/.claude/rules/docs.md +35 -0
  4. package/.claude/rules/frontend.md +72 -0
  5. package/.claude/rules/git.md +13 -0
  6. package/.claude/rules/safety-guards.md +9 -0
  7. package/.claude/rules/styling.md +32 -0
  8. package/.claude/rules/testing.md +59 -0
  9. package/.claude/settings.json +32 -0
  10. package/.claude/skills/commit/SKILL.md +54 -0
  11. package/.devcontainer/devcontainer.json +23 -0
  12. package/.mcp.json +8 -0
  13. package/CLAUDE.md +82 -0
  14. package/dist/react-ui.css +4 -4
  15. package/dist/react-ui.development.css +600 -590
  16. package/dist/react-ui.development.js +20 -10
  17. package/dist/react-ui.js +1 -1
  18. package/docker-compose.base.yml +93 -0
  19. package/docker-compose.yml.dist +41 -0
  20. package/opencode.json +10 -0
  21. package/package.json +3 -2
  22. package/scripts/auto-start-mkdocs.sh +5 -0
  23. package/scripts/auto-start-node.sh +33 -0
  24. package/scripts/mcps/chrome-host/README.md +118 -0
  25. package/scripts/mcps/chrome-host/cdp_proxy.py +90 -0
  26. package/scripts/mcps/chrome-host/mcp-entry.sh +33 -0
  27. package/scripts/mcps/chrome-host/mcp-launch.sh +36 -0
  28. package/scripts/mcps/chrome-host/mcp-setup.sh +70 -0
  29. package/scripts/mcps/chrome-host/start-host-chrome.sh +267 -0
  30. package/scripts/write-lockfile-hash.sh +15 -0
  31. package/setup.sh +107 -0
  32. package/src/components/Button/Button.jsx +4 -0
  33. package/src/components/Button/Button.module.scss +11 -0
  34. package/src/components/FileInputField/FileInputField.jsx +9 -2
  35. package/src/components/FormLayout/FormLayoutCustomField.jsx +4 -1
  36. package/src/components/FormLayout/FormLayoutCustomFieldContext.js +3 -0
  37. package/src/components/FormLayout/README.md +168 -161
  38. package/src/components/FormLayout/index.js +1 -0
  39. package/src/components/Popover/Popover.jsx +1 -0
  40. package/src/components/Popover/Popover.module.scss +1 -0
  41. package/src/components/Popover/README.md +29 -0
  42. package/src/components/Popover/_helpers/cleanPlacementStyle.js +1 -0
  43. package/src/components/Popover/_theme.scss +1 -0
  44. package/src/components/SelectField/SelectField.jsx +8 -3
  45. package/src/components/TextArea/TextArea.jsx +9 -2
  46. package/src/components/TextField/TextField.jsx +8 -3
  47. package/src/theme.scss +1 -0
  48. package/.env.playwright +0 -9
  49. package/.env.playwright.dist +0 -9
@@ -0,0 +1,93 @@
1
+ services:
2
+ # This service is responsible for providing the main development environment for developers
3
+ devcontainer:
4
+ hostname: ${COMPOSE_PROJECT_NAME:-react-ui}_devcontainer
5
+ build:
6
+ context: docker/react_ui_devcontainer/
7
+ dockerfile: Dockerfile
8
+ # Start dependent services before starting the `devcontainer` service to ensure that the necessary environments
9
+ # and tools are available when the `devcontainer` starts.
10
+ depends_on:
11
+ node:
12
+ condition: service_started
13
+ playwright:
14
+ condition: service_started
15
+ docs:
16
+ condition: service_started
17
+ # Run as host UID/GID so files created in mounted volumes are owned correctly on the host.
18
+ # The images use `fixuid` (https://github.com/boxboat/fixuid) to remap the built-in `developer`
19
+ # user to these IDs at startup — the `user:` directive is required for that remap to work.
20
+ # Applied to all services below for the same reason.
21
+ user: ${COMPOSE_UID}:${COMPOSE_GID}
22
+ # Keep the container running indefinitely to allow developers to attach to it and use it as their development environment
23
+ command: sleep infinity
24
+ # Injects environment variables from the `.env` file into the `devcontainer` service,
25
+ # making them accessible within the container's environment.
26
+ env_file:
27
+ - .env
28
+ environment:
29
+ # This must be set correctly for the `devcontainer` to be able to access the host's Docker daemon,
30
+ # enabling Docker-from-Docker capabilities (e.g., running Docker commands from within the `devcontainer`).
31
+ COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-react-ui}
32
+ init: true
33
+ volumes:
34
+ - .:/workspace:z
35
+ # The following volume is used to allow the `devcontainer` to access the host's Docker daemon,
36
+ # enabling Docker-from-Docker capabilities (e.g., running Docker commands from within the `devcontainer`).
37
+ - /var/run/docker.sock:/var/run/docker.sock
38
+ # The following named volumes persist data (e.g. terminal history, AI tools data, etc.) across container restarts.
39
+ # Using separate named volumes (instead of a single volume with subpaths) allows Docker to automatically
40
+ # seed the volume with data from the image on first use.
41
+ - terminal-history:/home/developer/.terminal_history
42
+ - claude-config:/home/developer/.config/claude
43
+ - claude-state:/home/developer/.local/state/claude
44
+ - copilot:/home/developer/.copilot
45
+ - copilot-config:/home/developer/.config/copilot
46
+ - opencode-config:/home/developer/.config/opencode
47
+ - opencode-share:/home/developer/.local/share/opencode
48
+ - opencode-state:/home/developer/.local/state/opencode
49
+
50
+ # This service provides Node environment and NPM
51
+ node:
52
+ build: docker/node
53
+ user: ${COMPOSE_UID}:${COMPOSE_GID}
54
+ entrypoint: sh -c 'if [ "$$COMPOSE_AUTOSTART" = "true" ]; then sh scripts/auto-start-node.sh; else sleep infinity; fi'
55
+ env_file:
56
+ - .env
57
+ volumes:
58
+ - .:/workspace:z
59
+
60
+ # This service provides Playwright environment and tools for browser automation and testing
61
+ playwright:
62
+ build: docker/playwright
63
+ user: ${COMPOSE_UID}:${COMPOSE_GID}
64
+ command: sleep infinity
65
+ env_file:
66
+ - .env
67
+ ports:
68
+ - ${COMPOSE_PLAYWRIGHT_REPORT_PORT}:9323
69
+ volumes:
70
+ - .:/workspace:z
71
+
72
+ # This provides server for documentation
73
+ docs:
74
+ build: docker/mkdocs
75
+ user: ${COMPOSE_UID}:${COMPOSE_GID}
76
+ entrypoint: sh -c 'if [ "$$COMPOSE_AUTOSTART" = "true" ]; then sh scripts/auto-start-mkdocs.sh; else sleep infinity; fi'
77
+ env_file:
78
+ - .env
79
+ ports:
80
+ - ${COMPOSE_DOCS_SERVER_PORT}:8000
81
+ volumes:
82
+ - .:/workspace:z
83
+
84
+ volumes:
85
+ # The following volumes are used to persist data (e.g. terminal history, AI tools data, etc.) across container restarts
86
+ terminal-history:
87
+ claude-config:
88
+ claude-state:
89
+ copilot:
90
+ copilot-config:
91
+ opencode-config:
92
+ opencode-share:
93
+ opencode-state:
@@ -0,0 +1,41 @@
1
+ services:
2
+ # This service is responsible for providing the main development environment for developers
3
+ devcontainer:
4
+ extends:
5
+ file: docker-compose.base.yml
6
+ service: devcontainer
7
+ # Use `build` when you want to customize the devcontainer using `docker/react_ui_devcontainer_local/Dockerfile`
8
+ # build:
9
+ # context: ./docker/react_ui_devcontainer_local/
10
+ # dockerfile: Dockerfile
11
+ # Use `image` when you want to use the default devcontainer
12
+ image: react-ui_devcontainer
13
+
14
+ # This service provides Node environment and NPM
15
+ node:
16
+ extends:
17
+ file: docker-compose.base.yml
18
+ service: node
19
+
20
+ # This service provides Playwright environment and tools for browser automation and testing
21
+ playwright:
22
+ extends:
23
+ file: docker-compose.base.yml
24
+ service: playwright
25
+
26
+ # This provides server for documentation
27
+ docs:
28
+ extends:
29
+ file: docker-compose.base.yml
30
+ service: docs
31
+
32
+ volumes:
33
+ # The following volumes are used to persist data (e.g. terminal history, AI tools data, etc.) across container restarts
34
+ terminal-history:
35
+ claude-config:
36
+ claude-state:
37
+ copilot:
38
+ copilot-config:
39
+ opencode-config:
40
+ opencode-share:
41
+ opencode-state:
package/opencode.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "chrome-host": {
5
+ "type": "local",
6
+ "command": ["sh", "scripts/mcps/chrome-host/mcp-entry.sh"],
7
+ "enabled": true
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-ui-org/react-ui",
3
3
  "description": "React UI is a themeable UI library for React apps.",
4
- "version": "0.63.1",
4
+ "version": "0.64.0",
5
5
  "keywords": [
6
6
  "react",
7
7
  "ui",
@@ -54,10 +54,11 @@
54
54
  "lint": "npm run eslint && npm run markdownlint && npm run stylelint",
55
55
  "markdownlint": "markdownlint-cli2 \"README.md\" \"src/**/*.md\"",
56
56
  "postbuild": "npm run copy",
57
- "postinstall": "cp -n .env.dist .env && cp -n .env.playwright.dist .env.playwright || true",
57
+ "postinstall": "sh scripts/write-lockfile-hash.sh",
58
58
  "precopy": "rm -rf dist && mkdir dist",
59
59
  "prepublishOnly": "npm run build",
60
60
  "start": "webpack --watch --mode=development",
61
+ "start:chrome": "sh scripts/mcps/chrome-host/start-host-chrome.sh",
61
62
  "stylelint": "stylelint \"src/**/*.{css,scss}\" \"!src/docs/_assets/generated/**\" --config stylelint.config.js",
62
63
  "test": "npm run test:jest",
63
64
  "test:jest": "npm run test:jest:ts && npm run test:jest:js",
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ # Start the docs server
4
+ echo "Starting the docs server..."
5
+ mkdocs serve
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ # File to read the hash of the package-lock.json
6
+ LOCK_HASH_FILE="node_modules/.package-lock-hash"
7
+
8
+ # Parent directory of the script
9
+ SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
10
+
11
+ # Change to the parent directory of the script
12
+ cd "$SCRIPT_DIR/.."
13
+
14
+ # Install dependencies if node_modules is missing or out of date
15
+ CURRENT_HASH=$(sha256sum package-lock.json | awk '{print $1}')
16
+ if [ ! -d "node_modules" ]; then
17
+ echo "Installing dependencies (node_modules directory is missing)..."
18
+ npm ci
19
+ elif [ ! -f "$LOCK_HASH_FILE" ]; then
20
+ echo "Installing dependencies (lockfile of package-lock.json is missing)..."
21
+ npm ci
22
+ elif [ "$(cat "$LOCK_HASH_FILE")" != "$CURRENT_HASH" ]; then
23
+ echo "Installing dependencies (package-lock.json has changed)..."
24
+ npm ci
25
+ fi
26
+
27
+ # Build the application (must be run prior to starting the server to ensure the latest code is used)
28
+ echo "Building the application..."
29
+ npm run build
30
+
31
+ # Start the application
32
+ echo "Starting the application..."
33
+ npm start
@@ -0,0 +1,118 @@
1
+ # Chrome-host bridge
2
+
3
+ Lets an MCP-capable AI assistant in the `devcontainer` drive **Chrome on the
4
+ host** via [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp)
5
+ over the Chrome DevTools Protocol (CDP).
6
+
7
+ ## Principles
8
+
9
+ - The container reaches the host only via `host.docker.internal`.
10
+ - Chrome's DevTools endpoint rejects any `Host` header that is not an IP literal
11
+ (or the name `localhost`) — so the container cannot connect to it directly.
12
+ - Fix: a loopback proxy **inside the container** (`127.0.0.1:9334`); the `Host`
13
+ Chrome finally sees is `127.0.0.1`, an IP, which it accepts.
14
+ - Chrome runs in a dedicated, persistent profile (separate from your everyday
15
+ one) and is started **by hand** on the host — never from a Claude action,
16
+ which can reap it with its process group.
17
+
18
+ ## Architecture
19
+
20
+ ```text
21
+ Claude (devcontainer)
22
+ │ .mcp.json → mcp-entry.sh → docker compose exec node mcp-launch.sh
23
+
24
+ node container
25
+ ├─ cdp_proxy.py 127.0.0.1:9334 ──► host.docker.internal:9333
26
+ └─ npx chrome-devtools-mcp --browser-url http://127.0.0.1:9334
27
+
28
+
29
+ host: Chrome --remote-debugging-port=9333 (throwaway profile)
30
+ ```
31
+
32
+ | Script | Runs on | Purpose |
33
+ | --- | --- | --- |
34
+ | `start-host-chrome.sh` | host | Launch the debug Chrome on `:9333`. Idempotent; auto-detects Chrome/Chromium/Edge. On native Docker, also runs an `ncat` forwarder to expose `:9333` on the bridge gateway. |
35
+ | `cdp_proxy.py` | node container | Loopback CDP proxy `:9334` → host `:9333`. Binds-or-exits. |
36
+ | `mcp-launch.sh` | node container | Ensure the proxy is up, then `exec` `chrome-devtools-mcp`. |
37
+ | `mcp-entry.sh` | host / devcontainer | The `.mcp.json` command; routes into the node container (adds `sudo` inside the container). |
38
+ | `mcp-setup.sh` | host (setup) | Add the `host.docker.internal:host-gateway` mapping on native Docker Engine. Idempotent; no-op on Docker Desktop. |
39
+
40
+ ## Registration
41
+
42
+ Register `mcp-entry.sh` as an MCP server named `chrome-host`. Any MCP client
43
+ works — point its config at the script.
44
+
45
+ **Claude / GitHub Copilot CLI** — `.mcp.json` (repo root):
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "chrome-host": {
51
+ "command": "sh",
52
+ "args": ["scripts/mcps/chrome-host/mcp-entry.sh"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ **OpenCode** — `opencode.json` (repo root):
59
+
60
+ ```json
61
+ {
62
+ "mcp": {
63
+ "chrome-host": {
64
+ "type": "local",
65
+ "command": ["sh", "scripts/mcps/chrome-host/mcp-entry.sh"],
66
+ "enabled": true
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ Requirements: a `docker-compose.yml` at the repo root, the repo mounted at
73
+ `/workspace` in the container, and a Compose service named `node` that runs
74
+ `node`/`npx`.
75
+
76
+ ## Usage
77
+
78
+ 1. Start host Chrome (from a host terminal, **not** from inside an AI action):
79
+
80
+ ```sh
81
+ ./scripts/mcps/chrome-host/start-host-chrome.sh
82
+ ```
83
+
84
+ 2. On first run, approve the `chrome-host` server in your MCP client.
85
+
86
+ The proxy and MCP server are bootstrapped by `mcp-launch.sh`; you only start
87
+ Chrome by hand.
88
+
89
+ ## Platform support
90
+
91
+ Decided by **Docker Desktop vs native Docker Engine**, not the OS.
92
+
93
+ | Host setup | `host.docker.internal` reaches host loopback? | Extra steps |
94
+ | --- | --- | --- |
95
+ | Docker Desktop (macOS/Linux) | Yes (auto) | None. |
96
+ | Native Docker Engine (Linux) | No (bridge gateway only) | `setup.sh` (via `mcp-setup.sh`) adds the host-gateway mapping; the launcher runs a host-side `ncat` forwarder (install `nmap-ncat`/`nmap`) to expose Chrome's loopback CDP port on the bridge gateway. **Firewall the port**: this exposes DevTools beyond loopback. |
97
+
98
+ ## Configuration
99
+
100
+ - Ports: host CDP `9333` (configurable), in-container proxy `9334` (fixed).
101
+ - Overrides (host launcher), set in the repo `.env`: `CHROME_DEBUG_PORT` and
102
+ `CHROME_BIN`. `CHROME_DEBUG_BIND` is an advanced escape hatch — the bind is
103
+ auto-detected, so it is not listed in `.env.dist`; set it only when detection
104
+ fails (e.g. a non-default daemon `host-gateway-ip`). **Security:** a
105
+ non-loopback bind exposes the unauthenticated CDP port; firewall it.
106
+ The proxy's listen port and target host are fixed constants; only its upstream
107
+ port is configurable, and it follows the same `CHROME_DEBUG_PORT`.
108
+ - Set these in the repo `.env`; Compose passes `CHROME_DEBUG_PORT` into the
109
+ container, so a single knob drives both sides. Recreate the node container
110
+ after editing. See `.env.dist`.
111
+ - Profile: `~/.chrome-debug-profile-<COMPOSE_PROJECT_NAME>` (per project, fixed).
112
+
113
+ ## Troubleshooting
114
+
115
+ - Status: `claude mcp get chrome-host` (`✔ Connected` = healthy chain).
116
+ - Chrome died? Re-run `start-host-chrome.sh` (it persists across sessions).
117
+ - `claude mcp list` shows *Pending approval* until you approve project servers.
118
+ - Disable: remove the `chrome-host` entry from `.mcp.json`.
@@ -0,0 +1,90 @@
1
+ import os
2
+ import socket
3
+ import threading
4
+
5
+ # Loopback CDP proxy. Runs INSIDE the node container.
6
+ #
7
+ # A container reaches host Chrome only via `host.docker.internal` (forwarded to
8
+ # the host loopback on Docker Desktop; the bridge gateway on native Linux Docker,
9
+ # where a host-side ncat forwarder exposes Chrome's loopback CDP port). But
10
+ # Chrome's DevTools endpoint rejects any Host header that isn't an IP literal (or
11
+ # the name "localhost") -- anti DNS-rebinding -- so connecting by that *name*
12
+ # fails, and the IPv6 it may also resolve to is often not forwarded.
13
+ #
14
+ # So this proxy listens on 127.0.0.1:LISTEN_PORT and forwards raw bytes to host
15
+ # Chrome. Clients reach it as "127.0.0.1:LISTEN_PORT", so the Host header Chrome
16
+ # sees is an IP (accepted), and the webSocketDebuggerUrl it echoes back stays on
17
+ # 127.0.0.1 too, so the follow-up WebSocket also works.
18
+ #
19
+ # The proxy's own endpoint is fixed: it always listens on the container loopback
20
+ # and forwards to host Chrome over host.docker.internal. Only the upstream Chrome
21
+ # port is configurable, via CHROME_DEBUG_PORT (set in the repo's .env, which
22
+ # Compose passes into the container), so a single knob keeps both sides in sync.
23
+
24
+ LISTEN_HOST = "127.0.0.1"
25
+ LISTEN_PORT = 9334 # fixed; must match PROXY_PORT in mcp-launch.sh
26
+ TARGET_HOST = "host.docker.internal" # fixed; how the container reaches the host
27
+ TARGET_PORT = int(os.environ.get("CHROME_DEBUG_PORT") or "9333") # host Chrome CDP port
28
+
29
+
30
+ def resolve_target():
31
+ # Force IPv4: host.docker.internal may also resolve to an IPv6 address that
32
+ # Docker does not forward to the host.
33
+ try:
34
+ infos = socket.getaddrinfo(TARGET_HOST, TARGET_PORT, socket.AF_INET, socket.SOCK_STREAM)
35
+ return infos[0][4]
36
+ except Exception:
37
+ # Docker Desktop's well-known host-gateway IPv4 as a last resort.
38
+ return ("192.168.65.254", TARGET_PORT)
39
+
40
+
41
+ TARGET = resolve_target()
42
+
43
+
44
+ def pipe(source, destination):
45
+ try:
46
+ while True:
47
+ data = source.recv(65536)
48
+ if not data:
49
+ break
50
+ destination.sendall(data)
51
+ except Exception:
52
+ pass
53
+ finally:
54
+ for sock in (source, destination):
55
+ try:
56
+ sock.close()
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ def handle(client):
62
+ try:
63
+ upstream = socket.create_connection(TARGET, timeout=10)
64
+ except Exception:
65
+ client.close()
66
+ return
67
+ threading.Thread(target=pipe, args=(client, upstream), daemon=True).start()
68
+ threading.Thread(target=pipe, args=(upstream, client), daemon=True).start()
69
+
70
+
71
+ def main():
72
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
74
+ try:
75
+ srv.bind((LISTEN_HOST, LISTEN_PORT))
76
+ except OSError:
77
+ # Another instance already owns the port; let it serve.
78
+ raise SystemExit(0)
79
+ srv.listen(128)
80
+ print("chrome-host: proxy listening %s:%d -> %s:%d" % (LISTEN_HOST, LISTEN_PORT, TARGET[0], TARGET[1]), flush=True)
81
+ while True:
82
+ try:
83
+ conn, _ = srv.accept()
84
+ except Exception:
85
+ continue
86
+ handle(conn)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+
3
+ # Referenced by .mcp.json / opencode.json as the `chrome-host` MCP server command.
4
+ #
5
+ # Spawns chrome-devtools-mcp inside the node container (via mcp-launch.sh),
6
+ # whether the assistant runs in the devcontainer or on the host. Docker needs
7
+ # `sudo` inside the container (like the node/npx wrappers) but not on the host;
8
+ # we tell them apart by /.dockerenv, which exists only in a container.
9
+
10
+ set -eu
11
+
12
+ # Compose service that runs the proxy + chrome-devtools-mcp.
13
+ DOCKER_SERVICE_NAME=node
14
+
15
+ script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
16
+
17
+ # Walk up to the repo root (the directory holding docker-compose.yml) so
18
+ # `docker compose` finds the compose files + .env regardless of the caller's cwd.
19
+ root="$script_dir"
20
+ while [ "$root" != "/" ] && [ ! -f "$root/docker-compose.yml" ]; do
21
+ root="$(dirname "$root")"
22
+ done
23
+ cd "$root"
24
+
25
+ # Path to mcp-launch.sh as seen inside the node container (repo mounted at /workspace).
26
+ rel="${script_dir#"$root"/}"
27
+ launch="/workspace/$rel/mcp-launch.sh"
28
+
29
+ if [ -f /.dockerenv ]; then
30
+ exec sudo docker compose exec -T "$DOCKER_SERVICE_NAME" "$launch"
31
+ else
32
+ exec docker compose exec -T "$DOCKER_SERVICE_NAME" "$launch"
33
+ fi
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+
3
+ # Runs INSIDE the node container, invoked by mcp-entry.sh.
4
+ #
5
+ # Ensures the loopback CDP proxy (cdp_proxy.py) is running, then hands stdio over
6
+ # to chrome-devtools-mcp pointed at the proxy. Resolves cdp_proxy.py next to
7
+ # itself, so it works from any location under the repo.
8
+
9
+ set -eu
10
+
11
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
12
+ PROXY_PORT=9334 # fixed; must match LISTEN_PORT in cdp_proxy.py
13
+ PROXY_SCRIPT="$SCRIPT_DIR/cdp_proxy.py"
14
+
15
+ is_up() {
16
+ python3 -c 'import socket,sys; s=socket.socket(); sys.exit(0 if s.connect_ex(("127.0.0.1",'"$PROXY_PORT"'))==0 else 1)' 2>/dev/null
17
+ }
18
+
19
+ if ! is_up; then
20
+ # cdp_proxy.py binds-or-exits, so concurrent launches are safe.
21
+ python3 "$PROXY_SCRIPT" >/tmp/cdp_proxy.log 2>&1 &
22
+ i=0
23
+ while [ "$i" -lt 25 ]; do
24
+ if is_up; then
25
+ break
26
+ fi
27
+ i=$((i + 1))
28
+ sleep 0.2
29
+ done
30
+ fi
31
+
32
+ # chrome-devtools-mcp is fetched on demand via npx rather than being added to
33
+ # package.json: it is host/dev MCP tooling for this bridge, not an app build or
34
+ # runtime dependency, so it deliberately stays out of the lockfile. `-y` confirms
35
+ # the one-off npx install; `@latest` tracks upstream fixes for the dev tool.
36
+ exec npx -y chrome-devtools-mcp@latest --browser-url "http://127.0.0.1:${PROXY_PORT}" "$@"
@@ -0,0 +1,70 @@
1
+ #!/bin/sh
2
+
3
+ # Run on the HOST during project setup (called from setup.sh).
4
+ #
5
+ # Native Docker Engine on Linux doesn't inject `host.docker.internal`, so the
6
+ # proxy container can't reach host Chrome. This adds the host-gateway mapping to
7
+ # the node service -- but only there. On Docker Desktop/macOS it's a no-op:
8
+ # the name already works, and adding the mapping would repoint it to the bridge
9
+ # gateway and break Desktop.
10
+ #
11
+ # Idempotent; after patching a running setup, recreate the service:
12
+ # `docker compose up -d node`.
13
+
14
+ set -eu
15
+
16
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)"
17
+ COMPOSE_FILE="$ROOT/docker-compose.yml"
18
+ DOCKER_SERVICE_NAME=node
19
+ MAPPING="host.docker.internal:host-gateway"
20
+
21
+ # Only native Docker Engine on Linux needs the mapping. macOS is always Desktop.
22
+ if [ "$(uname -s)" = "Darwin" ]; then
23
+ exit 0
24
+ fi
25
+
26
+ DOCKER_OS="$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || true)"
27
+ if [ -z "$DOCKER_OS" ]; then
28
+ echo "chrome-host: warning: could not query Docker (is it running?); skipping host-gateway setup." >&2
29
+ exit 0
30
+ fi
31
+ case "$DOCKER_OS" in
32
+ *"Docker Desktop"*) exit 0 ;;
33
+ esac
34
+
35
+ # Native Docker Engine from here on.
36
+
37
+ # The host launcher forwards Chrome's loopback CDP port to the bridge gateway
38
+ # with ncat (headed Chrome only binds loopback). Warn now rather than failing at
39
+ # first browser launch; don't auto-install -- that needs sudo plus distro
40
+ # detection, and adding host packages requires explicit approval.
41
+ if ! command -v ncat >/dev/null 2>&1; then
42
+ echo "chrome-host: warning: 'ncat' not found; install it (package 'nmap-ncat' or 'nmap'," >&2
43
+ echo "chrome-host: depending on your distro) so start-host-chrome.sh can expose Chrome's" >&2
44
+ echo "chrome-host: CDP port on the bridge gateway for the container." >&2
45
+ fi
46
+
47
+ [ -f "$COMPOSE_FILE" ] || exit 0
48
+ if grep -q "$MAPPING" "$COMPOSE_FILE"; then
49
+ exit 0
50
+ fi
51
+
52
+ # Insert extra_hosts under the `<service>:` block (sibling of `extends:`).
53
+ TMP_FILE="$COMPOSE_FILE.tmp"
54
+ awk -v service="$DOCKER_SERVICE_NAME" '
55
+ { print }
56
+ $0 ~ ("^ " service ":[[:space:]]*$") && !done {
57
+ print " extra_hosts:"
58
+ print " - \"host.docker.internal:host-gateway\""
59
+ done = 1
60
+ }
61
+ ' "$COMPOSE_FILE" > "$TMP_FILE"
62
+
63
+ if grep -q "$MAPPING" "$TMP_FILE"; then
64
+ mv "$TMP_FILE" "$COMPOSE_FILE"
65
+ echo "chrome-host: native Docker Engine detected -- added '$MAPPING' to the '$DOCKER_SERVICE_NAME' service in $COMPOSE_FILE."
66
+ echo "chrome-host: if the '$DOCKER_SERVICE_NAME' container is already running, recreate it: docker compose up -d $DOCKER_SERVICE_NAME"
67
+ else
68
+ rm -f "$TMP_FILE"
69
+ echo "chrome-host: warning: could not locate the '$DOCKER_SERVICE_NAME' service in $COMPOSE_FILE; add '$MAPPING' to its extra_hosts manually." >&2
70
+ fi