feat: add PWA install icon and uppercase branding
@@ -2,4 +2,4 @@
|
|||||||
"@jmfederico/pi-web": patch
|
"@jmfederico/pi-web": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Fix mobile workspace panels, including the Pi Web status panel, so overflowing content remains scrollable on iPhone.
|
Fix mobile workspace panels, including the PI WEB status panel, so overflowing content remains scrollable on iPhone.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add PWA manifest icons so installed PI WEB apps use the project icon.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Standardize user-facing PI WEB branding in uppercase across the app, docs, and install metadata.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Pi Web
|
# PI WEB
|
||||||
|
|
||||||
[](https://github.com/jmfederico/pi-web/actions/workflows/ci.yml)
|
[](https://github.com/jmfederico/pi-web/actions/workflows/ci.yml)
|
||||||
[](https://www.npmjs.com/package/@jmfederico/pi-web)
|
[](https://www.npmjs.com/package/@jmfederico/pi-web)
|
||||||
@@ -8,15 +8,15 @@
|
|||||||
|
|
||||||
Website: <https://pi-web.dev/>
|
Website: <https://pi-web.dev/>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**Run AI coding agents on your own machine or server, keep them alive in real workspaces, and control everything from a browser.**
|
**Run AI coding agents on your own machine or server, keep them alive in real workspaces, and control everything from a browser.**
|
||||||
|
|
||||||
Pi Web is a web control plane for [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent). Add your repositories once, open project workspaces and git worktrees, start agent sessions inside them, and come back later without losing the work. Your browser becomes the cockpit; your server becomes the persistent development environment. Start on your laptop, check in from your phone, and continue from an iPad or another machine whenever that is the device you have at hand.
|
PI WEB is a web control plane for [Pi Coding Agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent). Add your repositories once, open project workspaces and git worktrees, start agent sessions inside them, and come back later without losing the work. Your browser becomes the cockpit; your server becomes the persistent development environment. Start on your laptop, check in from your phone, and continue from an iPad or another machine whenever that is the device you have at hand.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
With Pi Web you can:
|
With PI WEB you can:
|
||||||
|
|
||||||
- launch and supervise multiple coding-agent sessions in parallel;
|
- launch and supervise multiple coding-agent sessions in parallel;
|
||||||
- keep sessions running when your browser disconnects or the UI restarts;
|
- keep sessions running when your browser disconnects or the UI restarts;
|
||||||
@@ -26,15 +26,15 @@ With Pi Web you can:
|
|||||||
- move fluidly between laptop, phone, tablet, and desktop without moving the development environment;
|
- move fluidly between laptop, phone, tablet, and desktop without moving the development environment;
|
||||||
- turn any server, desktop, or remote dev box into an agent-first development hub.
|
- turn any server, desktop, or remote dev box into an agent-first development hub.
|
||||||
|
|
||||||
## Why use Pi Web?
|
## Why use PI WEB?
|
||||||
|
|
||||||
Agentic development works best when agents are not trapped inside a single local terminal. They need stable environments, access to real repositories, and room to work across branches and tasks. Humans need the opposite: a clear place to supervise, redirect, review, and decide.
|
Agentic development works best when agents are not trapped inside a single local terminal. They need stable environments, access to real repositories, and room to work across branches and tasks. Humans need the opposite: a clear place to supervise, redirect, review, and decide.
|
||||||
|
|
||||||
Pi Web connects those two worlds. The work stays in the server-side environment while you move between devices: laptop for deep focus, phone for a quick check-in, tablet for review, desktop when you are back at a desk. It is not trying to recreate the old desktop IDE in a browser; it is a control surface for persistent, parallel, human-in-the-loop agent work.
|
PI WEB connects those two worlds. The work stays in the server-side environment while you move between devices: laptop for deep focus, phone for a quick check-in, tablet for review, desktop when you are back at a desk. It is not trying to recreate the old desktop IDE in a browser; it is a control surface for persistent, parallel, human-in-the-loop agent work.
|
||||||
|
|
||||||
## Core model
|
## Core model
|
||||||
|
|
||||||
Pi Web organizes work into three levels:
|
PI WEB organizes work into three levels:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Project a folder on the server
|
Project a folder on the server
|
||||||
@@ -65,7 +65,7 @@ This maps naturally to real development work:
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Pi Web uses a split-process architecture so agent runtimes are not owned by the browser-facing dev server.
|
PI WEB uses a split-process architecture so agent runtimes are not owned by the browser-facing dev server.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Browser UI
|
Browser UI
|
||||||
@@ -90,7 +90,7 @@ The web process serves the API and browser UI. In development it can autoreload
|
|||||||
|
|
||||||
## State model
|
## State model
|
||||||
|
|
||||||
Pi Web keeps its own state intentionally small:
|
PI WEB keeps its own state intentionally small:
|
||||||
|
|
||||||
- Projects: `~/.pi-web/projects.json`
|
- Projects: `~/.pi-web/projects.json`
|
||||||
- Workspaces: discovered from git worktrees, not stored
|
- Workspaces: discovered from git worktrees, not stored
|
||||||
@@ -99,18 +99,18 @@ Pi Web keeps its own state intentionally small:
|
|||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
Pi Web production installs can load trusted local UI plugins without rebuilding Pi Web. Plugins are browser-side ES modules that can add action-palette actions, workspace panels, and workspace-label metadata. They do not run in the session daemon and are not sandboxed.
|
PI WEB production installs can load trusted local UI plugins without rebuilding PI WEB. Plugins are browser-side ES modules that can add action-palette actions, workspace panels, and workspace-label metadata. They do not run in the session daemon and are not sandboxed.
|
||||||
|
|
||||||
The supported package shape is intentionally singular: `piWeb.plugins` entries with explicit `id` and `module`, plus a browser module that exports `{ apiVersion: 1, name, activate }`. The bundled `pi-web-plugins/info` TypeScript source is the canonical minimal real example, and `pi-web-plugins/pi-web` demonstrates a dynamic status panel.
|
The supported package shape is intentionally singular: `piWeb.plugins` entries with explicit `id` and `module`, plus a browser module that exports `{ apiVersion: 1, name, activate }`. The bundled `pi-web-plugins/info` TypeScript source is the canonical minimal real example, and `pi-web-plugins/pi-web` demonstrates a dynamic status panel.
|
||||||
|
|
||||||
A useful prompt for AI agents:
|
A useful prompt for AI agents:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Build a Pi Web plugin for this project. Goal: <describe the UI behavior>.
|
Build a PI WEB plugin for this project. Goal: <describe the UI behavior>.
|
||||||
Before coding, read https://pi-web.dev/plugins.html and https://pi-web.dev/plugins.md.
|
Before coding, read https://pi-web.dev/plugins.html and https://pi-web.dev/plugins.md.
|
||||||
Create it under ~/.pi-web/plugins/<plugin-id> using the documented Pi Web v1 plugin API.
|
Create it under ~/.pi-web/plugins/<plugin-id> using the documented PI WEB v1 plugin API.
|
||||||
Validate with /pi-web-plugins/manifest.json and explain reload/debug steps.
|
Validate with /pi-web-plugins/manifest.json and explain reload/debug steps.
|
||||||
Do not modify Pi Web itself.
|
Do not modify PI WEB itself.
|
||||||
```
|
```
|
||||||
|
|
||||||
Reload the browser tab after adding or editing a plugin. If `PI_WEB_DATA_DIR` is set, use `$PI_WEB_DATA_DIR/plugins` instead of `~/.pi-web/plugins`. Check discovery with:
|
Reload the browser tab after adding or editing a plugin. If `PI_WEB_DATA_DIR` is set, use `$PI_WEB_DATA_DIR/plugins` instead of `~/.pi-web/plugins`. Check discovery with:
|
||||||
@@ -134,7 +134,7 @@ sudo loginctl enable-linger "$USER"
|
|||||||
pi-web install
|
pi-web install
|
||||||
```
|
```
|
||||||
|
|
||||||
`loginctl enable-linger` is optional for local desktop use, but recommended on servers. It lets the user systemd manager start at boot and continue running after you log out, so Pi Web remains available without an active SSH/login session.
|
`loginctl enable-linger` is optional for local desktop use, but recommended on servers. It lets the user systemd manager start at boot and continue running after you log out, so PI WEB remains available without an active SSH/login session.
|
||||||
|
|
||||||
This writes and starts:
|
This writes and starts:
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ One-line install is also available for users who prefer it:
|
|||||||
curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/install.sh | sh
|
curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Pi Web is also published as a Pi package. Installing it through Pi exposes a `/pi-web` command inside Pi:
|
PI WEB is also published as a Pi package. Installing it through Pi exposes a `/pi-web` command inside Pi:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pi install npm:@jmfederico/pi-web
|
pi install npm:@jmfederico/pi-web
|
||||||
@@ -183,7 +183,7 @@ Then in Pi:
|
|||||||
/pi-web doctor
|
/pi-web doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
The Pi command is a convenience wrapper around the same service installer. When installed this way, the service installer can use Pi Web's package-local server entrypoints, so `pi-web-server` and `pi-web-sessiond` do not need to be on your shell `PATH`. `/pi-web logs` shows the last 100 journal lines; use `pi-web logs` in a shell when you want to follow logs continuously.
|
The Pi command is a convenience wrapper around the same service installer. When installed this way, the service installer can use PI WEB's package-local server entrypoints, so `pi-web-server` and `pi-web-sessiond` do not need to be on your shell `PATH`. `/pi-web logs` shows the last 100 journal lines; use `pi-web logs` in a shell when you want to follow logs continuously.
|
||||||
|
|
||||||
Advanced users may run the binaries however they prefer:
|
Advanced users may run the binaries however they prefer:
|
||||||
|
|
||||||
@@ -231,9 +231,9 @@ npm publish --access public
|
|||||||
|
|
||||||
`prepack` builds `dist/` and bundled plugin JavaScript before npm creates the tarball, and `prepublishOnly` runs verification before publishing. Releases can also be published by the GitHub Actions npm workflow when a GitHub release is published.
|
`prepack` builds `dist/` and bundled plugin JavaScript before npm creates the tarball, and `prepublishOnly` runs verification before publishing. Releases can also be published by the GitHub Actions npm workflow when a GitHub release is published.
|
||||||
|
|
||||||
Pi Web uses a single-line CalVer-inspired npm version: `MAJOR.YYYYMM.SEQUENCE`, for example `1.202605.1`. The major number signals breaking-change eras; the middle number is the release month; the final number increments for additional releases in that month. Older major eras may be deprecated rather than maintained in parallel.
|
PI WEB uses a single-line CalVer-inspired npm version: `MAJOR.YYYYMM.SEQUENCE`, for example `1.202605.1`. The major number signals breaking-change eras; the middle number is the release month; the final number increments for additional releases in that month. Older major eras may be deprecated rather than maintained in parallel.
|
||||||
|
|
||||||
Pi Web declares `@earendil-works/pi-coding-agent` as a peer dependency (`>=0.74.0 <1`) and a development dependency for local builds. This keeps published installs flexible: npm 7+ installs the peer automatically, and users can upgrade the Pi package within the compatible range without Pi Web pinning a separate copy.
|
PI WEB declares `@earendil-works/pi-coding-agent` as a peer dependency (`>=0.74.0 <1`) and a development dependency for local builds. This keeps published installs flexible: npm 7+ installs the peer automatically, and users can upgrade the Pi package within the compatible range without PI WEB pinning a separate copy.
|
||||||
|
|
||||||
|
|
||||||
The web server defaults to `127.0.0.1:8504`. Set `PI_WEB_HOST=0.0.0.0` only when you intentionally want to bind directly on all interfaces.
|
The web server defaults to `127.0.0.1:8504`. Set `PI_WEB_HOST=0.0.0.0` only when you intentionally want to bind directly on all interfaces.
|
||||||
@@ -248,7 +248,7 @@ Environment variables:
|
|||||||
|
|
||||||
- `PI_WEB_PORT` / `PORT` — web server port. Defaults to `8504`.
|
- `PI_WEB_PORT` / `PORT` — web server port. Defaults to `8504`.
|
||||||
- `PI_WEB_HOST` — web server bind host. Defaults to `127.0.0.1`.
|
- `PI_WEB_HOST` — web server bind host. Defaults to `127.0.0.1`.
|
||||||
- `PI_WEB_DATA_DIR` — Pi Web data directory. Defaults to `~/.pi-web`.
|
- `PI_WEB_DATA_DIR` — PI WEB data directory. Defaults to `~/.pi-web`.
|
||||||
- `PI_WEB_SESSIOND_SOCKET` — Unix socket path used by both the daemon and web process when `PI_WEB_SESSIOND_URL` is not set. Defaults to `$PI_WEB_DATA_DIR/sessiond.sock`.
|
- `PI_WEB_SESSIOND_SOCKET` — Unix socket path used by both the daemon and web process when `PI_WEB_SESSIOND_URL` is not set. Defaults to `$PI_WEB_DATA_DIR/sessiond.sock`.
|
||||||
- `PI_WEB_SESSIOND_PORT` — optional TCP port for the daemon. If unset, the daemon listens on the Unix socket instead.
|
- `PI_WEB_SESSIOND_PORT` — optional TCP port for the daemon. If unset, the daemon listens on the Unix socket instead.
|
||||||
- `PI_WEB_SESSIOND_HOST` — daemon TCP bind host when `PI_WEB_SESSIOND_PORT` is set. Defaults to `127.0.0.1`.
|
- `PI_WEB_SESSIOND_HOST` — daemon TCP bind host when `PI_WEB_SESSIOND_PORT` is set. Defaults to `127.0.0.1`.
|
||||||
@@ -267,7 +267,7 @@ Example units:
|
|||||||
```ini
|
```ini
|
||||||
# ~/.config/systemd/user/pi-web-sessiond.service
|
# ~/.config/systemd/user/pi-web-sessiond.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pi Web session daemon
|
Description=PI WEB session daemon
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@@ -282,7 +282,7 @@ WantedBy=default.target
|
|||||||
```ini
|
```ini
|
||||||
# ~/.config/systemd/user/pi-web-ui-dev.service
|
# ~/.config/systemd/user/pi-web-ui-dev.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pi Web UI dev server
|
Description=PI WEB UI dev server
|
||||||
After=pi-web-sessiond.service
|
After=pi-web-sessiond.service
|
||||||
Wants=pi-web-sessiond.service
|
Wants=pi-web-sessiond.service
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ systemctl --user restart pi-web-sessiond.service
|
|||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
Pi Web is the beginning of an agent-first development environment:
|
PI WEB is the beginning of an agent-first development environment:
|
||||||
|
|
||||||
- agents run persistently on servers;
|
- agents run persistently on servers;
|
||||||
- humans connect through the browser;
|
- humans connect through the browser;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Page not found — PI WEB</title>
|
<title>Page not found — PI WEB</title>
|
||||||
<meta name="description" content="The Pi Web page you requested does not exist." />
|
<meta name="description" content="The PI WEB page you requested does not exist." />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<meta property="og:title" content="Page not found — PI WEB" />
|
<meta property="og:title" content="Page not found — PI WEB" />
|
||||||
<meta property="og:image" content="/assets/pi-web-banner.png" />
|
<meta property="og:image" content="/assets/pi-web-banner.png" />
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
<body class="error-page">
|
<body class="error-page">
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="/" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="/" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/remote-first.html">Remote-first</a>
|
<a href="/remote-first.html">Remote-first</a>
|
||||||
<a href="/install.html">Install</a>
|
<a href="/install.html">Install</a>
|
||||||
<a href="/plugins.html">Plugins</a>
|
<a href="/plugins.html">Plugins</a>
|
||||||
<a href="/faq.html">FAQ</a>
|
<a href="/faq.html">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="error-copy">The route was hallucinated, deleted, or never shipped.</p>
|
<p class="error-copy">The route was hallucinated, deleted, or never shipped.</p>
|
||||||
<div class="error-actions">
|
<div class="error-actions">
|
||||||
<a class="button primary" href="/">Back to Pi Web</a>
|
<a class="button primary" href="/">Back to PI WEB</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web · remote control for persistent Pi Coding Agent sessions.</span>
|
<span>PI WEB · remote control for persistent Pi Coding Agent sessions.</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/install.html">Install</a>
|
<a href="/install.html">Install</a>
|
||||||
<a href="/plugins.html">Plugins</a>
|
<a href="/plugins.html">Plugins</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
|
||||||
<title id="title">piweb mark</title>
|
<title id="title">PI WEB mark</title>
|
||||||
<desc id="desc">A horizontal brand bar.</desc>
|
<desc id="desc">A horizontal brand bar.</desc>
|
||||||
<style>
|
<style>
|
||||||
.bar { fill: #00f0d8; }
|
.bar { fill: #00f0d8; }
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 392 B |
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PI WEB FAQ</title>
|
<title>PI WEB FAQ</title>
|
||||||
<meta name="description" content="Answers for common Pi Web install and runtime issues." />
|
<meta name="description" content="Answers for common PI WEB install and runtime issues." />
|
||||||
<meta property="og:title" content="PI WEB FAQ" />
|
<meta property="og:title" content="PI WEB FAQ" />
|
||||||
<meta property="og:image" content="assets/pi-web-banner.png" />
|
<meta property="og:image" content="assets/pi-web-banner.png" />
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="./" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
<a href="plugins.html">Plugins</a>
|
<a href="plugins.html">Plugins</a>
|
||||||
<a href="faq.html" aria-current="page">FAQ</a>
|
<a href="faq.html" aria-current="page">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<p class="eyebrow"><span class="pulse"></span> FAQ & troubleshooting</p>
|
<p class="eyebrow"><span class="pulse"></span> FAQ & troubleshooting</p>
|
||||||
<h1>Fix the things that usually go wrong first.</h1>
|
<h1>Fix the things that usually go wrong first.</h1>
|
||||||
<p>
|
<p>
|
||||||
Most Pi Web install issues are environment issues: systemd availability, PATH setup, Node version managers,
|
Most PI WEB install issues are environment issues: systemd availability, PATH setup, Node version managers,
|
||||||
or remote access expectations.
|
or remote access expectations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<article id="is-this-linux-only" class="faq-item">
|
<article id="is-this-linux-only" class="faq-item">
|
||||||
<h2>Is this Linux only?</h2>
|
<h2>Is this Linux only?</h2>
|
||||||
<p>
|
<p>
|
||||||
No. Pi Web can run anywhere its runtime dependencies work, including macOS and Windows through WSL. The
|
No. PI WEB can run anywhere its runtime dependencies work, including macOS and Windows through WSL. The
|
||||||
automatic <code>pi-web install</code> command is the Linux-specific part because it writes and manages
|
automatic <code>pi-web install</code> command is the Linux-specific part because it writes and manages
|
||||||
<code>systemctl --user</code> services.
|
<code>systemctl --user</code> services.
|
||||||
</p>
|
</p>
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
<article id="tools-are-not-found" class="faq-item">
|
<article id="tools-are-not-found" class="faq-item">
|
||||||
<h2>Tools are failing, node is not found, or Pi cannot find commands</h2>
|
<h2>Tools are failing, node is not found, or Pi cannot find commands</h2>
|
||||||
<p>
|
<p>
|
||||||
The shell environment needs to be set up so login shells have the required PATH entries for Pi Web, Pi,
|
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
|
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.
|
an interactive terminal can work while services fail.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
@@ -174,9 +174,9 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article id="public-internet" class="faq-item">
|
<article id="public-internet" class="faq-item">
|
||||||
<h2>Can I expose Pi Web to the public internet?</h2>
|
<h2>Can I expose PI WEB to the public internet?</h2>
|
||||||
<p>
|
<p>
|
||||||
Do not expose it directly to the public internet. Pi Web assumes trusted users and trusted server paths.
|
Do not expose it directly to the public internet. PI WEB assumes trusted users and trusted server paths.
|
||||||
For remote access, bind only to an interface that is already private and controlled: for example the
|
For remote access, bind only to an interface that is already private and controlled: for example the
|
||||||
server's VPN IP, a private LAN IP, or localhost behind an SSH tunnel.
|
server's VPN IP, a private LAN IP, or localhost behind an SSH tunnel.
|
||||||
</p>
|
</p>
|
||||||
@@ -190,9 +190,9 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article id="laptop-or-server" class="faq-item">
|
<article id="laptop-or-server" class="faq-item">
|
||||||
<h2>Should I run Pi Web on my laptop or a server?</h2>
|
<h2>Should I run PI WEB on my laptop or a server?</h2>
|
||||||
<p>
|
<p>
|
||||||
You can run it locally, but Pi Web is most useful on a machine that stays on. A remote dev box, home
|
You can run it locally, but PI WEB is most useful on a machine that stays on. A remote dev box, home
|
||||||
server, cloud VM, or workstation gives agents a stable place to keep working while your laptop becomes
|
server, cloud VM, or workstation gives agents a stable place to keep working while your laptop becomes
|
||||||
just a client.
|
just a client.
|
||||||
</p>
|
</p>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article id="plugins" class="faq-item">
|
<article id="plugins" class="faq-item">
|
||||||
<h2>Can I use local plugins with production Pi Web?</h2>
|
<h2>Can I use local plugins with production PI WEB?</h2>
|
||||||
<p>
|
<p>
|
||||||
Yes. Put trusted browser-side plugins in <code>~/.pi-web/plugins/<plugin-id>/</code>, or symlink a
|
Yes. Put trusted browser-side plugins in <code>~/.pi-web/plugins/<plugin-id>/</code>, or symlink a
|
||||||
development folder there. Reload the browser tab after edits. If <code>PI_WEB_DATA_DIR</code> is set, use
|
development folder there. Reload the browser tab after edits. If <code>PI_WEB_DATA_DIR</code> is set, use
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web FAQ</span>
|
<span>PI WEB FAQ</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="./">Home</a>
|
<a href="./">Home</a>
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>PI WEB — persistent AI coding agents in your browser</title>
|
<title>PI WEB — persistent AI coding agents in your browser</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Pi Web is a browser control plane for persistent Pi Coding Agent sessions running on your own machine or server."
|
content="PI WEB is a browser control plane for persistent Pi Coding Agent sessions running on your own machine or server."
|
||||||
/>
|
/>
|
||||||
<meta property="og:title" content="PI WEB" />
|
<meta property="og:title" content="PI WEB" />
|
||||||
<meta
|
<meta
|
||||||
@@ -32,13 +32,13 @@
|
|||||||
<body class="home-page">
|
<body class="home-page">
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="./" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
<a href="plugins.html">Plugins</a>
|
<a href="plugins.html">Plugins</a>
|
||||||
<a href="faq.html">FAQ</a>
|
<a href="faq.html">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-lede">
|
<p class="hero-lede">
|
||||||
Pi Web runs Pi Coding Agent sessions in real server-side workspaces, keeps them alive when your 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.
|
leaves, and gives you a fast web surface to supervise, redirect, and review the work.
|
||||||
</p>
|
</p>
|
||||||
<p class="hero-manifesto">
|
<p class="hero-manifesto">
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="terminal" aria-label="Pi Web install preview">
|
<aside class="terminal" aria-label="PI WEB install preview">
|
||||||
<div class="terminal-top">
|
<div class="terminal-top">
|
||||||
<div class="dots"><span></span><span></span><span></span></div>
|
<div class="dots"><span></span><span></span><span></span></div>
|
||||||
<span>agent-hub.local</span>
|
<span>agent-hub.local</span>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<strong>Workspaces, sessions, transcripts, terminals — one agent control plane.</strong>
|
<strong>Workspaces, sessions, transcripts, terminals — one agent control plane.</strong>
|
||||||
<span>Bring your own repositories.</span>
|
<span>Bring your own repositories.</span>
|
||||||
</div>
|
</div>
|
||||||
<img src="assets/pi-web-demo.gif" alt="Pi Web browser UI demo" />
|
<img src="assets/pi-web-demo.gif" alt="PI WEB browser UI demo" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<h2>Your laptop is a window, not the workstation.</h2>
|
<h2>Your laptop is a window, not the workstation.</h2>
|
||||||
<p>
|
<p>
|
||||||
Local-only development can become the blocker: one device owns the tools, terminals, build cache, editor,
|
Local-only development can become the blocker: one device owns the tools, terminals, build cache, editor,
|
||||||
and agent runtime. Pi Web moves the working environment to a machine that stays on, so agents can keep
|
and agent runtime. PI WEB moves the working environment to a machine that stays on, so agents can keep
|
||||||
making progress while every browser becomes a control surface.
|
making progress while every browser becomes a control surface.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,8 +245,8 @@
|
|||||||
<article class="doc-card">
|
<article class="doc-card">
|
||||||
<h3>macOS and WSL?</h3>
|
<h3>macOS and WSL?</h3>
|
||||||
<p>
|
<p>
|
||||||
Pi Web itself is not Linux-only. The one-command service installer targets Linux systemd. macOS and WSL can
|
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.
|
run PI WEB manually; WSL with systemd can use the installer too.
|
||||||
</p>
|
</p>
|
||||||
<a href="faq.html#is-this-linux-only">Read compatibility notes →</a>
|
<a href="faq.html#is-this-linux-only">Read compatibility notes →</a>
|
||||||
</article>
|
</article>
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
<article class="doc-card">
|
<article class="doc-card">
|
||||||
<h3>Node or tools not found?</h3>
|
<h3>Node or tools not found?</h3>
|
||||||
<p>
|
<p>
|
||||||
Services run login shells. If tools work in your interactive terminal but not in Pi Web, fix PATH in your
|
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.
|
login shell startup files and run the doctor command.
|
||||||
</p>
|
</p>
|
||||||
<a href="faq.html#tools-are-not-found">Fix PATH issues →</a>
|
<a href="faq.html#tools-are-not-found">Fix PATH issues →</a>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web · remote control for persistent Pi Coding Agent sessions.</span>
|
<span>PI WEB · remote control for persistent Pi Coding Agent sessions.</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Install PI WEB</title>
|
<title>Install PI WEB</title>
|
||||||
<meta name="description" content="Complete installation guide for Pi Web on Linux, macOS, and Windows WSL." />
|
<meta name="description" content="Complete installation guide for PI WEB on Linux, macOS, and Windows WSL." />
|
||||||
<meta property="og:title" content="Install PI WEB" />
|
<meta property="og:title" content="Install PI WEB" />
|
||||||
<meta property="og:image" content="assets/pi-web-banner.png" />
|
<meta property="og:image" content="assets/pi-web-banner.png" />
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="./" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
<a href="install.html" aria-current="page">Install</a>
|
<a href="install.html" aria-current="page">Install</a>
|
||||||
<a href="plugins.html">Plugins</a>
|
<a href="plugins.html">Plugins</a>
|
||||||
<a href="faq.html">FAQ</a>
|
<a href="faq.html">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -52,10 +52,10 @@
|
|||||||
<section class="page-hero">
|
<section class="page-hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p class="eyebrow"><span class="pulse"></span> Installation guide</p>
|
<p class="eyebrow"><span class="pulse"></span> Installation guide</p>
|
||||||
<h1>Get Pi Web running where your agents work.</h1>
|
<h1>Get PI WEB running where your agents work.</h1>
|
||||||
<p>
|
<p>
|
||||||
The best production-style setup is a Linux machine with user-level systemd services. macOS and Windows WSL
|
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.
|
are useful too: run the two PI WEB processes manually, or use the installer in WSL when systemd is enabled.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="callout warning">
|
<div class="callout warning">
|
||||||
<strong>Important PATH detail:</strong>
|
<strong>Important PATH detail:</strong>
|
||||||
Pi Web services run through your login shell with <code>-lc</code>. Setup that only lives in interactive shell
|
PI WEB services run through your login shell with <code>-lc</code>. Setup that only lives in interactive shell
|
||||||
files or prompt hooks may not be visible to services. Run <code>pi-web doctor</code> after installing.
|
files or prompt hooks may not be visible to services. Run <code>pi-web doctor</code> after installing.
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -125,8 +125,8 @@
|
|||||||
|
|
||||||
<section id="pi-package">
|
<section id="pi-package">
|
||||||
<h2>Install through Pi</h2>
|
<h2>Install through Pi</h2>
|
||||||
<p>Pi Web is also published as a Pi package. This exposes a <code>/pi-web</code> command inside Pi.</p>
|
<p>PI WEB is also published as a Pi package. This exposes a <code>/pi-web</code> command inside Pi.</p>
|
||||||
<p>When installed this way, <code>/pi-web install</code> can use Pi Web's package-local service entrypoints, so <code>pi-web-server</code> and <code>pi-web-sessiond</code> do not need to be on your shell <code>PATH</code>.</p>
|
<p>When installed this way, <code>/pi-web install</code> can use PI WEB's package-local service entrypoints, so <code>pi-web-server</code> and <code>pi-web-sessiond</code> do not need to be on your shell <code>PATH</code>.</p>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
<div class="copy-row">
|
<div class="copy-row">
|
||||||
<strong>Pi package path</strong>
|
<strong>Pi package path</strong>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<section id="manual-run">
|
<section id="manual-run">
|
||||||
<h2>macOS / WSL manual run</h2>
|
<h2>macOS / WSL manual run</h2>
|
||||||
<p>
|
<p>
|
||||||
Pi Web is not Linux-only, but the <code>pi-web install</code> service setup is Linux/systemd-specific. On macOS
|
PI WEB is not Linux-only, but the <code>pi-web install</code> service setup is Linux/systemd-specific. On macOS
|
||||||
or WSL without systemd, install the package and run the daemon and web server yourself.
|
or WSL without systemd, install the package and run the daemon and web server yourself.
|
||||||
</p>
|
</p>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
<section id="remote-access">
|
<section id="remote-access">
|
||||||
<h2>Remote access</h2>
|
<h2>Remote access</h2>
|
||||||
<p>
|
<p>
|
||||||
Pi Web binds to <code>127.0.0.1:8504</code> by default. For a remote server, the safest option is an SSH
|
PI WEB binds to <code>127.0.0.1:8504</code> by default. For a remote server, the safest option is an SSH
|
||||||
tunnel:
|
tunnel:
|
||||||
</p>
|
</p>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
@@ -182,8 +182,8 @@
|
|||||||
<span class="comment"># Open http://127.0.0.1:8504 on your local machine</span></code></pre>
|
<span class="comment"># Open http://127.0.0.1:8504 on your local machine</span></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="callout danger">
|
<div class="callout danger">
|
||||||
Pi Web is designed for trusted users and trusted server paths. Do not expose it directly to the public
|
PI WEB is designed for trusted users and trusted server paths. Do not expose it directly to the public
|
||||||
internet. If you use a VPN or private network, bind Pi Web to the server's VPN/private IP, such as a
|
internet. If you use a VPN or private network, bind PI WEB to the server's VPN/private IP, such as a
|
||||||
Tailscale/WireGuard/LAN address, and make sure that network policy limits access. Avoid
|
Tailscale/WireGuard/LAN address, and make sure that network policy limits access. Avoid
|
||||||
<code>0.0.0.0</code> unless a firewall, VPN, or authenticated reverse proxy strictly controls the port.
|
<code>0.0.0.0</code> unless a firewall, VPN, or authenticated reverse proxy strictly controls the port.
|
||||||
</div>
|
</div>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
}</code></pre>
|
}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The web server defaults to <code>127.0.0.1:8504</code> and stores Pi Web state in <code>~/.pi-web</code>.
|
The web server defaults to <code>127.0.0.1:8504</code> and stores PI WEB state in <code>~/.pi-web</code>.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>PI_WEB_CONFIG</code>: path to a config JSON file. Defaults to <code>~/.config/pi-web/config.json</code>.</li>
|
<li><code>PI_WEB_CONFIG</code>: path to a config JSON file. Defaults to <code>~/.config/pi-web/config.json</code>.</li>
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
<pre id="uninstall-commands"><code><span class="prompt">$</span> pi-web uninstall
|
<pre id="uninstall-commands"><code><span class="prompt">$</span> pi-web uninstall
|
||||||
<span class="prompt">$</span> npm uninstall -g @jmfederico/pi-web</code></pre>
|
<span class="prompt">$</span> npm uninstall -g @jmfederico/pi-web</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p>Optional cleanup, if you also want to delete Pi Web config and state:</p>
|
<p>Optional cleanup, if you also want to delete PI WEB config and state:</p>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
<div class="copy-row">
|
<div class="copy-row">
|
||||||
<strong>Delete config and data</strong>
|
<strong>Delete config and data</strong>
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web docs</span>
|
<span>PI WEB docs</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="./">Home</a>
|
<a href="./">Home</a>
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PI WEB plugins</title>
|
<title>PI WEB plugins</title>
|
||||||
<meta name="description" content="Use and develop trusted local Pi Web UI plugins." />
|
<meta name="description" content="Use and develop trusted local PI WEB UI plugins." />
|
||||||
<meta property="og:title" content="PI WEB plugins" />
|
<meta property="og:title" content="PI WEB plugins" />
|
||||||
<meta property="og:image" content="assets/pi-web-banner.png" />
|
<meta property="og:image" content="assets/pi-web-banner.png" />
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="./" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
<a href="plugins.html" aria-current="page">Plugins</a>
|
<a href="plugins.html" aria-current="page">Plugins</a>
|
||||||
<a href="faq.html">FAQ</a>
|
<a href="faq.html">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -52,10 +52,10 @@
|
|||||||
<section class="page-hero">
|
<section class="page-hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p class="eyebrow"><span class="pulse"></span> Plugin development</p>
|
<p class="eyebrow"><span class="pulse"></span> Plugin development</p>
|
||||||
<h1>Customize Pi Web with local UI plugins.</h1>
|
<h1>Customize PI WEB with local UI plugins.</h1>
|
||||||
<p>
|
<p>
|
||||||
Plugins are trusted browser-side ES modules. They can add actions, workspace panels, and compact workspace
|
Plugins are trusted browser-side ES modules. They can add actions, workspace panels, and compact workspace
|
||||||
labels to the Pi Web UI.
|
labels to the PI WEB UI.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -76,13 +76,13 @@
|
|||||||
<div class="doc-content">
|
<div class="doc-content">
|
||||||
<section id="extend">
|
<section id="extend">
|
||||||
<h2>What can be extended</h2>
|
<h2>What can be extended</h2>
|
||||||
<p>Plugins can add small, focused UI extensions to Pi Web:</p>
|
<p>Plugins can add small, focused UI extensions to PI WEB:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Action palette commands</strong> for custom workflows.</li>
|
<li><strong>Action palette commands</strong> for custom workflows.</li>
|
||||||
<li><strong>Workspace tools and panels</strong> next to Files, Git, and Terminal.</li>
|
<li><strong>Workspace tools and panels</strong> next to Files, Git, and Terminal.</li>
|
||||||
<li><strong>Workspace labels</strong> in the workspace list, header, and status bar.</li>
|
<li><strong>Workspace labels</strong> in the workspace list, header, and status bar.</li>
|
||||||
<li><strong>Static assets</strong> served from the plugin folder.</li>
|
<li><strong>Static assets</strong> served from the plugin folder.</li>
|
||||||
<li><strong>Browser-side integrations</strong> using Pi Web HTTP/WebSocket APIs.</li>
|
<li><strong>Browser-side integrations</strong> using PI WEB HTTP/WebSocket APIs.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Plugins cannot extend the session daemon or add server-side hooks. They run in the browser UI only.
|
Plugins cannot extend the session daemon or add server-side hooks. They run in the browser UI only.
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<h2>What to ask AI to build</h2>
|
<h2>What to ask AI to build</h2>
|
||||||
<p>
|
<p>
|
||||||
You do not need to hand-code plugins. Give an AI agent a clear goal, the workspace/project facts it
|
You do not need to hand-code plugins. Give an AI agent a clear goal, the workspace/project facts it
|
||||||
should use, and the Pi Web plugin rules.
|
should use, and the PI WEB plugin rules.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Show a workspace badge from a repo file, environment file, branch name, container, or dev URL.</li>
|
<li>Show a workspace badge from a repo file, environment file, branch name, container, or dev URL.</li>
|
||||||
@@ -111,24 +111,24 @@
|
|||||||
<strong>Prompt: create a plugin</strong>
|
<strong>Prompt: create a plugin</strong>
|
||||||
<button class="copy-button" data-copy="#plugin-create-prompt">Copy</button>
|
<button class="copy-button" data-copy="#plugin-create-prompt">Copy</button>
|
||||||
</div>
|
</div>
|
||||||
<pre id="plugin-create-prompt"><code>Build a Pi Web plugin for this project.
|
<pre id="plugin-create-prompt"><code>Build a PI WEB plugin for this project.
|
||||||
Goal: <describe the UI behavior>.
|
Goal: <describe the UI behavior>.
|
||||||
Before coding, read the Pi Web plugin docs:
|
Before coding, read the PI WEB plugin docs:
|
||||||
https://pi-web.dev/plugins.html
|
https://pi-web.dev/plugins.html
|
||||||
Full API reference:
|
Full API reference:
|
||||||
https://pi-web.dev/plugins.md
|
https://pi-web.dev/plugins.md
|
||||||
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
||||||
Use the appropriate extension points from the docs.
|
Use the appropriate extension points from the docs.
|
||||||
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
||||||
Do not modify Pi Web itself.</code></pre>
|
Do not modify PI WEB itself.</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-card">
|
<div class="code-card">
|
||||||
<div class="copy-row">
|
<div class="copy-row">
|
||||||
<strong>Prompt: improve a plugin</strong>
|
<strong>Prompt: improve a plugin</strong>
|
||||||
<button class="copy-button" data-copy="#plugin-improve-prompt">Copy</button>
|
<button class="copy-button" data-copy="#plugin-improve-prompt">Copy</button>
|
||||||
</div>
|
</div>
|
||||||
<pre id="plugin-improve-prompt"><code>Improve the Pi Web plugin at <path>.
|
<pre id="plugin-improve-prompt"><code>Improve the PI WEB plugin at <path>.
|
||||||
Before coding, read the Pi Web plugin docs:
|
Before coding, read the PI WEB plugin docs:
|
||||||
https://pi-web.dev/plugins.html
|
https://pi-web.dev/plugins.html
|
||||||
Full API reference:
|
Full API reference:
|
||||||
https://pi-web.dev/plugins.md
|
https://pi-web.dev/plugins.md
|
||||||
@@ -140,11 +140,11 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
|
|||||||
<section id="example">
|
<section id="example">
|
||||||
<h2>Canonical example</h2>
|
<h2>Canonical example</h2>
|
||||||
<p>
|
<p>
|
||||||
Pi Web ships a real bundled <strong>Info</strong> plugin. It is intentionally small while still using all
|
PI WEB ships a real bundled <strong>Info</strong> plugin. It is intentionally small while still using all
|
||||||
core contribution types: one action, one workspace label, and one workspace panel.
|
core contribution types: one action, one workspace label, and one workspace panel.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Bundled Pi Web plugins are developed as TypeScript in the repository, while their package metadata
|
Bundled PI WEB plugins are developed as TypeScript in the repository, while their package metadata
|
||||||
points at the built JavaScript ES modules that the browser loads. <code>npm run dev:web</code> watches and
|
points at the built JavaScript ES modules that the browser loads. <code>npm run dev:web</code> watches and
|
||||||
rebuilds bundled plugin TS into <code>dist/pi-web-plugins/</code> during development, and <code>npm run build</code>
|
rebuilds bundled plugin TS into <code>dist/pi-web-plugins/</code> during development, and <code>npm run build</code>
|
||||||
emits JS before release packaging.
|
emits JS before release packaging.
|
||||||
@@ -169,7 +169,7 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
|
|||||||
<h2>Production usage</h2>
|
<h2>Production usage</h2>
|
||||||
<p>
|
<p>
|
||||||
Local plugins work with the production npm/systemd install. Put each plugin under
|
Local plugins work with the production npm/systemd install. Put each plugin under
|
||||||
<code>~/.pi-web/plugins/<plugin-id>/</code>, or symlink it there while developing. No Pi Web rebuild or
|
<code>~/.pi-web/plugins/<plugin-id>/</code>, or symlink it there while developing. No PI WEB rebuild or
|
||||||
session-daemon restart is required.
|
session-daemon restart is required.
|
||||||
</p>
|
</p>
|
||||||
<div class="callout warning">
|
<div class="callout warning">
|
||||||
@@ -195,7 +195,7 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
|
|||||||
<p>
|
<p>
|
||||||
If you want to understand the API yourself, ask an agent to read that file and explain the relevant
|
If you want to understand the API yourself, ask an agent to read that file and explain the relevant
|
||||||
extension points for the plugin you want. The Markdown reference also marks which context fields are
|
extension points for the plugin you want. The Markdown reference also marks which context fields are
|
||||||
stable and which Pi Web internals should be avoided unless necessary.
|
stable and which PI WEB internals should be avoided unless necessary.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
|
|||||||
<section id="trust">
|
<section id="trust">
|
||||||
<h2>Trust model</h2>
|
<h2>Trust model</h2>
|
||||||
<p>
|
<p>
|
||||||
Plugins are not sandboxed. They run as JavaScript in the browser, can call Pi Web APIs available to the
|
Plugins are not sandboxed. They run as JavaScript in the browser, can call PI WEB APIs available to the
|
||||||
current session, and can render arbitrary UI. Install only plugins you trust.
|
current session, and can render arbitrary UI. Install only plugins you trust.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -237,7 +237,7 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web plugin docs</span>
|
<span>PI WEB plugin docs</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="./">Home</a>
|
<a href="./">Home</a>
|
||||||
<a href="remote-first.html">Remote-first</a>
|
<a href="remote-first.html">Remote-first</a>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Pi Web plugin API
|
# PI WEB plugin API
|
||||||
|
|
||||||
Pi Web plugins are trusted browser-side ES modules that extend the Pi Web UI. They are intended for personal, team, and project-local customization, and simple enough for an LLM to create or modify directly.
|
PI WEB plugins are trusted browser-side ES modules that extend the PI WEB UI. They are intended for personal, team, and project-local customization, and simple enough for an LLM to create or modify directly.
|
||||||
|
|
||||||
Plugins can currently:
|
Plugins can currently:
|
||||||
|
|
||||||
- add action-palette commands;
|
- add action-palette commands;
|
||||||
- add workspace tools/panels next to Files, Git, and Terminal;
|
- add workspace tools/panels next to Files, Git, and Terminal;
|
||||||
- add compact workspace-label items in the workspace list, panel header, and status bar;
|
- add compact workspace-label items in the workspace list, panel header, and status bar;
|
||||||
- call browser APIs and Pi Web HTTP/WebSocket APIs available to the current browser session;
|
- call browser APIs and PI WEB HTTP/WebSocket APIs available to the current browser session;
|
||||||
- serve their own static assets from the plugin directory.
|
- serve their own static assets from the plugin directory.
|
||||||
|
|
||||||
They do **not** run in the session daemon, do not get a server-side hook API, and are not sandboxed.
|
They do **not** run in the session daemon, do not get a server-side hook API, and are not sandboxed.
|
||||||
@@ -17,8 +17,8 @@ They do **not** run in the session daemon, do not get a server-side hook API, an
|
|||||||
Plugins run as JavaScript in the browser app. Treat them as trusted code:
|
Plugins run as JavaScript in the browser app. Treat them as trusted code:
|
||||||
|
|
||||||
- they can call browser APIs;
|
- they can call browser APIs;
|
||||||
- they can `fetch()` Pi Web API endpoints using the current browser access;
|
- they can `fetch()` PI WEB API endpoints using the current browser access;
|
||||||
- they can read workspace files through Pi Web's file endpoints if the UI can read them;
|
- they can read workspace files through PI WEB's file endpoints if the UI can read them;
|
||||||
- they can render arbitrary Lit templates/custom elements in plugin contribution areas;
|
- they can render arbitrary Lit templates/custom elements in plugin contribution areas;
|
||||||
- they should not be installed from untrusted sources.
|
- they should not be installed from untrusted sources.
|
||||||
|
|
||||||
@@ -37,23 +37,23 @@ Good plugin requests:
|
|||||||
Copy-paste prompt for creating a plugin:
|
Copy-paste prompt for creating a plugin:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Build a Pi Web plugin for this project.
|
Build a PI WEB plugin for this project.
|
||||||
Goal: <describe the UI behavior>.
|
Goal: <describe the UI behavior>.
|
||||||
Before coding, read the Pi Web plugin docs:
|
Before coding, read the PI WEB plugin docs:
|
||||||
https://pi-web.dev/plugins.html
|
https://pi-web.dev/plugins.html
|
||||||
Full API reference:
|
Full API reference:
|
||||||
https://pi-web.dev/plugins.md
|
https://pi-web.dev/plugins.md
|
||||||
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
||||||
Use the appropriate extension points from the docs.
|
Use the appropriate extension points from the docs.
|
||||||
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
||||||
Do not modify Pi Web itself.
|
Do not modify PI WEB itself.
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy-paste prompt for modifying a plugin:
|
Copy-paste prompt for modifying a plugin:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Improve the Pi Web plugin at <path>.
|
Improve the PI WEB plugin at <path>.
|
||||||
Before coding, read the Pi Web plugin docs:
|
Before coding, read the PI WEB plugin docs:
|
||||||
https://pi-web.dev/plugins.html
|
https://pi-web.dev/plugins.html
|
||||||
Full API reference:
|
Full API reference:
|
||||||
https://pi-web.dev/plugins.md
|
https://pi-web.dev/plugins.md
|
||||||
@@ -63,9 +63,9 @@ After editing, check the manifest endpoint and browser-console failure cases.
|
|||||||
|
|
||||||
## Canonical example: bundled Info plugin
|
## Canonical example: bundled Info plugin
|
||||||
|
|
||||||
Pi Web ships a real bundled `info` plugin. Use it as the reference example because it is intentionally small while still exercising all core contribution types: an action, a workspace label, and a workspace panel.
|
PI WEB ships a real bundled `info` plugin. Use it as the reference example because it is intentionally small while still exercising all core contribution types: an action, a workspace label, and a workspace panel.
|
||||||
|
|
||||||
Bundled Pi Web plugins are developed as TypeScript in the repository, but their `package.json` metadata still points at built JavaScript because plugins are loaded by the browser as JS ES modules. `npm run dev:web` watches and rebuilds bundled plugin TS into `dist/pi-web-plugins/` during development, and `npm run build` emits the JS before packaging a release.
|
Bundled PI WEB plugins are developed as TypeScript in the repository, but their `package.json` metadata still points at built JavaScript because plugins are loaded by the browser as JS ES modules. `npm run dev:web` watches and rebuilds bundled plugin TS into `dist/pi-web-plugins/` during development, and `npm run build` emits the JS before packaging a release.
|
||||||
|
|
||||||
Source files:
|
Source files:
|
||||||
|
|
||||||
@@ -112,20 +112,20 @@ export default {
|
|||||||
|
|
||||||
When copying the Info plugin, choose a new plugin id so it does not conflict with the bundled `info` plugin.
|
When copying the Info plugin, choose a new plugin id so it does not conflict with the bundled `info` plugin.
|
||||||
|
|
||||||
Pi Web also ships a `pi-web` status plugin that demonstrates dynamic `visible` and `badge` callbacks for tabs that only appear when the host has status messages or needs extra install visibility.
|
PI WEB also ships a `pi-web` status plugin that demonstrates dynamic `visible` and `badge` callbacks for tabs that only appear when the host has status messages or needs extra install visibility.
|
||||||
|
|
||||||
## Local plugin usage
|
## Local plugin usage
|
||||||
|
|
||||||
This works with the production npm/systemd install. Pi Web discovers plugins from `~/.pi-web/plugins/<plugin-package>/` on the web/API side; no Pi Web rebuild or session-daemon restart is required. If `PI_WEB_DATA_DIR` is set, use `$PI_WEB_DATA_DIR/plugins` instead.
|
This works with the production npm/systemd install. PI WEB discovers plugins from `~/.pi-web/plugins/<plugin-package>/` on the web/API side; no PI WEB rebuild or session-daemon restart is required. If `PI_WEB_DATA_DIR` is set, use `$PI_WEB_DATA_DIR/plugins` instead.
|
||||||
|
|
||||||
Symlink a plugin folder into Pi Web's local plugin directory:
|
Symlink a plugin folder into PI WEB's local plugin directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.pi-web/plugins
|
mkdir -p ~/.pi-web/plugins
|
||||||
ln -s /path/to/plugin-folder ~/.pi-web/plugins/plugin-id
|
ln -s /path/to/plugin-folder ~/.pi-web/plugins/plugin-id
|
||||||
```
|
```
|
||||||
|
|
||||||
Reload the Pi Web browser tab. Pi Web serves plugin modules with an mtime-based `?v=` cache buster. After editing a plugin, hard reload the browser if you do not see changes.
|
Reload the PI WEB browser tab. PI WEB serves plugin modules with an mtime-based `?v=` cache buster. After editing a plugin, hard reload the browser if you do not see changes.
|
||||||
|
|
||||||
## First-party separate plugin packages
|
## First-party separate plugin packages
|
||||||
|
|
||||||
@@ -133,11 +133,11 @@ First-party plugins that are published as their own npm packages can live in thi
|
|||||||
|
|
||||||
A separate plugin package should:
|
A separate plugin package should:
|
||||||
|
|
||||||
- use type-only imports from `@jmfederico/pi-web/plugin-api` when it needs shared Pi Web plugin interfaces; this subpath is currently a `.d.ts`-only dogfooding surface, not a runtime JavaScript module;
|
- use type-only imports from `@jmfederico/pi-web/plugin-api` when it needs shared PI WEB plugin interfaces; this subpath is currently a `.d.ts`-only dogfooding surface, not a runtime JavaScript module;
|
||||||
- keep its Pi Web metadata in its own `package.json` with `piWeb.plugins` entries pointing at built JavaScript in `dist/`;
|
- keep its PI WEB metadata in its own `package.json` with `piWeb.plugins` entries pointing at built JavaScript in `dist/`;
|
||||||
- include a package-level `build` script and `prepack` script so `npm pack --workspace <package>` and `npm publish --workspace <package>` produce a usable plugin package;
|
- include a package-level `build` script and `prepack` script so `npm pack --workspace <package>` and `npm publish --workspace <package>` produce a usable plugin package;
|
||||||
- use a local symlink into `~/.pi-web/plugins/<plugin-id>` while developing;
|
- use a local symlink into `~/.pi-web/plugins/<plugin-id>` while developing;
|
||||||
- document any private Pi Web APIs it dogfoods until those APIs become stable plugin runtime helpers.
|
- document any private PI WEB APIs it dogfoods until those APIs become stable plugin runtime helpers.
|
||||||
|
|
||||||
Typical local development loop from this repository:
|
Typical local development loop from this repository:
|
||||||
|
|
||||||
@@ -146,13 +146,13 @@ npm run dev
|
|||||||
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The main Pi Web `dev` command watches bundled plugins in `pi-web-plugins/`, builds/watches separate plugin packages in `plugins/*`, and discovers those source-checkout plugin packages without symlinking them into `~/.pi-web/plugins`.
|
The main PI WEB `dev` command watches bundled plugins in `pi-web-plugins/`, builds/watches separate plugin packages in `plugins/*`, and discovers those source-checkout plugin packages without symlinking them into `~/.pi-web/plugins`.
|
||||||
|
|
||||||
## Discovery and packaging
|
## Discovery and packaging
|
||||||
|
|
||||||
Pi Web builds `/pi-web-plugins/manifest.json` from these sources:
|
PI WEB builds `/pi-web-plugins/manifest.json` from these sources:
|
||||||
|
|
||||||
1. Bundled plugins in the Pi Web package:
|
1. Bundled plugins in the PI WEB package:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
pi-web-plugins/<plugin-package>/
|
pi-web-plugins/<plugin-package>/
|
||||||
@@ -166,7 +166,7 @@ Pi Web builds `/pi-web-plugins/manifest.json` from these sources:
|
|||||||
|
|
||||||
Entries may be real directories or symlinks. This is the recommended development workflow.
|
Entries may be real directories or symlinks. This is the recommended development workflow.
|
||||||
|
|
||||||
3. Installed Pi packages that expose Pi Web plugin metadata. Pi packages may be user or project scoped.
|
3. Installed Pi packages that expose PI WEB plugin metadata. Pi packages may be user or project scoped.
|
||||||
|
|
||||||
Plugin package directory names and plugin ids must be valid identifiers:
|
Plugin package directory names and plugin ids must be valid identifiers:
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ Plugin package directory names and plugin ids must be valid identifiers:
|
|||||||
^[a-z][a-z0-9.-]*$
|
^[a-z][a-z0-9.-]*$
|
||||||
```
|
```
|
||||||
|
|
||||||
A package can expose one or more Pi Web plugin modules. There is exactly one supported `package.json` metadata shape:
|
A package can expose one or more PI WEB plugin modules. There is exactly one supported `package.json` metadata shape:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -222,7 +222,7 @@ A plugin can fetch its own static assets with URLs under:
|
|||||||
/pi-web-plugins/<plugin-id>/<path-inside-plugin-root>
|
/pi-web-plugins/<plugin-id>/<path-inside-plugin-root>
|
||||||
```
|
```
|
||||||
|
|
||||||
Pi Web prevents asset path traversal outside the plugin root. JavaScript, JSON, CSS, and HTML get appropriate content types; other files are served as octet-stream.
|
PI WEB prevents asset path traversal outside the plugin root. JavaScript, JSON, CSS, and HTML get appropriate content types; other files are served as octet-stream.
|
||||||
|
|
||||||
## Plugin module shape
|
## Plugin module shape
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ export default {
|
|||||||
|
|
||||||
`activate()` is called once when the UI loads the plugin. Keep it cheap: define contributions there, but move expensive or async work into actions, custom elements, or explicit user interactions.
|
`activate()` is called once when the UI loads the plugin. Keep it cheap: define contributions there, but move expensive or async work into actions, custom elements, or explicit user interactions.
|
||||||
|
|
||||||
The plugin id comes from `package.json`, not from the JavaScript module. Contribution ids are local to the plugin and Pi Web qualifies them internally as:
|
The plugin id comes from `package.json`, not from the JavaScript module. Contribution ids are local to the plugin and PI WEB qualifies them internally as:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
<plugin-id>:<local-contribution-id>
|
<plugin-id>:<local-contribution-id>
|
||||||
@@ -345,17 +345,17 @@ Notes:
|
|||||||
|
|
||||||
- `state` is a snapshot of current UI state when actions are built.
|
- `state` is a snapshot of current UI state when actions are built.
|
||||||
- Only `state.selectedWorkspace` and `state.selectedSession` are documented as stable for plugin authors.
|
- Only `state.selectedWorkspace` and `state.selectedSession` are documented as stable for plugin authors.
|
||||||
- Other `state` fields may exist at runtime, but they are Pi Web internals and can change quickly.
|
- Other `state` fields may exist at runtime, but they are PI WEB internals and can change quickly.
|
||||||
- `enabled` is evaluated when the action palette asks for actions.
|
- `enabled` is evaluated when the action palette asks for actions.
|
||||||
- `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`.
|
- `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`.
|
||||||
- `openTerminal()` switches to the built-in terminal panel. Pass `{ terminalId }` to deep-link to a specific terminal after creating one through the terminal API.
|
- `openTerminal()` switches to the built-in terminal panel. Pass `{ terminalId }` to deep-link to a specific terminal after creating one through the terminal API.
|
||||||
|
|
||||||
#### Keyboard shortcuts
|
#### Keyboard shortcuts
|
||||||
|
|
||||||
- App-level keyboard shortcuts must be attached to actions. Pi Web does not support standalone plugin keyboard commands; contribute an action first, then add a `shortcut` if it needs a keybinding.
|
- App-level keyboard shortcuts must be attached to actions. PI WEB does not support standalone plugin keyboard commands; contribute an action first, then add a `shortcut` if it needs a keybinding.
|
||||||
- `shortcut` is the action's default keybinding. It is displayed in the action palette and handled by the global shortcut dispatcher when the action is enabled.
|
- `shortcut` is the action's default keybinding. It is displayed in the action palette and handled by the global shortcut dispatcher when the action is enabled.
|
||||||
- Use modified shortcuts such as `mod+shift+p`; plain letter shortcuts are intentionally ignored so normal typing is never captured.
|
- Use modified shortcuts such as `mod+shift+p`; plain letter shortcuts are intentionally ignored so normal typing is never captured.
|
||||||
- Future Pi Web versions may allow users to override or disable action shortcuts by action id, so plugins should treat `shortcut` as a default rather than a guaranteed final binding.
|
- Future PI WEB versions may allow users to override or disable action shortcuts by action id, so plugins should treat `shortcut` as a default rather than a guaranteed final binding.
|
||||||
- Choose shortcuts carefully to avoid conflicts. There is no user-facing shortcut override or conflict resolver yet.
|
- Choose shortcuts carefully to avoid conflicts. There is no user-facing shortcut override or conflict resolver yet.
|
||||||
- Local text input, terminal input, list navigation, and dialog keys such as Enter, Escape, and arrow keys do not need to be plugin actions unless they are app-level commands.
|
- Local text input, terminal input, list navigation, and dialog keys such as Enter, Escape, and arrow keys do not need to be plugin actions unless they are app-level commands.
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ interface WorkspacePanelContext {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`workspace` and `openTerminal()` are documented as stable for panel callbacks. Other fields may exist at runtime, but they are Pi Web internals and can change quickly. Use `openTerminal({ terminalId })` when a panel creates a terminal and wants Pi Web to navigate to that specific terminal. If a panel needs file, git, or session data, prefer explicit `fetch()` calls and keep them isolated.
|
`workspace` and `openTerminal()` are documented as stable for panel callbacks. Other fields may exist at runtime, but they are PI WEB internals and can change quickly. Use `openTerminal({ terminalId })` when a panel creates a terminal and wants PI WEB to navigate to that specific terminal. If a panel needs file, git, or session data, prefer explicit `fetch()` calls and keep them isolated.
|
||||||
|
|
||||||
Useful workspace shape:
|
Useful workspace shape:
|
||||||
|
|
||||||
@@ -420,7 +420,7 @@ Use existing classes such as `toolbar`, `viewer`, `empty`, and `muted` for panel
|
|||||||
|
|
||||||
### Workspace labels
|
### Workspace labels
|
||||||
|
|
||||||
Workspace labels add compact inline metadata wherever Pi Web displays a workspace label: workspace list, workspace panel header, and status bar.
|
Workspace labels add compact inline metadata wherever PI WEB displays a workspace label: workspace list, workspace panel header, and status bar.
|
||||||
|
|
||||||
Use them for short facts like project environment, local URL, branch status, container name, or health state.
|
Use them for short facts like project environment, local URL, branch status, container name, or health state.
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ interface WorkspaceLabelContext {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Only `workspace` is documented as stable for label callbacks. Other fields may exist at runtime, but they are Pi Web internals and can change quickly.
|
Only `workspace` is documented as stable for label callbacks. Other fields may exist at runtime, but they are PI WEB internals and can change quickly.
|
||||||
|
|
||||||
Items are sorted by `order` and then id. Return an empty array to render nothing.
|
Items are sorted by `order` and then id. Return an empty array to render nothing.
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ Items are sorted by `order` and then id. Return an empty array to render nothing
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Pi Web renders the anchor and adds safe defaults such as `rel="noopener noreferrer"` for `_blank` links. `javascript:` and `data:` links are rendered as plain text instead of links.
|
PI WEB renders the anchor and adds safe defaults such as `rel="noopener noreferrer"` for `_blank` links. `javascript:` and `data:` links are rendered as plain text instead of links.
|
||||||
|
|
||||||
#### Render items
|
#### Render items
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ export default {
|
|||||||
|
|
||||||
## Reading workspace files
|
## Reading workspace files
|
||||||
|
|
||||||
Plugins can use existing Pi Web endpoints. For example, to read a file in a workspace:
|
Plugins can use existing PI WEB endpoints. For example, to read a file in a workspace:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
async function readWorkspaceFile(workspace, path) {
|
async function readWorkspaceFile(workspace, path) {
|
||||||
@@ -533,13 +533,13 @@ async function readWorkspaceFile(workspace, path) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The file response includes fields such as `path`, `content`, `truncated`, and `binary`, but endpoint response shapes are private Pi Web implementation details for now and can change between releases.
|
The file response includes fields such as `path`, `content`, `truncated`, and `binary`, but endpoint response shapes are private PI WEB implementation details for now and can change between releases.
|
||||||
|
|
||||||
Be careful with sensitive files such as `.env`: plugins are trusted browser code, and file contents are exposed to the plugin.
|
Be careful with sensitive files such as `.env`: plugins are trusted browser code, and file contents are exposed to the plugin.
|
||||||
|
|
||||||
## Other useful Pi Web APIs
|
## Other useful PI WEB APIs
|
||||||
|
|
||||||
Plugins may call any endpoint available to the browser, but these HTTP endpoints are considered private Pi Web implementation APIs for now. They can change quickly between releases. Prefer plugin runtime context helpers when they cover the interaction, and keep any direct HTTP usage small and isolated.
|
Plugins may call any endpoint available to the browser, but these HTTP endpoints are considered private PI WEB implementation APIs for now. They can change quickly between releases. Prefer plugin runtime context helpers when they cover the interaction, and keep any direct HTTP usage small and isolated.
|
||||||
|
|
||||||
Common read endpoints:
|
Common read endpoints:
|
||||||
|
|
||||||
@@ -566,11 +566,11 @@ POST /api/sessions/:id/archive
|
|||||||
POST /api/sessions/:id/restore
|
POST /api/sessions/:id/restore
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer runtime context helpers (`startSession`, `stopActiveWork`, `refreshFiles`, `refreshGit`, etc.) when they cover the interaction. Use direct HTTP calls only for plugin-specific data or behavior, and expect to update them as Pi Web evolves.
|
Prefer runtime context helpers (`startSession`, `stopActiveWork`, `refreshFiles`, `refreshGit`, etc.) when they cover the interaction. Use direct HTTP calls only for plugin-specific data or behavior, and expect to update them as PI WEB evolves.
|
||||||
|
|
||||||
## Async data and caching
|
## Async data and caching
|
||||||
|
|
||||||
Pi Web does not provide a plugin cache/invalidation framework. Keep host callbacks cheap:
|
PI WEB does not provide a plugin cache/invalidation framework. Keep host callbacks cheap:
|
||||||
|
|
||||||
- simple contributions should be synchronous and cheap;
|
- simple contributions should be synchronous and cheap;
|
||||||
- expensive or async work should live inside the plugin;
|
- expensive or async work should live inside the plugin;
|
||||||
@@ -580,7 +580,7 @@ Pi Web does not provide a plugin cache/invalidation framework. Keep host callbac
|
|||||||
|
|
||||||
## Agent implementation checklist
|
## Agent implementation checklist
|
||||||
|
|
||||||
If you are an AI agent building or editing a Pi Web plugin, follow this checklist:
|
If you are an AI agent building or editing a PI WEB plugin, follow this checklist:
|
||||||
|
|
||||||
1. Create or update a plugin folder with `package.json` and a JavaScript module such as `pi-web-plugin.js`.
|
1. Create or update a plugin folder with `package.json` and a JavaScript module such as `pi-web-plugin.js`.
|
||||||
2. Use the single supported package metadata shape: `piWeb.plugins` array with `{ id, module }` entries.
|
2. Use the single supported package metadata shape: `piWeb.plugins` array with `{ id, module }` entries.
|
||||||
@@ -594,7 +594,7 @@ If you are an AI agent building or editing a Pi Web plugin, follow this checklis
|
|||||||
10. Add workspace labels for compact inline metadata.
|
10. Add workspace labels for compact inline metadata.
|
||||||
11. Return arrays from workspace label `items()`; return an empty array to render nothing.
|
11. Return arrays from workspace label `items()`; return an empty array to render nothing.
|
||||||
12. Use stable context fields first; only `workspace`, `state.selectedWorkspace`, and `state.selectedSession` are documented as stable.
|
12. Use stable context fields first; only `workspace`, `state.selectedWorkspace`, and `state.selectedSession` are documented as stable.
|
||||||
13. Use `fetch()` against Pi Web APIs only for plugin-specific behavior not provided by runtime context helpers, and isolate those calls because HTTP endpoints are private for now.
|
13. Use `fetch()` against PI WEB APIs only for plugin-specific behavior not provided by runtime context helpers, and isolate those calls because HTTP endpoints are private for now.
|
||||||
14. Treat plugins as trusted code and avoid reading or displaying secrets unless intentional.
|
14. Treat plugins as trusted code and avoid reading or displaying secrets unless intentional.
|
||||||
15. After local edits, tell the user to hard reload the browser and check the console for plugin errors.
|
15. After local edits, tell the user to hard reload the browser and check the console for plugin errors.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>Remote-first development with PI WEB</title>
|
<title>Remote-first development with PI WEB</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Why Pi Web is built for remote AI-driven development with persistent agents and browser-based control."
|
content="Why PI WEB is built for remote AI-driven development with persistent agents and browser-based control."
|
||||||
/>
|
/>
|
||||||
<meta property="og:title" content="Remote-first development with PI WEB" />
|
<meta property="og:title" content="Remote-first development with PI WEB" />
|
||||||
<meta property="og:image" content="assets/pi-web-banner.png" />
|
<meta property="og:image" content="assets/pi-web-banner.png" />
|
||||||
@@ -28,13 +28,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<nav class="container nav" aria-label="Main navigation">
|
<nav class="container nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="./" aria-label="Pi Web home">Pi Web</a>
|
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="remote-first.html" aria-current="page">Remote-first</a>
|
<a href="remote-first.html" aria-current="page">Remote-first</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
<a href="plugins.html">Plugins</a>
|
<a href="plugins.html">Plugins</a>
|
||||||
<a href="faq.html">FAQ</a>
|
<a href="faq.html">FAQ</a>
|
||||||
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="Pi Web on GitHub">
|
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
|
||||||
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<p class="eyebrow"><span class="pulse"></span> Remote-first development</p>
|
<p class="eyebrow"><span class="pulse"></span> Remote-first development</p>
|
||||||
<h1>Your laptop is a window, not the workstation.</h1>
|
<h1>Your laptop is a window, not the workstation.</h1>
|
||||||
<p>
|
<p>
|
||||||
Pi Web is built around a simple shift: development should live where the work happens, not where your
|
PI WEB is built around a simple shift: development should live where the work happens, not where your
|
||||||
current keyboard happens to be.
|
current keyboard happens to be.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<section id="browser-control">
|
<section id="browser-control">
|
||||||
<h2>The browser becomes the control plane</h2>
|
<h2>The browser becomes the control plane</h2>
|
||||||
<p>
|
<p>
|
||||||
Your device should not have to be the editor, terminal, build server, and agent runtime. In Pi Web, the
|
Your device should not have to be the editor, terminal, build server, and agent runtime. In PI WEB, the
|
||||||
browser is the cockpit: supervise sessions, redirect agents, inspect transcripts, and review progress
|
browser is the cockpit: supervise sessions, redirect agents, inspect transcripts, and review progress
|
||||||
while the development environment stays remote and stable.
|
while the development environment stays remote and stable.
|
||||||
</p>
|
</p>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<section id="recommended-shape">
|
<section id="recommended-shape">
|
||||||
<h2>Recommended shape</h2>
|
<h2>Recommended shape</h2>
|
||||||
<p>
|
<p>
|
||||||
Pi Web works locally, but it shines on a machine that stays on and is reachable through a private path:
|
PI WEB works locally, but it shines on a machine that stays on and is reachable through a private path:
|
||||||
SSH tunnel, VPN, private LAN, or an authenticated reverse proxy.
|
SSH tunnel, VPN, private LAN, or an authenticated reverse proxy.
|
||||||
</p>
|
</p>
|
||||||
<div class="callout warning">
|
<div class="callout warning">
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
keep working even when the laptop is gone.
|
keep working even when the laptop is gone.
|
||||||
</div>
|
</div>
|
||||||
<div class="doc-actions">
|
<div class="doc-actions">
|
||||||
<a class="button primary" href="install.html">Install Pi Web</a>
|
<a class="button primary" href="install.html">Install PI WEB</a>
|
||||||
<a class="button" href="faq.html#laptop-or-server">Laptop or server FAQ</a>
|
<a class="button" href="faq.html#laptop-or-server">Laptop or server FAQ</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container footer-inner">
|
<div class="container footer-inner">
|
||||||
<span>Pi Web · remote-first control for persistent AI agents.</span>
|
<span>PI WEB · remote-first control for persistent AI agents.</span>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="./">Home</a>
|
<a href="./">Home</a>
|
||||||
<a href="install.html">Install</a>
|
<a href="install.html">Install</a>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ async function boundedLogs(): Promise<{ code: number; output: string }> {
|
|||||||
|
|
||||||
export default function piWebExtension(pi: ExtensionAPI): void {
|
export default function piWebExtension(pi: ExtensionAPI): void {
|
||||||
pi.registerCommand("pi-web", {
|
pi.registerCommand("pi-web", {
|
||||||
description: "Manage Pi Web services: install, status, logs, restart, start, stop, doctor, open",
|
description: "Manage PI WEB services: install, status, logs, restart, start, stop, doctor, open",
|
||||||
getArgumentCompletions(prefix: string): { value: string; label: string }[] | null {
|
getArgumentCompletions(prefix: string): { value: string; label: string }[] | null {
|
||||||
const [first = ""] = parseArgs(prefix);
|
const [first = ""] = parseArgs(prefix);
|
||||||
const items = subcommands
|
const items = subcommands
|
||||||
@@ -95,12 +95,12 @@ export default function piWebExtension(pi: ExtensionAPI): void {
|
|||||||
const rest = parsedArgs.slice(1);
|
const rest = parsedArgs.slice(1);
|
||||||
|
|
||||||
if (subcommand === "help") {
|
if (subcommand === "help") {
|
||||||
ctx.ui.notify(`Pi Web commands:\n\n${subcommands.map((command) => `/pi-web ${command}`).join("\n")}\n\nLogs are bounded to the last 100 journal lines in the Pi command. Use \`pi-web logs\` in a shell to follow logs.`, "info");
|
ctx.ui.notify(`PI WEB commands:\n\n${subcommands.map((command) => `/pi-web ${command}`).join("\n")}\n\nLogs are bounded to the last 100 journal lines in the Pi command. Use \`pi-web logs\` in a shell to follow logs.`, "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === "open") {
|
if (subcommand === "open") {
|
||||||
ctx.ui.notify("Pi Web default URL: http://127.0.0.1:8504", "info");
|
ctx.ui.notify("PI WEB default URL: http://127.0.0.1:8504", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
const status = statusFor(state);
|
const status = statusFor(state);
|
||||||
if (status === undefined) {
|
if (status === undefined) {
|
||||||
return html`
|
return html`
|
||||||
<section class="toolbar"><strong>Pi Web</strong></section>
|
<section class="toolbar"><strong>PI WEB</strong></section>
|
||||||
<section class="viewer"><p class="muted">Checking Pi Web status…</p></section>
|
<section class="viewer"><p class="muted">Checking PI WEB status…</p></section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
.pi-web-command > span { grid-column: 1 / -1; }
|
.pi-web-command > span { grid-column: 1 / -1; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<section class="toolbar"><strong>Pi Web</strong><span class="stale">beta</span>${messages.length > 0 ? html`<span class="stale">${String(messages.length)}</span>` : null}</section>
|
<section class="toolbar"><strong>PI WEB</strong><span class="stale">beta</span>${messages.length > 0 ? html`<span class="stale">${String(messages.length)}</span>` : null}</section>
|
||||||
<section class="viewer pi-web-status">
|
<section class="viewer pi-web-status">
|
||||||
<section>
|
<section>
|
||||||
${messages.length === 0 ? html`<p class="muted">No Pi Web update or restart messages.</p>` : messages.map((message) => html`
|
${messages.length === 0 ? html`<p class="muted">No PI WEB update or restart messages.</p>` : messages.map((message) => html`
|
||||||
<article class=${`pi-web-message ${message.severity}`}>
|
<article class=${`pi-web-message ${message.severity}`}>
|
||||||
<div class="pi-web-message-title"><strong>${message.title}</strong><span>${message.severity}</span></div>
|
<div class="pi-web-message-title"><strong>${message.title}</strong><span>${message.severity}</span></div>
|
||||||
<p>${message.body}</p>
|
<p>${message.body}</p>
|
||||||
@@ -136,13 +136,13 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
|
|
||||||
const plugin: PiWebPlugin = {
|
const plugin: PiWebPlugin = {
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
name: "Pi Web Status",
|
name: "PI WEB Status",
|
||||||
activate: ({ html }) => ({
|
activate: ({ html }) => ({
|
||||||
contributions: {
|
contributions: {
|
||||||
workspacePanels: [
|
workspacePanels: [
|
||||||
{
|
{
|
||||||
id: "workspace.status",
|
id: "workspace.status",
|
||||||
title: "Pi Web",
|
title: "PI WEB",
|
||||||
order: 100,
|
order: 100,
|
||||||
visible: (context) => shouldShowStatusPanel(context.state),
|
visible: (context) => shouldShowStatusPanel(context.state),
|
||||||
badge: (context) => {
|
badge: (context) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Pi Web Actions
|
# PI WEB Actions
|
||||||
|
|
||||||
Configurable workspace actions for Pi Web.
|
Configurable workspace actions for PI WEB.
|
||||||
|
|
||||||
The plugin adds an **Actions** workspace tab. Actions create a new Pi Web terminal, send the configured shell command, and switch to that terminal so the user can monitor progress or take over.
|
The plugin adds an **Actions** workspace tab. Actions create a new PI WEB terminal, send the configured shell command, and switch to that terminal so the user can monitor progress or take over.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -41,19 +41,19 @@ Fields:
|
|||||||
- `group`: optional group heading.
|
- `group`: optional group heading.
|
||||||
- `confirm`: optional boolean. When true, the browser asks before dispatching the command.
|
- `confirm`: optional boolean. When true, the browser asks before dispatching the command.
|
||||||
|
|
||||||
Commands run in the workspace root because Pi Web creates the terminal for that workspace.
|
Commands run in the workspace root because PI WEB creates the terminal for that workspace.
|
||||||
|
|
||||||
After editing `.pi-web/actions.json`, click **Refresh** in the Actions tab or reload the browser tab. The plugin does not watch the file automatically.
|
After editing `.pi-web/actions.json`, click **Refresh** in the Actions tab or reload the browser tab. The plugin does not watch the file automatically.
|
||||||
|
|
||||||
## Development in this monorepo
|
## Development in this monorepo
|
||||||
|
|
||||||
This package is developed as a separate npm package, not as a bundled Pi Web plugin. From the Pi Web repository, the single root dev command builds, watches, and auto-loads this package without symlinking it into `~/.pi-web/plugins`:
|
This package is developed as a separate npm package, not as a bundled PI WEB plugin. From the PI WEB repository, the single root dev command builds, watches, and auto-loads this package without symlinking it into `~/.pi-web/plugins`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then reload Pi Web and check discovery:
|
Then reload PI WEB and check discovery:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
||||||
@@ -68,7 +68,7 @@ npm pack --workspace @jmfederico/pi-web-actions --dry-run
|
|||||||
|
|
||||||
## Beta/private API note
|
## Beta/private API note
|
||||||
|
|
||||||
This plugin intentionally dogfoods private Pi Web browser APIs for reading workspace files and creating/writing terminals. Those APIs are not yet stable public plugin APIs, so compatibility is best-effort and may require updates alongside Pi Web releases.
|
This plugin intentionally dogfoods private PI WEB browser APIs for reading workspace files and creating/writing terminals. Those APIs are not yet stable public plugin APIs, so compatibility is best-effort and may require updates alongside PI WEB releases.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@jmfederico/pi-web-actions",
|
"name": "@jmfederico/pi-web-actions",
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"description": "Configurable workspace actions plugin for Pi Web.",
|
"description": "Configurable workspace actions plugin for PI WEB.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
|
|
||||||
private openWorkspaceTerminal(terminalId?: string): void {
|
private openWorkspaceTerminal(terminalId?: string): void {
|
||||||
if (this.openTerminalValue === undefined) {
|
if (this.openTerminalValue === undefined) {
|
||||||
this.status = { kind: "error", message: "This Pi Web version does not provide terminal navigation to plugins." };
|
this.status = { kind: "error", message: "This PI WEB version does not provide terminal navigation to plugins." };
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,14 +203,14 @@ function describeServiceShell(): string {
|
|||||||
if (shell.fallback) {
|
if (shell.fallback) {
|
||||||
return shell.detected === undefined
|
return shell.detected === undefined
|
||||||
? "could not detect a supported login shell; using bash"
|
? "could not detect a supported login shell; using bash"
|
||||||
: `detected ${shell.detected}; using bash because Pi Web currently supports bash, zsh, and fish`;
|
: `detected ${shell.detected}; using bash because PI WEB currently supports bash, zsh, and fish`;
|
||||||
}
|
}
|
||||||
return shell.detected === undefined ? shell.name : `${shell.name} (${shell.detected})`;
|
return shell.detected === undefined ? shell.name : `${shell.name} (${shell.detected})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessiondUnit(executables: ServiceExecutables): string {
|
function sessiondUnit(executables: ServiceExecutables): string {
|
||||||
return `[Unit]
|
return `[Unit]
|
||||||
Description=Pi Web session daemon
|
Description=PI WEB session daemon
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@@ -226,7 +226,7 @@ WantedBy=default.target
|
|||||||
function webUnit(options: InstallOptions, executables: ServiceExecutables): string {
|
function webUnit(options: InstallOptions, executables: ServiceExecutables): string {
|
||||||
const configEnvironment = options.config === undefined ? "" : `Environment="PI_WEB_CONFIG=${systemdEscape(resolve(options.config))}"\n`;
|
const configEnvironment = options.config === undefined ? "" : `Environment="PI_WEB_CONFIG=${systemdEscape(resolve(options.config))}"\n`;
|
||||||
return `[Unit]
|
return `[Unit]
|
||||||
Description=Pi Web server
|
Description=PI WEB server
|
||||||
After=${sessiondServiceName}
|
After=${sessiondServiceName}
|
||||||
Wants=${sessiondServiceName}
|
Wants=${sessiondServiceName}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ async function install(args: string[]): Promise<void> {
|
|||||||
const options = parseInstallOptions(args);
|
const options = parseInstallOptions(args);
|
||||||
|
|
||||||
const executables = resolveServiceExecutables();
|
const executables = resolveServiceExecutables();
|
||||||
console.log("Running Pi Web install preflight checks...");
|
console.log("Running PI WEB install preflight checks...");
|
||||||
console.log(`Service shell: ${describeServiceShell()}`);
|
console.log(`Service shell: ${describeServiceShell()}`);
|
||||||
if (!runChecks(installPreflightChecks(executables))) {
|
if (!runChecks(installPreflightChecks(executables))) {
|
||||||
printPathSetupAdvice();
|
printPathSetupAdvice();
|
||||||
@@ -271,7 +271,7 @@ async function install(args: string[]): Promise<void> {
|
|||||||
run("systemctl", ["--user", "enable", "--now", sessiondServiceName], { check: true });
|
run("systemctl", ["--user", "enable", "--now", sessiondServiceName], { check: true });
|
||||||
run("systemctl", ["--user", "enable", "--now", webServiceName], { check: true });
|
run("systemctl", ["--user", "enable", "--now", webServiceName], { check: true });
|
||||||
|
|
||||||
console.log(`\nPi Web is installed and starting.`);
|
console.log(`\nPI WEB is installed and starting.`);
|
||||||
console.log(`Config: ${configPath}`);
|
console.log(`Config: ${configPath}`);
|
||||||
console.log(`Open: http://${options.host === "0.0.0.0" ? "127.0.0.1" : options.host}:${options.port}`);
|
console.log(`Open: http://${options.host === "0.0.0.0" ? "127.0.0.1" : options.host}:${options.port}`);
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ async function uninstall(): Promise<void> {
|
|||||||
await rm(join(serviceDir, webServiceName), { force: true });
|
await rm(join(serviceDir, webServiceName), { force: true });
|
||||||
await rm(join(serviceDir, sessiondServiceName), { force: true });
|
await rm(join(serviceDir, sessiondServiceName), { force: true });
|
||||||
run("systemctl", ["--user", "daemon-reload"]);
|
run("systemctl", ["--user", "daemon-reload"]);
|
||||||
console.log("Pi Web systemd user services removed.");
|
console.log("PI WEB systemd user services removed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function serviceAction(action: "start" | "stop" | "restart" | "status"): void {
|
function serviceAction(action: "start" | "stop" | "restart" | "status"): void {
|
||||||
@@ -384,10 +384,10 @@ function printPathSetupAdvice(): void {
|
|||||||
console.log(" Do not rely only on ~/.bashrc or prompt hooks for tools needed by services or agents.");
|
console.log(" Do not rely only on ~/.bashrc or prompt hooks for tools needed by services or agents.");
|
||||||
} else if (shell.name === "zsh") {
|
} else if (shell.name === "zsh") {
|
||||||
console.log(" Detected zsh. Put PATH setup for node/version managers/tools in ~/.zprofile, not only ~/.zshrc.");
|
console.log(" Detected zsh. Put PATH setup for node/version managers/tools in ~/.zprofile, not only ~/.zshrc.");
|
||||||
console.log(" Avoid relying on prompt hooks; Pi Web services run non-interactive login shells.");
|
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
||||||
} else {
|
} else {
|
||||||
console.log(" Detected fish. Prefer universal PATH setup such as `fish_add_path -U ...` for tools needed by services or agents.");
|
console.log(" Detected fish. Prefer universal PATH setup such as `fish_add_path -U ...` for tools needed by services or agents.");
|
||||||
console.log(" Avoid relying on prompt hooks; Pi Web services run non-interactive login shells.");
|
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,14 +408,14 @@ function doctor(): void {
|
|||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.log("\nIf a command works in your terminal but fails here, make sure your service shell login files set PATH the same way.");
|
console.log("\nIf a command works in your terminal but fails here, make sure your service shell login files set PATH the same way.");
|
||||||
console.log("If a bundled entrypoint is not accessible, reinstall or update the Pi Web package.");
|
console.log("If a bundled entrypoint is not accessible, reinstall or update the PI WEB package.");
|
||||||
printPathSetupAdvice();
|
printPathSetupAdvice();
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function help(): void {
|
function help(): void {
|
||||||
console.log(`Pi Web
|
console.log(`PI WEB
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pi-web install [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
|
pi-web install [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<title>PI WEB</title>
|
<title>PI WEB</title>
|
||||||
|
<meta name="theme-color" content="#0d1117" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 452 B |
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
|
||||||
<title id="title">piweb mark</title>
|
<title id="title">PI WEB mark</title>
|
||||||
<desc id="desc">A horizontal brand bar.</desc>
|
<desc id="desc">A horizontal brand bar.</desc>
|
||||||
<style>
|
<style>
|
||||||
.bar { fill: #00f0d8; }
|
.bar { fill: #00f0d8; }
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 392 B |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "PI WEB",
|
||||||
|
"short_name": "PI WEB",
|
||||||
|
"description": "Remote web UI and browser control plane for persistent Pi Coding Agent sessions.",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0d1117",
|
||||||
|
"theme_color": "#0d1117",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/pwa-icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -359,9 +359,9 @@ function optionalPiWebInstallationInfo(value: unknown): PiWebInstallationInfo |
|
|||||||
if (value === undefined) return undefined;
|
if (value === undefined) return undefined;
|
||||||
const record = requireRecord(value);
|
const record = requireRecord(value);
|
||||||
const kind = requireString(record, "kind");
|
const kind = requireString(record, "kind");
|
||||||
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "unknown") throw new Error("Invalid Pi Web installation kind");
|
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "unknown") throw new Error("Invalid PI WEB installation kind");
|
||||||
const scope = record["scope"];
|
const scope = record["scope"];
|
||||||
if (scope !== undefined && scope !== "user" && scope !== "project") throw new Error("Invalid Pi Web installation scope");
|
if (scope !== undefined && scope !== "user" && scope !== "project") throw new Error("Invalid PI WEB installation scope");
|
||||||
return {
|
return {
|
||||||
kind,
|
kind,
|
||||||
...optionalField("path", optionalString(record, "path")),
|
...optionalField("path", optionalString(record, "path")),
|
||||||
@@ -400,12 +400,12 @@ function parsePiWebStatusMessage(value: unknown): PiWebStatusMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebServiceComponent(value: unknown): PiWebServiceComponent {
|
function parsePiWebServiceComponent(value: unknown): PiWebServiceComponent {
|
||||||
if (value !== "web" && value !== "sessiond") throw new Error("Invalid Pi Web service component");
|
if (value !== "web" && value !== "sessiond") throw new Error("Invalid PI WEB service component");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebStatusSeverity(value: unknown): PiWebStatusSeverity {
|
function parsePiWebStatusSeverity(value: unknown): PiWebStatusSeverity {
|
||||||
if (value !== "info" && value !== "warning" && value !== "error") throw new Error("Invalid Pi Web status severity");
|
if (value !== "info" && value !== "warning" && value !== "error") throw new Error("Invalid PI WEB status severity");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export class PiWebApp extends LitElement {
|
|||||||
try {
|
try {
|
||||||
this.setState({ piWebStatus: await piWebApi.piWebStatus() });
|
this.setState({ piWebStatus: await piWebApi.piWebStatus() });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to refresh Pi Web status", error);
|
console.warn("Failed to refresh PI WEB status", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ export class PiWebApp extends LitElement {
|
|||||||
});
|
});
|
||||||
return html`
|
return html`
|
||||||
<header>
|
<header>
|
||||||
<strong>Pi Web</strong>
|
<strong>PI WEB</strong>
|
||||||
<button title="Show Actions" aria-label="Show Actions" @click=${() => { this.setState({ actionPaletteOpen: true }); }}>Actions</button>
|
<button title="Show Actions" aria-label="Show Actions" @click=${() => { this.setState({ actionPaletteOpen: true }); }}>Actions</button>
|
||||||
</header>
|
</header>
|
||||||
<project-list
|
<project-list
|
||||||
@@ -517,7 +517,7 @@ export class PiWebApp extends LitElement {
|
|||||||
if (this.state.isLoadingProjects) {
|
if (this.state.isLoadingProjects) {
|
||||||
return {
|
return {
|
||||||
title: "Loading projects…",
|
title: "Loading projects…",
|
||||||
body: "Looking for projects you have added to Pi Web.",
|
body: "Looking for projects you have added to PI WEB.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (project === undefined) {
|
if (project === undefined) {
|
||||||
@@ -603,13 +603,13 @@ export class PiWebApp extends LitElement {
|
|||||||
try {
|
try {
|
||||||
this.plugins.register(registration);
|
this.plugins.register(registration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to register Pi Web plugin ${registration.id}`, error);
|
console.warn(`Failed to register PI WEB plugin ${registration.id}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.applyPreferredTheme(false);
|
this.applyPreferredTheme(false);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to load external Pi Web plugins", error);
|
console.warn("Failed to load external PI WEB plugins", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export class ProjectList extends LitElement {
|
|||||||
|
|
||||||
private close(project: Project) {
|
private close(project: Project) {
|
||||||
this.openMenuProjectId = undefined;
|
this.openMenuProjectId = undefined;
|
||||||
if (confirm(`Close ${project.name}?\n\nThis only removes it from Pi Web; it will not change the project folder.`)) this.onClose?.(project);
|
if (confirm(`Close ${project.name}?\n\nThis only removes it from PI WEB; it will not change the project folder.`)) this.onClose?.(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
static override styles = listStyles;
|
static override styles = listStyles;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function createCoreActions(): PluginAction[] {
|
|||||||
{
|
{
|
||||||
id: "theme.select",
|
id: "theme.select",
|
||||||
title: "Select Theme",
|
title: "Select Theme",
|
||||||
description: "Choose the Pi Web color theme",
|
description: "Choose the PI WEB color theme",
|
||||||
group: "Preferences",
|
group: "Preferences",
|
||||||
run: (context) => { context.openThemePicker(); },
|
run: (context) => { context.openThemePicker(); },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createCoreWorkspacePanels } from "./panels";
|
|||||||
|
|
||||||
export const corePlugin: PiWebPlugin = {
|
export const corePlugin: PiWebPlugin = {
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
name: "Pi Web Core",
|
name: "PI WEB Core",
|
||||||
activate: () => ({
|
activate: () => ({
|
||||||
contributions: {
|
contributions: {
|
||||||
actions: createCoreActions(),
|
actions: createCoreActions(),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export async function loadExternalPlugins(manifestUrl = "/pi-web-plugins/manifes
|
|||||||
const plugin = parsePluginModule(module, moduleUrl);
|
const plugin = parsePluginModule(module, moduleUrl);
|
||||||
registrations.push({ id: entry.id, plugin });
|
registrations.push({ id: entry.id, plugin });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to load Pi Web plugin ${entry.module}`, error);
|
console.warn(`Failed to load PI WEB plugin ${entry.module}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return registrations;
|
return registrations;
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe("PluginRegistry", () => {
|
|||||||
expect(new Set(shortcuts.map(([, shortcut]) => shortcut)).size).toBe(shortcuts.length);
|
expect(new Set(shortcuts.map(([, shortcut]) => shortcut)).size).toBe(shortcuts.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("collects built-in Pi Web themes from an in-app plugin", () => {
|
it("collects built-in PI WEB themes from an in-app plugin", () => {
|
||||||
const registry = new PluginRegistry();
|
const registry = new PluginRegistry();
|
||||||
registry.register({ id: "themes", plugin: themePackPlugin });
|
registry.register({ id: "themes", plugin: themePackPlugin });
|
||||||
|
|
||||||
|
|||||||
@@ -116,30 +116,30 @@ const piWebLightTokens = {
|
|||||||
|
|
||||||
export const themePackPlugin: PiWebPlugin = {
|
export const themePackPlugin: PiWebPlugin = {
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
name: "Pi Web Themes",
|
name: "PI WEB Themes",
|
||||||
activate: () => ({
|
activate: () => ({
|
||||||
contributions: {
|
contributions: {
|
||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
id: "pi-web-dark",
|
id: "pi-web-dark",
|
||||||
name: "Pi Web Dark",
|
name: "PI WEB Dark",
|
||||||
description: "Dark Pi Web palette.",
|
description: "Dark PI WEB palette.",
|
||||||
order: 10,
|
order: 10,
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
tokens: piWebDarkTokens,
|
tokens: piWebDarkTokens,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pi-web-light",
|
id: "pi-web-light",
|
||||||
name: "Pi Web Light",
|
name: "PI WEB Light",
|
||||||
description: "Light Pi Web palette.",
|
description: "Light PI WEB palette.",
|
||||||
order: 20,
|
order: 20,
|
||||||
colorScheme: "light",
|
colorScheme: "light",
|
||||||
tokens: piWebLightTokens,
|
tokens: piWebLightTokens,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "classic",
|
id: "classic",
|
||||||
name: "Pi Web Classic",
|
name: "PI WEB Classic",
|
||||||
description: "The original Pi Web dark palette.",
|
description: "The original PI WEB dark palette.",
|
||||||
order: 30,
|
order: 30,
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
tokens: classicTokens,
|
tokens: classicTokens,
|
||||||
@@ -148,8 +148,8 @@ export const themePackPlugin: PiWebPlugin = {
|
|||||||
themePairs: [
|
themePairs: [
|
||||||
{
|
{
|
||||||
id: "pi-web",
|
id: "pi-web",
|
||||||
name: "Pi Web",
|
name: "PI WEB",
|
||||||
description: "Follow the system light/dark preference with Pi Web themes.",
|
description: "Follow the system light/dark preference with PI WEB themes.",
|
||||||
order: 10,
|
order: 10,
|
||||||
light: "pi-web-light",
|
light: "pi-web-light",
|
||||||
dark: "pi-web-dark",
|
dark: "pi-web-dark",
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ const tokens = {
|
|||||||
} satisfies ThemeTokens;
|
} satisfies ThemeTokens;
|
||||||
|
|
||||||
const themes = [
|
const themes = [
|
||||||
theme("pi-web-dark", "Pi Web Dark", "dark"),
|
theme("pi-web-dark", "PI WEB Dark", "dark"),
|
||||||
theme("pi-web-light", "Pi Web Light", "light"),
|
theme("pi-web-light", "PI WEB Light", "light"),
|
||||||
theme("classic", "Pi Web Classic", "dark"),
|
theme("classic", "PI WEB Classic", "dark"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const themePairs: QualifiedThemePairContribution[] = [
|
const themePairs: QualifiedThemePairContribution[] = [
|
||||||
@@ -51,7 +51,7 @@ const themePairs: QualifiedThemePairContribution[] = [
|
|||||||
id: "themes:pi-web",
|
id: "themes:pi-web",
|
||||||
pluginId: "themes",
|
pluginId: "themes",
|
||||||
localId: "pi-web",
|
localId: "pi-web",
|
||||||
name: "Pi Web",
|
name: "PI WEB",
|
||||||
light: "themes:pi-web-light",
|
light: "themes:pi-web-light",
|
||||||
dark: "themes:pi-web-dark",
|
dark: "themes:pi-web-dark",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function loadPiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig {
|
|||||||
if (!existsSync(path)) return { path, exists: false, config: {} };
|
if (!existsSync(path)) return { path, exists: false, config: {} };
|
||||||
|
|
||||||
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||||
if (!isRecord(parsed)) throw new Error(`Pi Web config must be a JSON object: ${path}`);
|
if (!isRecord(parsed)) throw new Error(`PI WEB config must be a JSON object: ${path}`);
|
||||||
|
|
||||||
return { path, exists: true, config: parsePiWebConfig(parsed, path) };
|
return { path, exists: true, config: parsePiWebConfig(parsed, path) };
|
||||||
}
|
}
|
||||||
@@ -78,20 +78,20 @@ function parsePiWebConfig(value: Record<string, unknown>, path: string): PiWebCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseString(value: unknown, key: string, path: string): string {
|
function parseString(value: unknown, key: string, path: string): string {
|
||||||
if (typeof value !== "string" || value === "") throw new Error(`Pi Web config ${key} must be a non-empty string: ${path}`);
|
if (typeof value !== "string" || value === "") throw new Error(`PI WEB config ${key} must be a non-empty string: ${path}`);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePort(value: unknown, key: string, path = "environment"): number {
|
function parsePort(value: unknown, key: string, path = "environment"): number {
|
||||||
const port = typeof value === "number" ? value : typeof value === "string" && value !== "" ? Number(value) : NaN;
|
const port = typeof value === "number" ? value : typeof value === "string" && value !== "" ? Number(value) : NaN;
|
||||||
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Pi Web config ${key} must be an integer from 1 to 65535: ${path}`);
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`PI WEB config ${key} must be an integer from 1 to 65535: ${path}`);
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAllowedHosts(value: unknown, path: string): string[] | true {
|
function parseAllowedHosts(value: unknown, path: string): string[] | true {
|
||||||
if (value === true) return true;
|
if (value === true) return true;
|
||||||
if (!isNonEmptyStringArray(value)) {
|
if (!isNonEmptyStringArray(value)) {
|
||||||
throw new Error(`Pi Web config allowedHosts must be true or an array of non-empty strings: ${path}`);
|
throw new Error(`PI WEB config allowedHosts must be true or an array of non-empty strings: ${path}`);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe("buildApp", () => {
|
|||||||
expect(emptyListResponse.json<Project[]>()).toEqual([]);
|
expect(emptyListResponse.json<Project[]>()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves the Pi Web plugin manifest and plugin assets", async () => {
|
it("serves the PI WEB plugin manifest and plugin assets", async () => {
|
||||||
const manifestResponse = await app.inject({ method: "GET", url: "/pi-web-plugins/manifest.json" });
|
const manifestResponse = await app.inject({ method: "GET", url: "/pi-web-plugins/manifest.json" });
|
||||||
expect(manifestResponse.statusCode).toBe(200);
|
expect(manifestResponse.statusCode).toBe(200);
|
||||||
expect(manifestResponse.json()).toEqual({ plugins: [{ id: "fake", module: "/pi-web-plugins/fake/plugin.js?v=1", source: "test", scope: "local" }] });
|
expect(manifestResponse.json()).toEqual({ plugins: [{ id: "fake", module: "/pi-web-plugins/fake/plugin.js?v=1", source: "test", scope: "local" }] });
|
||||||
|
|||||||
@@ -203,10 +203,10 @@ async function discoverPackageRoot(root: string, configuredPackage: ConfiguredPi
|
|||||||
async function discoverPluginEntries(root: string, config: PiWebPackageConfig): Promise<ArraylessPluginRecord[]> {
|
async function discoverPluginEntries(root: string, config: PiWebPackageConfig): Promise<ArraylessPluginRecord[]> {
|
||||||
const plugins: ArraylessPluginRecord[] = [];
|
const plugins: ArraylessPluginRecord[] = [];
|
||||||
for (const entry of config.plugins) {
|
for (const entry of config.plugins) {
|
||||||
if (!isSafeRelativePath(entry.module)) throw new Error(`Unsafe Pi Web plugin module path for ${entry.id}: ${entry.module}`);
|
if (!isSafeRelativePath(entry.module)) throw new Error(`Unsafe PI WEB plugin module path for ${entry.id}: ${entry.module}`);
|
||||||
const entryPath = join(root, entry.module);
|
const entryPath = join(root, entry.module);
|
||||||
const entryStat = await stat(entryPath).catch(() => undefined);
|
const entryStat = await stat(entryPath).catch(() => undefined);
|
||||||
if (entryStat?.isFile() !== true) throw new Error(`Pi Web plugin module not found for ${entry.id}: ${entry.module}`);
|
if (entryStat?.isFile() !== true) throw new Error(`PI WEB plugin module not found for ${entry.id}: ${entry.module}`);
|
||||||
plugins.push({ id: entry.id, root, entryFile: entry.module, version: String(Math.floor(entryStat.mtimeMs)) });
|
plugins.push({ id: entry.id, root, entryFile: entry.module, version: String(Math.floor(entryStat.mtimeMs)) });
|
||||||
}
|
}
|
||||||
return plugins;
|
return plugins;
|
||||||
@@ -227,24 +227,24 @@ async function readPiWebPackageConfig(root: string): Promise<PiWebPackageConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parsePluginEntries(piWeb: Record<string, unknown>, packagePath: string): PiWebPluginEntry[] {
|
function parsePluginEntries(piWeb: Record<string, unknown>, packagePath: string): PiWebPluginEntry[] {
|
||||||
if (piWeb["plugin"] !== undefined) throw new Error(`Unsupported Pi Web plugin metadata in ${packagePath}: use piWeb.plugins with { id, module } entries`);
|
if (piWeb["plugin"] !== undefined) throw new Error(`Unsupported PI WEB plugin metadata in ${packagePath}: use piWeb.plugins with { id, module } entries`);
|
||||||
const plugins = piWeb["plugins"];
|
const plugins = piWeb["plugins"];
|
||||||
if (plugins === undefined) return [];
|
if (plugins === undefined) return [];
|
||||||
if (!Array.isArray(plugins)) throw new Error(`Pi Web plugins must be an array in ${packagePath}`);
|
if (!Array.isArray(plugins)) throw new Error(`PI WEB plugins must be an array in ${packagePath}`);
|
||||||
|
|
||||||
return plugins.map((entry, index): PiWebPluginEntry => {
|
return plugins.map((entry, index): PiWebPluginEntry => {
|
||||||
if (!isRecord(entry)) throw new Error(`Pi Web plugin entry ${String(index + 1)} must be an object in ${packagePath}`);
|
if (!isRecord(entry)) throw new Error(`PI WEB plugin entry ${String(index + 1)} must be an object in ${packagePath}`);
|
||||||
const id = entry["id"];
|
const id = entry["id"];
|
||||||
const module = entry["module"];
|
const module = entry["module"];
|
||||||
if (typeof id !== "string" || !pluginIdPattern.test(id)) throw new Error(`Invalid Pi Web plugin id in ${packagePath}: ${String(id)}`);
|
if (typeof id !== "string" || !pluginIdPattern.test(id)) throw new Error(`Invalid PI WEB plugin id in ${packagePath}: ${String(id)}`);
|
||||||
if (typeof module !== "string" || module === "") throw new Error(`Invalid Pi Web plugin module for ${id} in ${packagePath}`);
|
if (typeof module !== "string" || module === "") throw new Error(`Invalid PI WEB plugin module for ${id} in ${packagePath}`);
|
||||||
return { id, module };
|
return { id, module };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUnique(records: Map<string, PluginRecord>, plugin: PluginRecord): void {
|
function addUnique(records: Map<string, PluginRecord>, plugin: PluginRecord): void {
|
||||||
if (records.has(plugin.id)) {
|
if (records.has(plugin.id)) {
|
||||||
warnInvalidPlugin(plugin.source, `Duplicate Pi Web plugin id: ${plugin.id}`);
|
warnInvalidPlugin(plugin.source, `Duplicate PI WEB plugin id: ${plugin.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
records.set(plugin.id, plugin);
|
records.set(plugin.id, plugin);
|
||||||
@@ -252,7 +252,7 @@ function addUnique(records: Map<string, PluginRecord>, plugin: PluginRecord): vo
|
|||||||
|
|
||||||
function warnInvalidPlugin(source: string, error: unknown): void {
|
function warnInvalidPlugin(source: string, error: unknown): void {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
console.warn(`Skipping Pi Web plugin from ${source}: ${message}`);
|
console.warn(`Skipping PI WEB plugin from ${source}: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeRelativePath(path: string): boolean {
|
function isSafeRelativePath(path: string): boolean {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Pi Web status", () => {
|
describe("PI WEB status", () => {
|
||||||
it("compares semver-shaped CalVer versions", () => {
|
it("compares semver-shaped CalVer versions", () => {
|
||||||
expect(comparePackageVersions("1.202605.9", "1.202605.8")).toBeGreaterThan(0);
|
expect(comparePackageVersions("1.202605.9", "1.202605.8")).toBeGreaterThan(0);
|
||||||
expect(comparePackageVersions("1.202605.8", "1.202605.8")).toBe(0);
|
expect(comparePackageVersions("1.202605.8", "1.202605.8")).toBe(0);
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ function buildMessages(components: PiWebStatusResponse["components"], release: P
|
|||||||
messages.push({
|
messages.push({
|
||||||
id: "update-available",
|
id: "update-available",
|
||||||
severity: "info",
|
severity: "info",
|
||||||
title: "Pi Web update available",
|
title: "PI WEB update available",
|
||||||
body: `Pi Web ${release.latestVersion} is available${installedVersion === undefined ? "" : `; installed version is ${installedVersion}`}. Update Pi Web, then restart Pi Web services.`,
|
body: `PI WEB ${release.latestVersion} is available${installedVersion === undefined ? "" : `; installed version is ${installedVersion}`}. Update PI WEB, then restart PI WEB services.`,
|
||||||
command: commands.update,
|
command: commands.update,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,7 @@ function buildMessages(components: PiWebStatusResponse["components"], release: P
|
|||||||
id: "sessiond-unavailable",
|
id: "sessiond-unavailable",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
title: "Session daemon version unavailable",
|
title: "Session daemon version unavailable",
|
||||||
body: `Pi Web could not check the session daemon version${components.sessiond.error === undefined ? "." : `: ${components.sessiond.error}`}`,
|
body: `PI WEB could not check the session daemon version${components.sessiond.error === undefined ? "." : `: ${components.sessiond.error}`}`,
|
||||||
command: "systemctl --user status pi-web-sessiond.service",
|
command: "systemctl --user status pi-web-sessiond.service",
|
||||||
});
|
});
|
||||||
} else if (components.sessiond.stale) {
|
} else if (components.sessiond.stale) {
|
||||||
|
|||||||