diff --git a/.changeset/sleek-agents-arrive.md b/.changeset/sleek-agents-arrive.md new file mode 100644 index 0000000..3769ccd --- /dev/null +++ b/.changeset/sleek-agents-arrive.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add a static Pi Web website with installation docs, troubleshooting FAQ, and GitHub Pages deployment. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..3c277ed --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,41 @@ +name: Deploy static site + +on: + push: + branches: [main] + paths: + - docs/** + - .github/workflows/pages.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + deploy: + name: Deploy GitHub Pages + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Configure Pages + uses: actions/configure-pages@v6 + + - name: Upload static site + uses: actions/upload-pages-artifact@v4 + with: + path: docs + + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 65a9a48..3a763d5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Pi Coding Agent](https://img.shields.io/badge/Pi-Coding%20Agent-6f42c1)](https://github.com/earendil-works/pi/tree/main/packages/coding-agent) +Website: + ![Pi Web](docs/assets/pi-web-banner.png) **Run AI coding agents on your own machine or server, keep them alive in real workspaces, and control everything from a browser.** @@ -175,6 +177,8 @@ npm run dev Open the Vite URL, usually . +During development, the static marketing/docs site is also served by the Vite dev server at . + For the recommended split development setup, run these in separate terminals: ```bash diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/faq.html b/docs/faq.html new file mode 100644 index 0000000..83e685a --- /dev/null +++ b/docs/faq.html @@ -0,0 +1,202 @@ + + + + + + Pi Web FAQ + + + + + + + + + + + + +
+
+
+

FAQ & troubleshooting

+

Fix the things that usually go wrong first.

+

+ Most Pi Web install issues are environment issues: systemd availability, PATH setup, Node version managers, + or remote access expectations. +

+
+
+ +
+
+ + +
+
+

Is this Linux only?

+

+ No. Pi Web can run anywhere its runtime dependencies work, including macOS and Windows through WSL. The + automatic pi-web install command is the Linux-specific part because it writes and manages + systemctl --user services. +

+
    +
  • Linux with systemd: use npm install -g @jmfederico/pi-web and pi-web install.
  • +
  • macOS: install the package, then run pi-web-sessiond and pi-web-server manually.
  • +
  • Windows: use WSL. If your WSL distro has systemd enabled, the installer may work; otherwise use the manual run path.
  • +
+

Native Windows outside WSL is not the recommended path today.

+
+ +
+

Tools are failing, node is not found, or Pi cannot find commands

+

+ The shell environment needs to be set up so login shells have the required PATH entries for Pi Web, Pi, + and any tools your agents need. Pi Web services run commands through a non-interactive login shell, so + an interactive terminal can work while services fail. +

+
+
+ Run diagnostics + +
+
$ pi-web doctor
+
+
    +
  • bash: put PATH setup in ~/.bash_profile or ~/.profile. If ~/.bash_profile exists, source ~/.profile from it if needed.
  • +
  • zsh: put PATH setup in ~/.zprofile, not only ~/.zshrc.
  • +
  • fish: prefer universal PATH setup, for example fish_add_path -U ....
  • +
+

Avoid relying only on prompt hooks or interactive-only shell files for tools needed by services.

+
+ +
+

What does pi-web doctor check?

+

+ It checks whether the service shell and systemd user environment can find Node 22+, npm, Pi, and the Pi + Web binaries. It also reports whether user service lingering is enabled for server-style installs. +

+

+ If something works in your terminal but fails in doctor, treat that as a login-shell PATH mismatch and + move the setup earlier in your shell startup chain. +

+
+ +
+

I installed Node with nvm, fnm, asdf, or another version manager

+

+ Version managers often initialize in interactive shells only. Pi Web needs Node and npm to be available to + login shells and systemd user services. +

+
    +
  • Move the version manager initialization to your login shell file.
  • +
  • Make sure node --version is at least v22 from bash -lc, zsh -lc, or your detected shell.
  • +
  • Run pi-web doctor again after changing shell files.
  • +
+
+ +
+

systemctl --user is not available

+

+ The automatic installer needs Linux user systemd. On macOS, WSL without systemd, containers without a + user systemd manager, or minimal Linux environments, run the two processes manually: +

+
+
+ Manual run + +
+
$ pi-web-sessiond
+# in another terminal
+$ PI_WEB_PORT=8504 pi-web-server
+
+
+ +
+

I cannot open the web UI

+
    +
  • Check pi-web status.
  • +
  • Check logs with pi-web logs.
  • +
  • Make sure port 8504 is not already in use.
  • +
  • On a remote server, use an SSH tunnel: ssh -L 8504:127.0.0.1:8504 user@server.
  • +
+
+ +
+

Can I expose Pi Web to the public internet?

+

+ Do not expose it directly. Pi Web assumes trusted users and trusted server paths. Keep the default + localhost bind and use SSH tunneling, a VPN, or a reverse proxy with authentication and network policy. +

+
+ +
+

Sessions stop unexpectedly

+

+ Active agent runtimes are owned by the session daemon. Browser disconnects and web/API restarts should + not stop them. If they stop anyway, check whether the session daemon service restarted or crashed. +

+
+
+ Session daemon logs + +
+
$ systemctl --user status pi-web-sessiond.service
+$ journalctl --user -u pi-web-sessiond.service -f
+
+
+ +
+

Where are logs?

+

For the Linux systemd install, use:

+
+
+ Logs + +
+
$ pi-web logs
+$ journalctl --user -u pi-web-sessiond.service -u pi-web.service -f
+
+
+
+
+
+
+ + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..78e563a --- /dev/null +++ b/docs/index.html @@ -0,0 +1,189 @@ + + + + + + Pi Web — persistent AI coding agents in your browser + + + + + + + + + + + + + +
+
+
+
+

A cockpit for agentic development

+

Your agents keep working. You just need a browser.

+

+ Pi Web runs Pi Coding Agent sessions in real server-side workspaces, keeps them alive when your browser + leaves, and gives you a fast web surface to supervise, redirect, and review the work. +

+ +
+
Persistentsessions survive browser disconnects
+
Parallelprojects, worktrees, and agents
+
Yoursruns on your machine or server
+
+
+ + +
+
+ +
+
+
+
+ Workspaces, sessions, transcripts, terminals — one agent control plane. + Bring your own repositories. +
+ Pi Web browser UI demo +
+
+
+ +
+
+
+

Why it feels different

+

Not another tab. A persistent place for agent work.

+
+
+
+
+

Come back later

+

+ Active sessions live in a long-running daemon, not in your browser tab. Close the lid, reconnect from a + phone, and continue supervising the same work. +

+
+
+
+

Use real workspaces

+

+ Add projects once, discover git worktrees automatically, and start agents where the code actually lives. +

+
+
+
+

Keep control

+

+ Watch transcript history, status, shell activity, context usage, and costs while redirecting the agent in + realtime. +

+
+
+
+
+ +
+
+
+

Quick install

+

Linux servers get the smooth path.

+
    +
  1. Install the package globally with npm.
  2. +
  3. Run the installer to create user-level systemd services.
  4. +
  5. Open the local URL, or tunnel it from a remote machine.
  6. +
+ +
+ +
+
+ Recommended Linux install + +
+
$ npm install -g @jmfederico/pi-web
+$ pi-web install
+$ pi-web doctor
+# Open http://127.0.0.1:8504
+
+
+
+ +
+
+
+

macOS and WSL?

+

+ Pi Web itself is not Linux-only. The one-command service installer targets Linux systemd. macOS and WSL can + run Pi Web manually; WSL with systemd can use the installer too. +

+ Read compatibility notes → +
+
+

Node or tools not found?

+

+ Services run login shells. If tools work in your interactive terminal but not in Pi Web, fix PATH in your + login shell startup files and run the doctor command. +

+ Fix PATH issues → +
+
+
+
+ +
+ +
+ + + diff --git a/docs/install.html b/docs/install.html new file mode 100644 index 0000000..d4d74c0 --- /dev/null +++ b/docs/install.html @@ -0,0 +1,229 @@ + + + + + + Install Pi Web + + + + + + + + + + + + +
+
+
+

Installation guide

+

Get Pi Web running where your agents work.

+

+ The best production-style setup is a Linux machine with user-level systemd services. macOS and Windows WSL + are useful too: run the two Pi Web processes manually, or use the installer in WSL when systemd is enabled. +

+
+
+ +
+
+ + +
+
+

Requirements

+
    +
  • Node.js 22 or newer and npm.
  • +
  • Pi Coding Agent installed/configured so the pi command works for your user.
  • +
  • A shell login environment that exposes Node, npm, Pi, git, and any tools your agents need.
  • +
  • For the automatic installer: Linux with systemctl --user.
  • +
+
+ Important PATH detail: + Pi Web services run through your login shell with -lc. Setup that only lives in interactive shell + files or prompt hooks may not be visible to services. Run pi-web doctor after installing. +
+
+ +
+

Recommended: Linux systemd install

+

+ This creates two user services: one long-lived session daemon and one web/API service. It is the easiest + way to keep sessions available after SSH disconnects or browser restarts. +

+
+
+ Linux install + +
+
$ npm install -g @jmfederico/pi-web
+# Recommended on servers so user services survive logout/reboot:
+$ sudo loginctl enable-linger "$USER"
+$ pi-web install
+$ pi-web doctor
+
+

Then open http://127.0.0.1:8504.

+
+ +
+

One-line install

+

If you prefer a curl pipe, use the repository installer:

+
+
+ One-liner + +
+
$ curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/install.sh | sh
+
+
+ +
+

Install through Pi

+

Pi Web is also published as a Pi package. This exposes a /pi-web command inside Pi.

+
+
+ Pi package path + +
+
$ pi install npm:@jmfederico/pi-web
+
+# Then inside Pi:
+/pi-web install
+/pi-web status
+/pi-web logs
+/pi-web doctor
+
+
+ +
+

macOS / WSL manual run

+

+ Pi Web is not Linux-only, but the pi-web install service setup is Linux/systemd-specific. On macOS + or WSL without systemd, install the package and run the daemon and web server yourself. +

+
+
+ Manual processes + +
+
$ npm install -g @jmfederico/pi-web
+
+# Terminal 1
+$ pi-web-sessiond
+
+# Terminal 2
+$ PI_WEB_PORT=8504 pi-web-server
+
+

+ On modern WSL distributions with systemd enabled, the Linux installer can work. Without systemd, use the + manual run approach above. +

+
+ +
+

Remote access

+

+ Pi Web binds to 127.0.0.1:8504 by default. For a remote server, keep that safe default and use an + SSH tunnel: +

+
+
+ SSH tunnel + +
+
$ ssh -L 8504:127.0.0.1:8504 user@your-server
+# Open http://127.0.0.1:8504 on your local machine
+
+
+ Pi Web is designed for trusted users and trusted server paths. Do not expose it directly to the public + internet without adding your own network controls, authentication, or reverse proxy policy. +
+
+ +
+

Manage services

+
+
+ Useful commands + +
+
$ pi-web status
+$ pi-web logs
+$ pi-web restart
+$ pi-web doctor
+$ systemctl --user status pi-web-sessiond.service pi-web.service
+$ journalctl --user -u pi-web-sessiond.service -u pi-web.service -f
+
+
+ +
+

Configure

+

+ The installer writes a config file under your user config directory. The web server defaults to + 127.0.0.1:8504 and stores Pi Web state in ~/.pi-web. +

+
    +
  • PI_WEB_PORT or PORT: web server port.
  • +
  • PI_WEB_HOST: web server bind host. Keep 127.0.0.1 unless you know why not.
  • +
  • PI_WEB_DATA_DIR: data directory, default ~/.pi-web.
  • +
  • PI_WEB_SESSIOND_SOCKET: Unix socket path for daemon communication.
  • +
+
+ +
+

Uninstall

+

Remove the user services, then remove the npm package if you installed it globally.

+
+
+ Uninstall + +
+
$ pi-web uninstall
+$ npm uninstall -g @jmfederico/pi-web
+
+
+
+
+
+
+ + + + + diff --git a/docs/site.js b/docs/site.js new file mode 100644 index 0000000..cdcc99f --- /dev/null +++ b/docs/site.js @@ -0,0 +1,21 @@ +const copyButtons = document.querySelectorAll("[data-copy]"); + +for (const button of copyButtons) { + button.addEventListener("click", async () => { + const targetSelector = button.getAttribute("data-copy"); + const target = targetSelector === null ? null : document.querySelector(targetSelector); + const text = target?.textContent?.replace(/^\s*\$ /gm, "").trim(); + if (!text) return; + + try { + await navigator.clipboard.writeText(text); + const original = button.textContent; + button.textContent = "Copied"; + window.setTimeout(() => { + button.textContent = original; + }, 1400); + } catch { + button.textContent = "Select code"; + } + }); +} diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..fcbe470 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,646 @@ +:root { + color-scheme: dark; + --bg: #070912; + --bg-soft: #0b0f1d; + --panel: #101527; + --panel-strong: #151b31; + --line: #26304f; + --line-bright: #3d4a78; + --text: #f7f4ff; + --muted: #aaa4bd; + --muted-2: #817a99; + --brand: #7c3cff; + --brand-2: #00f0d8; + --brand-3: #ffb000; + --danger: #ff4f7b; + --radius: 0; + font-family: + "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + background: linear-gradient(180deg, #070912 0%, #080b15 44%, #0a0d18 100%); + color: var(--text); +} + +body::after { + pointer-events: none; + content: ""; + position: fixed; + top: 0; + right: 0; + left: 0; + height: 3px; + background: linear-gradient(90deg, var(--brand), var(--brand-2), var(--brand-3)); +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--brand-2); +} + +img, +video { + display: block; + max-width: 100%; +} + +.container { + width: min(1120px, calc(100% - 40px)); + margin: 0 auto; +} + +.site-header { + position: sticky; + top: 0; + z-index: 10; + border-bottom: 1px solid var(--line); + background: rgba(7, 9, 18, 0.94); + backdrop-filter: blur(10px); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + min-height: 72px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: var(--text); + font-size: 1.05rem; + font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.brand::before { + content: ""; + width: 18px; + height: 3px; + background: var(--brand-2); +} + +.nav-links { + display: flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 0.95rem; +} + +.nav-links a { + padding: 10px 12px; + border: 1px solid transparent; +} + +.nav-links a[aria-current="page"], +.nav-links a:hover { + border-color: var(--line-bright); + background: var(--panel); + color: var(--text); +} + +.button, +.copy-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + border: 1px solid var(--line-bright); + border-radius: var(--radius); + background: var(--panel); + color: var(--text); + font: inherit; + font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-weight: 600; + cursor: pointer; + transition: transform 140ms ease, border-color 140ms ease, background 140ms ease, color 140ms ease; +} + +.button { + padding: 13px 18px; +} + +.button.primary { + border-color: var(--brand-2); + background: var(--brand-2); + color: #02050a; + box-shadow: 7px 7px 0 var(--brand); +} + +.button:hover, +.copy-button:hover { + transform: translate(-1px, -1px); + border-color: var(--brand-2); + background: var(--panel-strong); + color: var(--text); +} + +.button.primary:hover { + border-color: var(--brand-3); + background: var(--brand-3); + color: #02050a; +} + +.hero { + position: relative; + overflow: hidden; + padding: 84px 0 44px; +} + +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(340px, 0.82fr); + gap: 44px; + align-items: center; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + margin: 0 0 18px; + padding: 8px 12px; + border: 1px solid var(--line); + background: var(--panel); + color: var(--muted); + font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.86rem; +} + +.pulse { + width: 9px; + height: 9px; + background: var(--brand-2); + box-shadow: 0 0 0 5px rgba(0, 240, 216, 0.12); +} + +h1, +h2, +h3 { + margin: 0; + letter-spacing: -0.055em; +} + +h1 { + max-width: 760px; + font-size: clamp(3.2rem, 8vw, 6.8rem); + line-height: 0.9; +} + +.gradient-text { + background: linear-gradient(90deg, #ffffff 0%, var(--brand-2) 48%, var(--brand-3) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.hero-lede { + max-width: 660px; + margin: 24px 0 0; + color: var(--muted); + font-size: clamp(1.12rem, 2vw, 1.36rem); + line-height: 1.62; +} + +.hero-actions, +.doc-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 30px; +} + +.hero-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-top: 36px; +} + +.stat, +.card, +.install-card, +.doc-card, +.faq-item, +.callout, +.toc, +.code-card, +.demo-frame { + border: 1px solid var(--line); + background: var(--panel); +} + +.stat { + padding: 18px; + border-top: 3px solid var(--brand); +} + +.stat strong { + display: block; + font-size: 1.4rem; +} + +.stat span { + color: var(--muted-2); + font-size: 0.9rem; +} + +.terminal { + overflow: hidden; + border: 1px solid var(--line-bright); + border-top: 4px solid var(--brand-2); + border-radius: var(--radius); + background: #050710; +} + +.terminal-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 15px 16px; + border-bottom: 1px solid var(--line); + background: #0c1020; + color: var(--muted-2); + font-size: 0.86rem; +} + +.dots { + display: flex; + gap: 7px; +} + +.dots span { + width: 10px; + height: 10px; + background: var(--brand-2); +} + +.dots span:first-child { + background: var(--danger); +} + +.dots span:nth-child(2) { + background: var(--brand-3); +} + +pre, +code { + font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +pre { + margin: 0; + overflow-x: auto; + padding: 22px; + color: #e9e5ff; + font-size: 0.96rem; + line-height: 1.7; +} + +code .prompt, +.prompt { + color: var(--brand-2); +} + +code .comment, +.comment { + color: #928cab; +} + +.quick-install { + margin-top: 18px; +} + +.copy-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 18px 0; +} + +.copy-row strong { + font-size: 0.95rem; +} + +.copy-button { + padding: 8px 11px; + font-size: 0.82rem; +} + +.section { + padding: 74px 0; +} + +.section.compact { + padding-top: 40px; +} + +.section-head { + max-width: 740px; + margin-bottom: 28px; +} + +.section h2, +.page-hero h1 { + font-size: clamp(2.35rem, 5vw, 4.4rem); + line-height: 0.95; +} + +.section p, +.page-hero p, +.doc-content p, +.doc-content li, +.faq-item p, +.faq-item li { + color: var(--muted); + line-height: 1.75; +} + +.cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; +} + +.card, +.install-card, +.doc-card, +.faq-item, +.callout, +.toc, +.code-card { + border-radius: var(--radius); +} + +.card, +.doc-card, +.faq-item, +.callout, +.toc { + padding: 24px; +} + +.card:hover, +.doc-card:hover { + border-color: var(--line-bright); +} + +.card-icon { + display: grid; + width: 46px; + height: 46px; + margin-bottom: 20px; + place-items: center; + border: 1px solid var(--line-bright); + border-left: 4px solid var(--brand-2); + background: var(--panel-strong); + color: var(--brand-2); + font-size: 1.3rem; +} + +.card h3, +.doc-card h3, +.faq-item h2, +.faq-item h3 { + margin-bottom: 10px; + font-size: 1.24rem; + letter-spacing: -0.035em; +} + +.demo-frame { + overflow: hidden; + border-radius: var(--radius); + border-color: var(--line-bright); +} + +.demo-caption { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 15px 18px; + border-bottom: 1px solid var(--line); + background: #0c1020; + color: var(--muted); +} + +.install-panel { + display: grid; + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); + gap: 22px; + align-items: stretch; +} + +.install-card { + padding: 28px; + border-left: 4px solid var(--brand-2); +} + +.install-card ol { + margin: 20px 0 0; + padding-left: 20px; +} + +.install-card li { + margin-bottom: 14px; + color: var(--muted); + line-height: 1.65; +} + +.doc-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 18px; +} + +.doc-card { + min-height: 190px; +} + +.doc-card a { + color: var(--brand-2); + font-weight: 760; +} + +.page-hero { + padding: 76px 0 26px; +} + +.doc-layout { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + gap: 28px; + align-items: start; +} + +.toc { + position: sticky; + top: 96px; + box-shadow: none; +} + +.toc strong { + display: block; + margin-bottom: 12px; +} + +.toc a { + display: block; + padding: 9px 0; + border-left: 2px solid transparent; + color: var(--muted); + font-size: 0.95rem; +} + +.toc a:hover { + border-left-color: var(--brand-2); + padding-left: 8px; +} + +.doc-content { + min-width: 0; +} + +.doc-content section { + scroll-margin-top: 100px; + margin-bottom: 26px; +} + +.doc-content h2 { + margin: 28px 0 12px; + font-size: clamp(1.75rem, 3vw, 2.7rem); +} + +.doc-content h3 { + margin: 22px 0 8px; + font-size: 1.25rem; +} + +.code-card { + overflow: hidden; + margin: 16px 0; + background: #050710; +} + +.callout { + margin: 18px 0; + border-left: 4px solid var(--brand-2); +} + +.callout.warning { + border-color: var(--brand-3); + background: #16140d; +} + +.callout.danger { + border-color: var(--danger); + background: #170d15; +} + +.faq-list { + display: grid; + gap: 18px; +} + +.faq-item ul, +.doc-content ul { + padding-left: 21px; +} + +.kbd { + display: inline-block; + padding: 2px 7px; + border: 1px solid var(--line-bright); + border-radius: var(--radius); + background: #050710; + color: var(--text); + font-size: 0.88em; +} + +.site-footer { + padding: 44px 0 58px; + border-top: 1px solid var(--line); + color: var(--muted-2); +} + +.footer-inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.footer-links { + display: flex; + gap: 14px; +} + +@media (max-width: 920px) { + .hero-grid, + .install-panel, + .doc-layout { + grid-template-columns: 1fr; + } + + .toc { + position: static; + } + + .cards, + .hero-stats, + .doc-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 680px) { + .container { + width: min(100% - 28px, 1120px); + } + + .nav { + align-items: flex-start; + flex-direction: column; + padding: 14px 0; + } + + .nav-links { + flex-wrap: wrap; + } + + .hero { + padding-top: 52px; + } + + h1 { + font-size: clamp(3rem, 15vw, 4.2rem); + } + + .footer-inner { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/package.json b/package.json index ae4977f..26bfb65 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "bugs": { "url": "https://github.com/jmfederico/pi-web/issues" }, - "homepage": "https://github.com/jmfederico/pi-web#readme", + "homepage": "https://jmfederico.github.io/pi-web/", "packageManager": "npm@11.11.0", "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.74.0 <1", diff --git a/vite.config.ts b/vite.config.ts index 18ef412..ae5d5f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,93 @@ +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { extname, join, resolve, sep } from "node:path"; +import type { Plugin } from "vite"; import { defineConfig } from "vite"; import { effectivePiWebConfig } from "./src/config"; const { config } = effectivePiWebConfig(); const apiPort = config.port ?? 8504; +const docsRoot = resolve("docs"); +const docsPrefix = "/site"; + +const contentTypes: Record = { + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".md": "text/markdown; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webm": "video/webm", +}; + +type MiddlewareNext = (error?: unknown) => void; + +async function serveDevDocs(request: IncomingMessage, response: ServerResponse, next: MiddlewareNext): Promise { + const requestUrl = request.url; + if (requestUrl === undefined) { + next(); + return; + } + + const url = new URL(requestUrl, "http://localhost"); + if (url.pathname === docsPrefix) { + response.statusCode = 302; + response.setHeader("Location", `${docsPrefix}/`); + response.end(); + return; + } + if (!url.pathname.startsWith(`${docsPrefix}/`)) { + next(); + return; + } + + const relativePath = decodeURIComponent(url.pathname.slice(docsPrefix.length + 1)) || "index.html"; + const requestedPath = relativePath.endsWith("/") ? join(relativePath, "index.html") : relativePath; + const filePath = resolve(docsRoot, requestedPath); + if (filePath !== docsRoot && !filePath.startsWith(`${docsRoot}${sep}`)) { + response.statusCode = 403; + response.end("Forbidden"); + return; + } + + try { + const fileStat = await stat(filePath); + if (!fileStat.isFile()) { + response.statusCode = 404; + response.end("Not found"); + return; + } + response.statusCode = 200; + response.setHeader("Content-Type", contentTypes[extname(filePath)] ?? "application/octet-stream"); + response.setHeader("Cache-Control", "no-store"); + createReadStream(filePath).pipe(response); + } catch (error) { + const code = error instanceof Error && "code" in error ? error.code : undefined; + if (code === "ENOENT") { + response.statusCode = 404; + response.end("Not found"); + return; + } + next(error); + } +} + +function devDocsPlugin(): Plugin { + return { + name: "pi-web-dev-docs", + apply: "serve", + configureServer(server) { + server.middlewares.use((request, response, next) => { + void serveDevDocs(request, response, next); + }); + }, + }; +} export default defineConfig({ + plugins: [devDocsPlugin()], root: "src/client", build: { outDir: "../../dist/client",