@timqi/pier 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/credentials.js +5 -2
- package/dist/agent/pi.js +1 -1
- package/dist/boards/pier.css +58 -39
- package/dist/channels/handoff.js +1 -1
- package/dist/cli.js +8 -0
- package/dist/db.js +13 -0
- package/dist/main.js +7 -2
- package/dist/secrets.js +15 -1
- package/dist/settings.js +28 -0
- package/dist/socket.js +13 -1
- package/dist/web/auth.js +152 -29
- package/dist/web/instance.js +19 -4
- package/dist/web/passkeys.js +438 -0
- package/dist/web/public/assets/activity-CImcJ4xF.js +5 -0
- package/dist/web/public/assets/activity-CImcJ4xF.js.br +0 -0
- package/dist/web/public/assets/activity-CImcJ4xF.js.gz +0 -0
- package/dist/web/public/assets/boards-43luxbPd.js +1 -0
- package/dist/web/public/assets/boards-43luxbPd.js.br +0 -0
- package/dist/web/public/assets/boards-43luxbPd.js.gz +0 -0
- package/dist/web/public/assets/explorer-uSHcxQMy.js +4 -0
- package/dist/web/public/assets/explorer-uSHcxQMy.js.br +0 -0
- package/dist/web/public/assets/explorer-uSHcxQMy.js.gz +0 -0
- package/dist/web/public/assets/{index-T-vLXPch.js → index-Dje7tHE4.js} +15 -15
- package/dist/web/public/assets/index-Dje7tHE4.js.br +0 -0
- package/dist/web/public/assets/index-Dje7tHE4.js.gz +0 -0
- package/dist/web/public/assets/index-sQWFDUPu.css +2 -0
- package/dist/web/public/assets/index-sQWFDUPu.css.br +0 -0
- package/dist/web/public/assets/index-sQWFDUPu.css.gz +0 -0
- package/dist/web/public/assets/runs-rggAaD0k.js +1 -0
- package/dist/web/public/assets/runs-rggAaD0k.js.br +0 -0
- package/dist/web/public/assets/runs-rggAaD0k.js.gz +0 -0
- package/dist/web/public/assets/settings-DN20Mj9n.js +5 -0
- package/dist/web/public/assets/settings-DN20Mj9n.js.br +0 -0
- package/dist/web/public/assets/settings-DN20Mj9n.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BF79aZgL.js +3 -0
- package/dist/web/public/assets/task-runs-BF79aZgL.js.br +0 -0
- package/dist/web/public/assets/task-runs-BF79aZgL.js.gz +0 -0
- package/dist/web/public/assets/tasks-MCHgTBcD.js +4 -0
- package/dist/web/public/assets/tasks-MCHgTBcD.js.br +0 -0
- package/dist/web/public/assets/tasks-MCHgTBcD.js.gz +0 -0
- package/dist/web/public/icon-32-amber.png +0 -0
- package/dist/web/public/icon-32-emerald.png +0 -0
- package/dist/web/public/icon-32-rose.png +0 -0
- package/dist/web/public/icon-32-teal.png +0 -0
- package/dist/web/public/icon-32-violet.png +0 -0
- package/dist/web/public/icon-32.png +0 -0
- package/dist/web/public/icon-maskable-512.png +0 -0
- package/dist/web/public/icon-touch-192-amber.png +0 -0
- package/dist/web/public/icon-touch-192-emerald.png +0 -0
- package/dist/web/public/icon-touch-192-rose.png +0 -0
- package/dist/web/public/icon-touch-192-teal.png +0 -0
- package/dist/web/public/icon-touch-192-violet.png +0 -0
- package/dist/web/public/icon-touch-192.png +0 -0
- package/dist/web/public/icon.svg +3 -1
- package/dist/web/public/index.html +14 -10
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +10 -10
- package/dist/web/public/sw.js +5 -5
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +15 -3
- package/dist/web/server.js +70 -11
- package/docs/deploy.md +19 -2
- package/package.json +1 -1
- package/dist/web/public/assets/activity-BWpiRicQ.js +0 -5
- package/dist/web/public/assets/activity-BWpiRicQ.js.br +0 -0
- package/dist/web/public/assets/activity-BWpiRicQ.js.gz +0 -0
- package/dist/web/public/assets/boards-BnRMjBln.js +0 -1
- package/dist/web/public/assets/boards-BnRMjBln.js.br +0 -0
- package/dist/web/public/assets/boards-BnRMjBln.js.gz +0 -0
- package/dist/web/public/assets/explorer-DLZ0ccgE.js +0 -4
- package/dist/web/public/assets/explorer-DLZ0ccgE.js.br +0 -0
- package/dist/web/public/assets/explorer-DLZ0ccgE.js.gz +0 -0
- package/dist/web/public/assets/index-DMq4awU8.css +0 -2
- package/dist/web/public/assets/index-DMq4awU8.css.br +0 -0
- package/dist/web/public/assets/index-DMq4awU8.css.gz +0 -0
- package/dist/web/public/assets/index-T-vLXPch.js.br +0 -0
- package/dist/web/public/assets/index-T-vLXPch.js.gz +0 -0
- package/dist/web/public/assets/runs-oFQ162um.js +0 -1
- package/dist/web/public/assets/runs-oFQ162um.js.br +0 -0
- package/dist/web/public/assets/runs-oFQ162um.js.gz +0 -0
- package/dist/web/public/assets/settings-_t0Js-DJ.js +0 -5
- package/dist/web/public/assets/settings-_t0Js-DJ.js.br +0 -0
- package/dist/web/public/assets/settings-_t0Js-DJ.js.gz +0 -0
- package/dist/web/public/assets/task-runs-m3ufvOQI.js +0 -3
- package/dist/web/public/assets/task-runs-m3ufvOQI.js.br +0 -0
- package/dist/web/public/assets/task-runs-m3ufvOQI.js.gz +0 -0
- package/dist/web/public/assets/tasks-C1ATvPUc.js +0 -4
- package/dist/web/public/assets/tasks-C1ATvPUc.js.br +0 -0
- package/dist/web/public/assets/tasks-C1ATvPUc.js.gz +0 -0
- package/dist/web/public/icon.svg.br +0 -0
- package/dist/web/public/icon.svg.gz +0 -0
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
|
@@ -23,22 +23,23 @@
|
|
|
23
23
|
</script>
|
|
24
24
|
<!-- Installed as a home-screen app: standalone window, own icon. The
|
|
25
25
|
manifest shares the workbench's cookie boundary. -->
|
|
26
|
-
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
26
|
+
<link rel="manifest" href="/app/manifest.webmanifest" crossorigin="use-credentials" />
|
|
27
27
|
<!-- SVG first (scales to any tab/bookmark size), 32px PNG for the engines
|
|
28
28
|
that still ignore it. iOS wants an opaque square and rounds it itself. -->
|
|
29
|
-
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
30
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32.png" />
|
|
31
|
-
<link rel="apple-touch-icon" href="/icon-touch-192.png" />
|
|
29
|
+
<link rel="icon" type="image/svg+xml" href="/app/icon.svg" />
|
|
30
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/app/icon-32.png" />
|
|
31
|
+
<link rel="apple-touch-icon" href="/app/icon-touch-192.png" />
|
|
32
32
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
33
33
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
34
|
-
|
|
34
|
+
<!-- No apple-mobile-web-app-title: it would override the manifest's
|
|
35
|
+
short_name, which is where the instance's name is (web/server.ts). -->
|
|
35
36
|
<!-- Without this iOS paints the status-bar strip in a system grey that
|
|
36
37
|
belongs to no part of the app. Translucent hands the strip to the page,
|
|
37
38
|
which style.css pays for with the safe-area inset. -->
|
|
38
39
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
39
40
|
<title>Pier</title>
|
|
40
|
-
<script type="module" crossorigin src="/assets/index-
|
|
41
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
41
|
+
<script type="module" crossorigin src="/app/assets/index-Dje7tHE4.js"></script>
|
|
42
|
+
<link rel="stylesheet" crossorigin href="/app/assets/index-sQWFDUPu.css">
|
|
42
43
|
</head>
|
|
43
44
|
<!-- The document never scrolls: this is a fixed-viewport workbench, and every
|
|
44
45
|
scrollable region is an inner pane. h-dvh, not h-screen, because 100vh can
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
<header class="flex h-12 flex-none items-center gap-2 px-3">
|
|
61
62
|
<!-- The same mark as the tab and the home-screen icon, so an installed
|
|
62
63
|
Pier looks like the thing you installed. -->
|
|
63
|
-
<img src="/icon.svg" alt="" width="20" height="20" class="h-7 w-7 flex-none rounded-lg border border-neutral-200/70 bg-white p-1 shadow-xs" />
|
|
64
|
+
<img src="/app/icon.svg" alt="" width="20" height="20" class="h-7 w-7 flex-none rounded-lg border border-neutral-200/70 bg-white p-1 shadow-xs" />
|
|
64
65
|
<h1 class="text-[17px] font-semibold tracking-tight">Pier</h1>
|
|
65
66
|
<!-- A link, not a label: AGPL-3.0 section 13 asks a network service to
|
|
66
67
|
point its users at the source it is running. Beside the title
|
|
@@ -253,8 +254,11 @@
|
|
|
253
254
|
<button type="button" id="image-prev" aria-label="Previous image" class="lightbox-btn left-2 top-1/2 -translate-y-1/2"><span data-icon="ChevronLeft" class="h-6 w-6"></span></button>
|
|
254
255
|
<button type="button" id="image-next" aria-label="Next image" class="lightbox-btn right-2 top-1/2 -translate-y-1/2"><span data-icon="ChevronRight" class="h-6 w-6"></span></button>
|
|
255
256
|
<!-- The way out on a touch screen: tapping the image now zooms it, and the
|
|
256
|
-
scrim beside a fitted shot is a 4vw strip — not a thumb's target.
|
|
257
|
-
|
|
257
|
+
scrim beside a fitted shot is a 4vw strip — not a thumb's target. The
|
|
258
|
+
inset is its own: the dialog sits outside `main`, so style.css's rule
|
|
259
|
+
never reaches it and `top-2` alone is under iOS's status bar, where
|
|
260
|
+
the tap belongs to the system. -->
|
|
261
|
+
<button type="button" id="image-close" aria-label="Close" class="lightbox-btn right-2 top-[calc(0.5rem+env(safe-area-inset-top))]"><span data-icon="X" class="h-6 w-6"></span></button>
|
|
258
262
|
</dialog>
|
|
259
263
|
|
|
260
264
|
<!-- Text preview of an agent attachment — the Files view's code pane fills
|
|
Binary file
|
|
Binary file
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "/",
|
|
2
|
+
"id": "/app/",
|
|
3
3
|
"name": "Pier",
|
|
4
4
|
"short_name": "Pier",
|
|
5
5
|
"description": "A self-hosted workspace for coding agents: sessions, tasks and files, in one workbench.",
|
|
6
6
|
"lang": "en",
|
|
7
7
|
"dir": "ltr",
|
|
8
|
-
"start_url": "/",
|
|
9
|
-
"scope": "/",
|
|
8
|
+
"start_url": "/app/",
|
|
9
|
+
"scope": "/app/",
|
|
10
10
|
"display": "standalone",
|
|
11
11
|
"background_color": "#f1f4f3",
|
|
12
12
|
"theme_color": "#f1f4f3",
|
|
13
13
|
"categories": ["developer", "productivity", "utilities"],
|
|
14
14
|
"launch_handler": { "client_mode": "focus-existing" },
|
|
15
15
|
"icons": [
|
|
16
|
-
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" },
|
|
17
|
-
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
18
|
-
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
19
|
-
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
16
|
+
{ "src": "/app/icon.svg", "sizes": "any", "type": "image/svg+xml" },
|
|
17
|
+
{ "src": "/app/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
18
|
+
{ "src": "/app/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
19
|
+
{ "src": "/app/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
20
20
|
],
|
|
21
21
|
"shortcuts": [
|
|
22
|
-
{ "name": "Tasks", "url": "/#/tasks", "icons": [{ "src": "/icon-192.png", "sizes": "192x192" }] },
|
|
23
|
-
{ "name": "Activity", "url": "/#/activity", "icons": [{ "src": "/icon-192.png", "sizes": "192x192" }] },
|
|
24
|
-
{ "name": "Settings", "url": "/#/settings", "icons": [{ "src": "/icon-192.png", "sizes": "192x192" }] }
|
|
22
|
+
{ "name": "Tasks", "url": "/app/#/tasks", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] },
|
|
23
|
+
{ "name": "Activity", "url": "/app/#/activity", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] },
|
|
24
|
+
{ "name": "Settings", "url": "/app/#/settings", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] }
|
|
25
25
|
]
|
|
26
26
|
}
|
package/dist/web/public/sw.js
CHANGED
|
@@ -19,11 +19,11 @@ button{margin-top:1rem;padding:.5rem 1rem;border:1px solid light-dark(#d4d4d4,#3
|
|
|
19
19
|
<button onclick="location.reload()">Try again</button></main></body></html>`;
|
|
20
20
|
|
|
21
21
|
/** A payload URL is data: resolved against this origin, and replaced by the
|
|
22
|
-
*
|
|
22
|
+
* workbench if it lands anywhere else — a notification may not open a foreign page. */
|
|
23
23
|
const here = (url) => {
|
|
24
|
-
const root = new URL("/", self.location.origin);
|
|
24
|
+
const root = new URL("/app/", self.location.origin);
|
|
25
25
|
try {
|
|
26
|
-
const target = new URL(url || "/", self.location.origin);
|
|
26
|
+
const target = new URL(url || "/app/", self.location.origin);
|
|
27
27
|
return target.origin === root.origin ? target : root;
|
|
28
28
|
} catch {
|
|
29
29
|
return root;
|
|
@@ -75,8 +75,8 @@ self.addEventListener("push", (event) => {
|
|
|
75
75
|
// second turn silently replaces the first — no sound, no re-alert, which
|
|
76
76
|
// reads as "notifications stopped working". Ignored where unsupported.
|
|
77
77
|
renotify: true,
|
|
78
|
-
icon: "/icon-192.png",
|
|
79
|
-
badge: "/icon-32.png",
|
|
78
|
+
icon: "/app/icon-192.png",
|
|
79
|
+
badge: "/app/icon-32.png",
|
|
80
80
|
timestamp: Date.now(),
|
|
81
81
|
data: { url: here(data.url).href },
|
|
82
82
|
});
|
package/dist/web/public/sw.js.br
CHANGED
|
Binary file
|
package/dist/web/public/sw.js.gz
CHANGED
|
Binary file
|
package/dist/web/push.js
CHANGED
|
@@ -59,7 +59,7 @@ export class PushStore {
|
|
|
59
59
|
}
|
|
60
60
|
list() {
|
|
61
61
|
return this.#db
|
|
62
|
-
.prepare(`SELECT endpoint, p256dh, auth, label, created_at AS createdAt
|
|
62
|
+
.prepare(`SELECT endpoint, p256dh, auth, label, created_at AS createdAt, session_id AS sessionId
|
|
63
63
|
FROM push_subscriptions ORDER BY created_at DESC`)
|
|
64
64
|
.all();
|
|
65
65
|
}
|
|
@@ -190,7 +190,7 @@ export function registerPushRoutes(app, deps) {
|
|
|
190
190
|
await deliver({
|
|
191
191
|
title: sessionLabel(await summary(e.sessionId)),
|
|
192
192
|
body: preview(text) || "Turn finished.",
|
|
193
|
-
url:
|
|
193
|
+
url: `/app/#/session/${encodeURIComponent(e.sessionId)}`,
|
|
194
194
|
tag: e.sessionId,
|
|
195
195
|
});
|
|
196
196
|
})().catch((err) => log.error(`delivering a push for ${e.sessionId} failed`, err));
|
|
@@ -200,6 +200,18 @@ export function registerPushRoutes(app, deps) {
|
|
|
200
200
|
});
|
|
201
201
|
// --- routes ---------------------------------------------------------------------
|
|
202
202
|
app.get("/api/push", (c) => c.json({ publicKey: store.identity().publicKey }));
|
|
203
|
+
// Never the keys: a row with them is a sender, not a device.
|
|
204
|
+
app.get("/api/push/subscriptions", (c) => {
|
|
205
|
+
const me = sessionIdOf(c);
|
|
206
|
+
return c.json({
|
|
207
|
+
devices: store.list().map(({ endpoint, label, createdAt, sessionId }) => ({
|
|
208
|
+
endpoint,
|
|
209
|
+
label,
|
|
210
|
+
createdAt,
|
|
211
|
+
current: sessionId === me,
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
203
215
|
app.post("/api/push/subscribe", async (c) => {
|
|
204
216
|
const body = await c.req.json().catch(() => null);
|
|
205
217
|
const target = parseTarget(body);
|
|
@@ -233,7 +245,7 @@ export function registerPushRoutes(app, deps) {
|
|
|
233
245
|
const { sent, failed } = await deliver({
|
|
234
246
|
title: "Pier",
|
|
235
247
|
body: "Notifications are working.",
|
|
236
|
-
url: "/",
|
|
248
|
+
url: "/app/",
|
|
237
249
|
tag: "pier-test",
|
|
238
250
|
});
|
|
239
251
|
if (!sent && !failed)
|
package/dist/web/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isThinkingLevel, SESSION_TITLE_MAX } from "../core/types.js";
|
|
|
21
21
|
import { saveInbound } from "../core/inbox.js";
|
|
22
22
|
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
23
23
|
import {} from "./session-state.js";
|
|
24
|
+
import { ACCENTS, DEFAULT_ACCENT } from "../settings.js";
|
|
24
25
|
import { registerInstanceRoutes } from "./instance.js";
|
|
25
26
|
import { registerProviderRoutes } from "./providers.js";
|
|
26
27
|
const log = logger("web");
|
|
@@ -46,6 +47,34 @@ export const tabPrefix = (title, host) => [title?.trim(), host.trim()].filter(Bo
|
|
|
46
47
|
export const withTabPrefix = (html, prefix) => prefix
|
|
47
48
|
? html.replace("<title>Pier</title>", `<title>${prefix.replace(/&/g, "&").replace(/</g, "<")} - Pier</title>`)
|
|
48
49
|
: html;
|
|
50
|
+
/** The accent rides on `<html>` so the first paint is already in it; the
|
|
51
|
+
* stylesheet's `[data-accent]` ramps do the rest (style.css). Safari and the
|
|
52
|
+
* iOS Home Screen take the PNG links, never the SVG or the manifest, so those
|
|
53
|
+
* point at the preset's pre-rendered copy (`icon-32-<accent>.png`,
|
|
54
|
+
* `icon-touch-192-<accent>.png`, beside the defaults in public/). A preset
|
|
55
|
+
* name, already validated — never a colour. */
|
|
56
|
+
export const withAccent = (html, accent) => accent
|
|
57
|
+
? html
|
|
58
|
+
.replace('<html lang="en">', `<html lang="en" data-accent="${accent}">`)
|
|
59
|
+
.replace('/app/icon-32.png', `/app/icon-32-${accent}.png`)
|
|
60
|
+
.replace('/app/icon-touch-192.png', `/app/icon-touch-192-${accent}.png`)
|
|
61
|
+
: html;
|
|
62
|
+
/** Two Piers on one phone need two names and two colours: the manifest names
|
|
63
|
+
* the instance (`$PIER_TITLE`) and paints its chrome with the accent's 600
|
|
64
|
+
* step, the same one the icon's plate takes. */
|
|
65
|
+
export const instanceManifest = (template, title, accent) => {
|
|
66
|
+
const name = title?.trim() || "Pier";
|
|
67
|
+
return {
|
|
68
|
+
...template,
|
|
69
|
+
name,
|
|
70
|
+
short_name: name.slice(0, 12),
|
|
71
|
+
theme_color: ACCENTS[accent || DEFAULT_ACCENT],
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** The plate colour is the one thing the served SVG changes; the mark stays
|
|
75
|
+
* white. The PNG fallbacks beside it are static and keep the shipped blue. */
|
|
76
|
+
export const ICON_PLATE = "#4f46e5";
|
|
77
|
+
export const withAccentIcon = (svg, accent) => svg.replace(`fill="${ICON_PLATE}"`, `fill="${ACCENTS[accent || DEFAULT_ACCENT]}"`);
|
|
49
78
|
const HEARTBEAT_MS = 15_000;
|
|
50
79
|
/** Past this much frame text in flight a stream is dropped. */
|
|
51
80
|
const SSE_HIGH_WATER = 4 * 1024 * 1024;
|
|
@@ -72,7 +101,7 @@ function boundedWriter(stream, who) {
|
|
|
72
101
|
}
|
|
73
102
|
// Canonical base64 only: Buffer.from(.., "base64") happily "decodes" garbage.
|
|
74
103
|
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
75
|
-
export function createServer({ factory, router, hub, sessions: state, config, packages, providers, settings, catalog, names, onToolsChanged, validateCustomTools, secrets, onUnlocked, reload, updates, updater, backgroundRuns, activeBackgroundRunCounts, taskSessions, channelOf, }) {
|
|
104
|
+
export function createServer({ factory, router, hub, sessions: state, config, packages, providers, settings, catalog, names, onToolsChanged, validateCustomTools, secrets, passkeys, onUnlocked, reload, updates, updater, backgroundRuns, activeBackgroundRunCounts, taskSessions, channelOf, }) {
|
|
76
105
|
const app = new Hono();
|
|
77
106
|
const epoch = randomUUID();
|
|
78
107
|
// One frame shared by synchronous fan-out to every watching tab.
|
|
@@ -506,6 +535,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
506
535
|
validateCustomTools,
|
|
507
536
|
onUnlocked,
|
|
508
537
|
onSettingsChanged: () => recycle("instance settings"),
|
|
538
|
+
passkeys,
|
|
509
539
|
});
|
|
510
540
|
registerProviderRoutes(app, providers, () => recycle("provider configuration"));
|
|
511
541
|
registerConfigRoutes(app, { factory, config, onConfigWritten: () => recycle("an agent file") });
|
|
@@ -515,15 +545,16 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
515
545
|
// serveStatic resolves `root` against the working directory, and an installed
|
|
516
546
|
// Pier is started from wherever the operator happens to be.
|
|
517
547
|
const bundle = fileURLToPath(new URL("./public", import.meta.url));
|
|
548
|
+
// The workbench lives under /app/ (vite.config.ts `base`): a manifest scope
|
|
549
|
+
// is a path prefix with no exclusions, so at `/` an installed Pier would
|
|
550
|
+
// capture the Show pages at /boards/* and /p/*.
|
|
551
|
+
app.get("/", (c) => c.redirect("/app/"));
|
|
518
552
|
// The tab says which instance this is: mistaking staging for production is
|
|
519
553
|
// the mistake worth a few lines. Behind the auth guard, so a stranger at
|
|
520
554
|
// /login learns neither fact.
|
|
521
555
|
const prefix = tabPrefix(process.env.PIER_TITLE, hostname().split(".")[0] ?? "");
|
|
522
556
|
let shell = null;
|
|
523
|
-
|
|
524
|
-
// siblings below cannot cover it. Exact path: never the SSE streams.
|
|
525
|
-
app.use("/", compress());
|
|
526
|
-
app.get("/", async (c, next) => {
|
|
557
|
+
const serveShell = async (c, next) => {
|
|
527
558
|
// A cached index must not name bundles a release has replaced.
|
|
528
559
|
c.header("cache-control", "private, no-cache");
|
|
529
560
|
if (shell === null) {
|
|
@@ -536,27 +567,55 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
536
567
|
return next();
|
|
537
568
|
}
|
|
538
569
|
}
|
|
539
|
-
|
|
540
|
-
|
|
570
|
+
// Per request, not cached: the accent is a setting.
|
|
571
|
+
return c.html(withAccent(shell, settings.get().accent));
|
|
572
|
+
};
|
|
573
|
+
// Answers from the patched string, not from disk, so the precompressed
|
|
574
|
+
// siblings below cannot cover it. Exact paths: never the SSE streams.
|
|
575
|
+
for (const path of ["/app", "/app/"]) {
|
|
576
|
+
app.use(path, compress());
|
|
577
|
+
app.get(path, serveShell);
|
|
578
|
+
}
|
|
579
|
+
// The two install assets that carry the instance's identity, rendered from
|
|
580
|
+
// the shipped files; `no-cache` so a changed accent shows on the next load.
|
|
581
|
+
// Unreadable → the static copy below, and the log says so.
|
|
582
|
+
const identity = (file, render, type) => async (c, next) => {
|
|
583
|
+
let text;
|
|
584
|
+
try {
|
|
585
|
+
text = await readFile(join(bundle, file), "utf8");
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
log.warn(`${file} unreadable, serving it unpatched: ${String(err)}`);
|
|
589
|
+
return next();
|
|
590
|
+
}
|
|
591
|
+
c.header("cache-control", "private, no-cache");
|
|
592
|
+
return c.body(render(text, settings.get().accent), 200, { "content-type": type });
|
|
593
|
+
};
|
|
594
|
+
app.get("/app/manifest.webmanifest", identity("manifest.webmanifest", (text, accent) => JSON.stringify(instanceManifest(JSON.parse(text), process.env.PIER_TITLE, accent)), "application/manifest+json"));
|
|
595
|
+
app.get("/app/icon.svg", identity("icon.svg", withAccentIcon, "image/svg+xml"));
|
|
541
596
|
// The one unhashed asset: an installed app keeps its worker until the
|
|
542
597
|
// re-fetched script differs, so a cached copy is a fix that never ships.
|
|
543
|
-
app.get("/sw.js", async (c, next) => {
|
|
598
|
+
app.get("/app/sw.js", async (c, next) => {
|
|
544
599
|
c.header("cache-control", "private, no-cache");
|
|
545
600
|
await next();
|
|
546
601
|
});
|
|
547
602
|
// Hashed bundles never change under their name; without this the auth
|
|
548
603
|
// layer's bare `private` costs a revalidation round trip per bundle per open.
|
|
549
|
-
app.get("/assets/*", async (c, next) => {
|
|
604
|
+
app.get("/app/assets/*", async (c, next) => {
|
|
550
605
|
c.header("cache-control", "private, max-age=31536000, immutable");
|
|
551
606
|
await next();
|
|
552
607
|
});
|
|
553
608
|
// The build writes `.br`/`.gz` siblings (vite.config.ts). serveStatic sets
|
|
554
609
|
// Vary only when it selects one; identity must carry it too, or a cache can
|
|
555
610
|
// reuse that response for a later Brotli request.
|
|
556
|
-
app.use("/*", async (c, next) => {
|
|
611
|
+
app.use("/app/*", async (c, next) => {
|
|
557
612
|
await next();
|
|
558
613
|
c.header("Vary", "Accept-Encoding");
|
|
559
614
|
});
|
|
560
|
-
app.use("/*", serveStatic({
|
|
615
|
+
app.use("/app/*", serveStatic({
|
|
616
|
+
root: relative(process.cwd(), bundle) || ".",
|
|
617
|
+
rewriteRequestPath: (path) => path.replace(/^\/app/, ""),
|
|
618
|
+
precompressed: true,
|
|
619
|
+
}));
|
|
561
620
|
return app;
|
|
562
621
|
}
|
package/docs/deploy.md
CHANGED
|
@@ -96,7 +96,21 @@ pier restart
|
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
- Changing or recovering the password signs out every browser.
|
|
99
|
-
- One browser: Settings →
|
|
99
|
+
- One browser: Settings → Security → Signed-in devices.
|
|
100
|
+
- Passkeys (Settings → Security, needs an https public URL): while one is
|
|
101
|
+
registered the password is refused. A browser without your passkey signs in
|
|
102
|
+
through a one-time link (two minutes, single use) from a shell on the host:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
pier login
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Locked out of every passkey? Drop the rows; the table is read live, so no
|
|
109
|
+
restart:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
sqlite3 ~/.pier/db/pier.db 'DELETE FROM passkeys'
|
|
113
|
+
```
|
|
100
114
|
- A browser session expires 7 days after its last request, and 90 days after
|
|
101
115
|
it signed in however often it is used.
|
|
102
116
|
- However a session ends, its push subscription goes with it.
|
|
@@ -231,7 +245,10 @@ Loopback bind; reach it over a tunnel, not a wider bind:
|
|
|
231
245
|
|
|
232
246
|
- `ssh -L 3141:localhost:3141 server`
|
|
233
247
|
- Tailscale, or Cloudflare Tunnel — no open port, TLS terminates outside.
|
|
234
|
-
- A reverse proxy (Caddy, nginx)
|
|
248
|
+
- A reverse proxy (Caddy, nginx) **on the same host**, connecting over
|
|
249
|
+
loopback: forwarded headers are trusted from a loopback peer only, so a proxy
|
|
250
|
+
on another machine gets one throttle bucket for all its clients and a cookie
|
|
251
|
+
without `Secure`. Terminate TLS there, preserve the external
|
|
235
252
|
`Host` (or pass `X-Forwarded-Host`), and **set** `X-Forwarded-For` to the
|
|
236
253
|
client — nginx `proxy_set_header X-Forwarded-For $remote_addr;`, Caddy by
|
|
237
254
|
default. Pier reads the rightmost hop, so appending the peer is safe too
|
package/package.json
CHANGED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{C as e,D as t,K as n,N as r,Q as i,U as a,X as o,Y as s,q as c,rt as l,t as u,u as d,z as f}from"./index-T-vLXPch.js";var p=[[`path`,{d:`M3 5h.01`}],[`path`,{d:`M3 12h.01`}],[`path`,{d:`M3 19h.01`}],[`path`,{d:`M8 5h13`}],[`path`,{d:`M8 12h13`}],[`path`,{d:`M8 19h13`}]],m=[[`path`,{d:`m10.586 5.414-5.172 5.172`}],[`path`,{d:`m18.586 13.414-5.172 5.172`}],[`path`,{d:`M6 12h12`}],[`circle`,{cx:`12`,cy:`20`,r:`2`}],[`circle`,{cx:`12`,cy:`4`,r:`2`}],[`circle`,{cx:`20`,cy:`12`,r:`2`}],[`circle`,{cx:`4`,cy:`12`,r:`2`}]],h=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},g=e=>e===null?`-`:n(Date.now()-e),_={invocation:{stroke:`stroke-neutral-400`,fill:`fill-neutral-400`},callback:{stroke:`stroke-cyan-500`,fill:`fill-cyan-500`,dash:`6 5`},message:{stroke:`stroke-amber-500`,fill:`fill-amber-500`,dash:`2 5`}},v={scheduler:`border-violet-200 bg-violet-50 text-violet-700`,console:`border-indigo-200 bg-indigo-50 text-indigo-700`,task:`border-amber-200 bg-amber-50 text-amber-700`,process:`border-dashed border-neutral-300 bg-neutral-50 text-neutral-500`,streaming:`border-emerald-200 bg-white font-medium text-neutral-800`,idle:`border-neutral-200 bg-white font-medium text-neutral-600`},y=300,b=4;function x(e,t){let n,r=null,i=!1,a=!1,o=()=>{clearTimeout(n),n=setTimeout(()=>{i||a||!r||r.dataset.closing!==void 0||(d(),r=null)},120)};e.onpointerenter=s=>{s.pointerType!==`touch`&&(i=!0,clearTimeout(n),!(r?.isConnected&&r.dataset.closing===void 0)&&(n=setTimeout(()=>{!e.isConnected||!i||(r=u(e,t),r.onpointerenter=()=>{a=!0,clearTimeout(n)},r.onpointerleave=()=>{a=!1,o()})},y)))},e.onpointerleave=()=>{i=!1,o()},e.onpointerdown=()=>clearTimeout(n)}function S(e){let t,n=!1;e.classList.add(`cursor-grab`),e.onpointerdown=r=>{n=!1,!(r.button!==0||r.pointerType===`touch`||r.target.closest(`[data-node], path`))&&(t={x:r.clientX,y:r.clientY,left:e.scrollLeft,top:e.scrollTop},e.setPointerCapture(r.pointerId),e.classList.add(`cursor-grabbing`,`select-none`))},e.onpointermove=r=>{if(!t)return;let i=r.clientX-t.x,a=r.clientY-t.y;(Math.abs(i)>b||Math.abs(a)>b)&&(n=!0),e.scrollLeft=t.left-i,e.scrollTop=t.top-a};let r=()=>{t=void 0,e.classList.remove(`cursor-grabbing`,`select-none`)};e.onpointerup=r,e.onpointercancel=r,e.addEventListener(`click`,e=>{n&&(e.stopPropagation(),e.preventDefault())},!0)}function C(n,u,d){let y=`sessions`,b=`active`,C={sessions:[],runs:[],messages:[]},w=``,T=o(async()=>{let e=b,t=await i(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){w=``,n.replaceChildren(c(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==b)return;let r=JSON.stringify(t.value);if(`${e}:${r}`===w)return;w=`${e}:${r}`;let a=t.value;C={...a,sessions:a.sessions.map(e=>({...e,title:l(e.title)}))},E()});function E(){let e=f(r([[`Sessions`,`sessions`,p],[`Relationships`,`dependencies`,m]],y,e=>{y=e,E()}),c(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${C.sessions.length} sessions · ${C.runs.length} runs`),r([[`Current`,`active`],[`Last 24h`,`recent`]],b,e=>{b=e,E(),T()})),t=c(`div`,`automation-list min-h-0 flex-1 overflow-auto`);y===`sessions`?D(t):k(t),n.replaceChildren(e,t)}function D(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
|
|
2
|
-
<th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
|
|
3
|
-
<th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
|
|
4
|
-
<th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
|
|
5
|
-
<th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let r=document.createElement(`tbody`);for(let e of C.sessions){let t=document.createElement(`tr`);t.className=`cursor-pointer border-b border-neutral-100 transition-colors hover:bg-neutral-50`,t.onclick=()=>u(e.id);let n=c(`button`,`block w-full cursor-pointer truncate text-left font-medium text-neutral-800`,e.title??s(e.cwd));n.setAttribute(`type`,`button`),n.title=`${e.id}\n${e.cwd}`,n.onclick=t=>{t.stopPropagation(),u(e.id)},t.append(c(`td`,`px-4 py-2.5`,n,c(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),c(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] text-neutral-500 md:table-cell`,e.cwd||`-`),c(`td`,`px-2 py-2.5`,O(e.state)),c(`td`,`px-2 py-2.5 font-mono text-[11.5px] text-neutral-400`,g(e.stateSince))),r.append(t)}n.append(r),e.append(n),C.sessions.length||e.append(c(`div`,`p-4`,t(b===`active`?`No active sessions.`:`No sessions in the last 24 hours.`)))}function O(t){return t===`streaming`?e(t,`bg-emerald-50 text-emerald-700 ring-emerald-200`,`animate-pulse bg-emerald-500`):e(t,`bg-neutral-100 text-neutral-600 ring-neutral-200`,`bg-neutral-300`)}function k(e){let n=new Map,r=[],i=(e,t,r,i)=>{n.has(e)||n.set(e,{id:e,label:t,kind:r,state:i})};for(let e of C.sessions)i(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of C.runs){let t=e.invokedBySessionId??`scheduler`,n=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);i(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),i(n,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),r.push({from:t,to:n,run:e,kind:`invocation`}),e.callbackSessionId&&(i(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),r.push({from:n,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of C.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=C.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])i(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);r.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!n.size){e.append(c(`div`,`p-4`,t(b===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`)));return}let a=[...n.values()],o=new Set(r.map(e=>e.to)),s=new Map,l=a.filter(e=>!o.has(e.id)).map(e=>e.id);l.length||l.push(a[0].id);for(let e of l)s.set(e,0);for(;l.length;){let e=l.shift();for(let t of r)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),l.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let f=new Map;for(let e of a){let t=s.get(e.id);f.set(t,[...f.get(t)??[],e])}let p=Math.max(...f.keys())+1,m=Math.max(...[...f.values()].map(e=>e.length)),g=r.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,y=56+p*180+(p-1)*110+g,w=56+m*46+(m-1)*30,T=new Map;for(let[e,t]of f)t.forEach((t,n)=>T.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let E=c(`div`,`relative`);E.style.width=`${y}px`,E.style.height=`${w}px`;let D=h(`svg`,{width:String(y),height:String(w),viewBox:`0 0 ${y} ${w}`,class:`absolute inset-0`}),O=h(`defs`);for(let[e,t]of Object.entries(_)){let n=h(`marker`,{id:`activity-arrow-${e}`,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(h(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,class:t.fill})),O.append(n)}D.append(O);let k=new Set;for(let e of r){let t=`${e.from}→${e.to}:${e.kind}`;if(k.has(t))continue;k.add(t);let n=T.get(e.from),r=T.get(e.to),i=n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),a=_[e.kind],o=h(`path`,{d:i,fill:`none`,class:`cursor-pointer ${a.stroke}`,"stroke-width":`1.5`,"marker-end":`url(#activity-arrow-${e.kind})`});a.dash&&o.setAttribute(`stroke-dasharray`,a.dash),o.onclick=()=>d(e.run.id),D.append(o)}E.append(D);for(let e of a){let t=T.get(e.id),n=e.kind===`session`?e.state===`streaming`?v.streaming:v.idle:v[e.kind],r=c(e.kind===`session`?`button`:`div`,`absolute flex items-center gap-2 rounded-xl border px-3 text-left text-[11.5px] shadow-xs ${n} ${e.kind===`session`?`cursor-pointer hover:bg-neutral-50`:`cursor-default`}`,...e.kind===`session`?[c(`span`,`inline-block h-1.5 w-1.5 flex-none rounded-full ${e.state===`streaming`?`animate-pulse bg-emerald-500`:`bg-neutral-300`}`)]:[],c(`span`,`min-w-0 truncate`,e.label));if(r.dataset.node=e.id,r.style.left=`${t.x-90}px`,r.style.top=`${t.y-23}px`,r.style.width=`180px`,r.style.height=`46px`,r.title=e.label===e.id?e.id:`${e.label}\n${e.id}`,e.kind===`session`){r.setAttribute(`type`,`button`),r.onclick=()=>u(e.id);let t=C.sessions.find(t=>t.id===e.id);t&&t.createdAt!==null&&x(r,{...t,createdAt:t.createdAt})}E.append(r)}S(e);let A=e=>c(`span`,`inline-block h-2 w-2 rounded-full ${e}`),j=e=>c(`span`,`inline-block h-0 w-6 border-t-2 ${e}`);e.append(c(`div`,`w-max p-4`,E),c(`div`,`flex flex-wrap gap-x-5 gap-y-1.5 border-t border-neutral-200 bg-neutral-50/60 px-4 py-2 text-[11px] text-neutral-500`,c(`span`,`inline-flex items-center gap-2`,j(`border-solid border-neutral-400`),`task invocation`),c(`span`,`inline-flex items-center gap-2 text-cyan-700`,j(`border-dashed border-cyan-500`),`callback`),c(`span`,`inline-flex items-center gap-2 text-amber-700`,j(`border-dotted border-amber-500`),`control message`),c(`span`,`inline-flex items-center gap-1.5`,A(`bg-emerald-500`),`streaming`),c(`span`,`inline-flex items-center gap-1.5`,A(`bg-neutral-300`),`idle`)))}let A=a(n,e=>{e===`dependencies`?y=`dependencies`:e===`sessions`&&(y=`sessions`),T()},()=>{n.replaceChildren(),C={sessions:[],runs:[],messages:[]},w=``});return Object.assign(A,{refresh(){A.visible&&T()}})}export{C as createActivityView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{D as e,E as t,J as n,Q as r,R as i,U as a,W as o,Z as s,j as c,nt as l,q as u,tt as d,w as f}from"./index-T-vLXPch.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=u(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,c(`Boards`)),y=u(`div`,`px-4 pb-5 pt-1`);m.append(u(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(e,t){let n=await l(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(n.ok)return e.token=(await n.json().catch(()=>({}))).token??``,w();_=await s(n,`Could not change ${e.slug}`),await T()}async function x(e){_=await d(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let t=u(`div`,`group flex flex-col gap-1 py-2.5`),r=document.createElement(`a`);r.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,r.href=p(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let a=u(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,r),s=i(``,``,e.public,t=>{e.public=t,b(e,t)});s.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,s.append(u(`span`,``,`Public`));let c=f(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100 pointer-coarse:opacity-100`);c.title=`Renames the folder on disk; nothing is erased`,c.onclick=()=>void x(e.slug);let l=o(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100 pointer-coarse:opacity-100`,()=>`${location.origin}${p(e)}`);l.title=`Copy the board's link`,a.append(u(`div`,`ml-auto flex flex-none items-center gap-2`,s,l,c)),t.append(a);let d=u(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);d.append(u(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&d.append(u(`span`,`min-w-0 truncate`,e.description)),d.append(u(`span`,`flex-none text-neutral-400`,n(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=f(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>h(t),d.append(e)}if(e.public&&e.token){let t=u(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));t.title=`Public URL — anyone holding this link can read the board, no password`,d.append(t)}return t.append(d),t}let C=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function w(){let n=u(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&n.append(e(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:e,hint:r,wanted:i}of C){let a=g.filter(e=>e.public===i);a.length!==0&&n.append(t(e,r,u(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[u(`p`,`mb-3 text-[13px] text-red-600`,_)]:[],n)}async function T(){let e=await r(`/api/boards`,`Could not load boards`);if(!e.ok)return _=e.error,w();g=e.value,w(),_=``}let E=a(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import{$ as e,D as t,G as n,H as r,J as i,U as a,a as o,at as s,dt as c,f as l,ft as u,g as d,h as f,j as p,l as m,m as ee,ot as te,p as h,q as g,u as ne,v as re,w as ie,y as _}from"./index-T-vLXPch.js";var ae=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],oe=[[`path`,{d:`M12 22v-6`}],[`path`,{d:`M12 8V2`}],[`path`,{d:`M4 12H2`}],[`path`,{d:`M10 12H8`}],[`path`,{d:`M16 12h-2`}],[`path`,{d:`M22 12h-2`}],[`path`,{d:`m15 19-3-3-3 3`}],[`path`,{d:`m15 5-3 3-3-3`}]],se=[[`path`,{d:`M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z`}]],ce=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],v=[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`}],[`path`,{d:`M3 3v5h5`}]],y=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},le=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?y(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
|
-
|
|
3
|
-
`),b=null;function x(){b&&(b.remove(),b=null,document.removeEventListener(`pointerdown`,x,!0),window.removeEventListener(`scroll`,x,!0))}function ue(e,t){e.onmouseenter=()=>{x();let n=t();if(!n)return;b=g(`div`,`pointer-events-none fixed z-50 max-w-96 whitespace-pre-wrap rounded-lg bg-neutral-800/95 px-3 py-2 font-sans text-[11.5px] leading-snug text-neutral-100 shadow-lg`),b.textContent=n,document.body.append(b),document.addEventListener(`pointerdown`,x,!0),window.addEventListener(`scroll`,x,!0);let r=e.getBoundingClientRect();b.style.left=`${Math.max(8,Math.min(r.left,window.innerWidth-b.offsetWidth-8))}px`,b.style.top=r.bottom+6+b.offsetHeight>window.innerHeight?`${r.top-b.offsetHeight-6}px`:`${r.bottom+6}px`},e.onmouseleave=x}function S(e,t,n){let r=g(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,s(c,`h-3 w-3 text-indigo-600 ${e.value===t?``:`invisible`}`),g(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&r.append(g(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&r.append(g(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?i(e.at):``}`)),ue(r,()=>le(e)),r.onclick=()=>n(e.value),r}function de(e,t,n,r,i){let{base:a,head:o}=n,c=()=>o&&a===`${o}~1`?o:``,u=c()?`commit`:`compare`,d=t.commits[0],f=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...d,value:`HEAD`},...t.refs.map(e=>({value:e.name,subject:e.subject})),...t.commits.map(e=>({...e,value:e.hash}))],p=g(`div`,`flex w-[38rem] max-w-full flex-col`);function m(){let e=(e,t)=>{let n=g(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${u===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return n.onclick=()=>{u=t,m()},n},n=(e,t,n,r)=>g(`div`,`flex min-w-0 flex-1 flex-col`,g(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),g(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>S(e,n,r)))),l=u===`compare`?g(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,n(`base`,f(!1),a,e=>{a=e,m(),r(a,o)}),n(`to`,f(!0),o,e=>{o=e,m(),r(a,o)})):g(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>S({value:e.hash,...e},c(),e=>{a=`${e}~1`,o=e,ne(),r(a,o)}))),d=g(`div`,`flex-none border-t border-neutral-200`,S({value:i,label:`${i} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},a===i&&o===``?i:``,()=>{a=i,o=``,m(),r(a,o)}));d.querySelector(`button > span`).prepend(s(v)),p.replaceChildren(g(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,e(`Compare two points`,`compare`),e(`One commit`,`commit`)),l,...u===`compare`?[d]:[])}m(),l(e,p)}var fe=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,C={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},pe=30,w=`pier.filesPrefs`,T=50,E=()=>{try{let e=JSON.parse(localStorage.getItem(w)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},me=e=>{let t=e?E()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},he=(e,t)=>{let n=E();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-T)))delete n[e];try{localStorage.setItem(w,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},ge=`flex min-w-0 flex-none items-center gap-1.5 rounded-full px-2.5 py-1 font-mono text-[11.5px]`,_e=(e,t=`indigo`)=>ie(e,`${ge} cursor-pointer truncate text-left ${t===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`);function D(i,c,l,ne){let v=``,y=``,b={branch:null,refs:[],commits:[],worktrees:[]},x=`HEAD`,S=``,w=new Map,T=!0,E=!1,D=[],O=new Set,k=null,A=null,j=null,ve=g(`header`,`pagehead flex-nowrap`),M=g(`div`,`${d} flex-col items-stretch gap-1.5 px-3 py-2.5`),N=g(`div`,`min-h-0 flex-1 overflow-y-auto px-1.5 py-1.5`),ye=g(`aside`,`${_} w-64 flex-none text-[12.5px] leading-5 max-md:h-1/2 max-md:w-full`,M,N),P=g(`div`,`${d} flex-wrap gap-2 px-4 py-2.5 font-mono text-[11.5px] text-neutral-500`),F=g(`div`,`h-full min-w-0 overflow-auto`),I=g(`div`,`pointer-events-none absolute inset-y-1 right-0.5 z-20 hidden w-2`),be=g(`div`,`relative min-h-0 flex-1`,F,I),xe=g(`section`,`${_} min-h-0 min-w-0 flex-1`,P,be);i.append(ve,g(`div`,`flex min-h-0 flex-1 gap-3 px-4 pb-4 pt-1 max-md:flex-col`,ye,xe));let L=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:v,...t})}`,R=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:v,...t})}`,z=(e,t=`text-neutral-400`)=>g(`p`,`px-3 py-2 text-[12.5px] ${t}`,e),B=e=>g(`div`,`flex h-full items-center justify-center p-6`,t(e)),V=()=>T&&b.branch!==null,Se=e=>{let t=0;for(let n of w.keys())n.startsWith(`${e}/`)&&t++;return t};function Ce(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of w){if(i.status!==`D`||!e.startsWith(r))continue;let a=e.slice(r.length).split(`/`)[0];n.has(a)||(n.add(a),t.push({name:a,dir:e.slice(r.length).includes(`/`)}))}let i=t;return V()&&(i=t.filter(e=>e.dir?Se(`${r}${e.name}`)>0:w.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?Te(`${r}${e.name}`,e.name):Ee(`${r}${e.name}`,e.name))}async function we(t,n){try{let{entries:r}=await e(R(`ls`,{path:n}),`could not list this folder`);t.replaceChildren(...Ce(n,r)),t.childElementCount||t.append(z(V()?`No changes.`:`Empty.`))}catch(e){let r=Ce(n,[]);r.length?t.replaceChildren(...r):t.replaceChildren(z(String(e),`text-red-600`))}}function Te(e,t){let r=Se(e),i=[g(`span`,`truncate`,t)];r&&i.push(g(`span`,`ml-auto flex-none rounded-full bg-amber-100/80 px-1.5 font-mono text-[10px] font-semibold text-amber-700`,String(r)));let{el:a,summary:o}=n(``,i);o.className+=` rounded-lg px-2 py-1 transition-colors hover:bg-neutral-100`;let s=g(`div`,`pl-3`);a.append(s);let c=!1,l=()=>{c||(c=!0,s.append(z(`…`)),we(s,e))};return a.ontoggle=()=>{O[a.open?`add`:`delete`](e),a.open&&l()},((b.branch?T:!e.includes(`/`))&&!E||O.has(e))&&(a.open=!0,l()),a}function H(e){A?.classList.remove(`bg-indigo-50`,`font-medium`,`text-indigo-700`),A=e,e.classList.add(`bg-indigo-50`,`font-medium`,`text-indigo-700`)}function Ee(e,t){let n=w.get(e)?.status,r=g(`button`,`flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 pl-6 pr-2 text-left transition-colors hover:bg-neutral-100`,g(`span`,`truncate`,t));return n&&r.append(g(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${C[n]??`text-neutral-500`}`,n)),r.title=e,e===k&&H(r),e===j&&(j=null,k=e,H(r),J(e)),r.onclick=()=>{H(r),k=e,J(e)},r}let U=()=>void we(N,``),De=(e,t)=>[...e?[g(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[g(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function W(e,t=!1,n){if(P.classList.toggle(`hidden`,!e),P.classList.toggle(`flex`,!!e),!e)return P.replaceChildren();let i=w.get(e),a=[g(`span`,`min-w-0 truncate`,e)];i&&(a.push(g(`span`,`flex-none font-semibold ${C[i.status]??``}`,i.status)),a.push(...De(i.add,i.del)));let o=g(`div`,`ml-auto flex flex-none items-center gap-2`);if(n&&o.append(n),t){let t=document.createElement(`a`);t.className=`btn flex-none text-[12px]`,t.href=R(`file`,{path:e}),t.download=r(e),t.textContent=`Download`,o.append(t)}P.replaceChildren(...a,o)}function Oe(e){let t=0;for(;t<e.length;){if(e[t].tone!==`del`){t++;continue}let n=t;for(;t<e.length&&e[t].tone===`del`;)t++;let r=t;for(;t<e.length&&e[t].tone===`add`;)t++;let i=Math.min(r-n,t-r);for(let t=0;t<i;t++){let i=e[n+t],a=e[r+t],o=Math.min(i.text.length,a.text.length),s=0;for(;s<o&&i.text[s]===a.text[s];)s++;let c=0;for(;c<o-s&&i.text[i.text.length-1-c]===a.text[a.text.length-1-c];)c++;s+c!==0&&(i.mark=[s,i.text.length-c],a.mark=[s,a.text.length-c])}}}let G=Ie();function K(){I.classList.add(`hidden`),I.replaceChildren(),G.set([],[])}let ke=0,q=e=>e===ke;function J(e){let t=++ke;return w.has(e)?Le(e,t):Ae(e,t)}async function Ae(e,t){if(!q(t))return;K();let n=R(`file`,{path:e}),r=g(`div`,`min-w-0`);if(F.classList.remove(`flex`,`flex-col`),W(e,!0),F.replaceChildren(r),fe.test(e)){let e=g(`img`,`max-w-full p-4`);e.src=n,e.onerror=()=>r.replaceChildren(z(`Could not load image.`,`text-red-600`)),r.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let e=document.createElement(`iframe`);e.className=`h-full w-full`,e.src=n,F.replaceChildren(e),F.classList.add(`flex`,`flex-col`);return}r.append(z(`…`));let i=await fetch(n);if(!q(t))return;if(!i.ok){let{error:e}=await i.json().catch(()=>({}));r.replaceChildren(z(e??`HTTP ${i.status}`,`text-red-600`));return}if(!(i.headers.get(`content-type`)??``).startsWith(`text/plain`)){r.replaceChildren(z(`Binary file — use Download.`));return}let a=ee(await i.text());if(!q(t))return;let o=await f(e);q(t)&&r.replaceChildren(h(a,o))}function je(e){let t=[],n=0,r=0,i=!1,a=e.split(`
|
|
4
|
-
`);a.at(-1)===``&&a.pop();for(let e of a){let a=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(e);if(a){i&&t.push({nums:[``,``],text:`⋯`,tone:``}),[n,r]=[Number(a[1]),Number(a[2])],i=!0;continue}!i||e.startsWith(`\\`)||(e.startsWith(`+`)?t.push({nums:[``,r++],text:e.slice(1),tone:`add`}):e.startsWith(`-`)?t.push({nums:[n++,``],text:e.slice(1),tone:`del`}):t.push({nums:[n++,r++],text:e.slice(1),tone:``}))}return t}function Me(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n].tone;if(!r)continue;let i=t.at(-1);i&&i.end===n-1?(i.end=n,(r===`add`&&i.tone===`del`||r===`del`&&i.tone===`add`)&&(i.tone=`mixed`)):t.push({start:n,end:n,tone:r})}return t}let Ne={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Pe(e,t,n){I.replaceChildren(...e.map(e=>{let r=g(`button`,`map-mark pointer-events-auto absolute left-1/2 w-1 -translate-x-1/2 cursor-pointer rounded-full ${Ne[e.tone]}`);return r.style.top=`${e.start/t*100}%`,r.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,r.title=`line ${Fe(n,e.start)}`,r.onclick=()=>n[e.start].scrollIntoView({block:`center`}),r})),I.classList.remove(`hidden`)}let Fe=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function Ie(){let e=[],t=[],n=-1,r=g(`span`,`flex-none text-[10.5px] text-neutral-400`),a=i=>{e.length&&(n=(n+i+e.length)%e.length,t[e[n].start].scrollIntoView({block:`center`}),r.textContent=`${n+1}/${e.length}`)},c=()=>e.length>0&&!i.classList.contains(`hidden`),l=(e,t,n,r)=>{let i=g(`button`,`icon-btn flex-none`,s(e));return i.onclick=()=>a(t),o(i,r,n,()=>a(t),c),i};return{el:g(`div`,`flex flex-none items-center gap-1`,r,l(u,-1,`Previous change`,[`p`,`k`]),l(ae,1,`Next change`,[`n`,`j`])),set(i,a){e=i,t=a,n=-1,r.textContent=`${i.length} change${i.length===1?``:`s`}`}}}async function Le(t,n){K(),F.classList.remove(`flex`,`flex-col`);let r=w.get(t)?.status!==`D`;W(t,r),F.replaceChildren(z(`…`));try{let{diff:i}=await e(L(`diff`,{base:x,head:S,file:t,context:`99999`}),`could not read this diff`);if(!q(n))return;if(!i||/^Binary files /m.test(i))return Ae(t,n);let a=je(i);Oe(a);let o=await f(t);if(!q(n))return;let s=h(a,o),c=[...s.children],l=Me(a);G.set(l,c),W(t,r,G.el),F.replaceChildren(s),Pe(l,a.length,c)}catch(e){if(!q(n))return;W(t),F.replaceChildren(z(String(e),`text-red-600`))}}let Y=()=>S&&x===`${S}~1`?S:``,Re=()=>b.commits.find(e=>e.hash===Y())?.subject??``,ze=()=>Y()?`${Y()} — ${Re()||`commit`}`:`${x} ↔ ${S||`working tree`}`,Be=(e,t,n,r)=>{let i=g(`button`,`icon-btn ${n?`bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:text-indigo-700`:``}`);return i.append(s(e)),i.title=t,i.setAttribute(`aria-label`,t),i.setAttribute(`aria-pressed`,String(n)),i.onclick=r,i};function X(){let e=Be(se,`Show only files that differ`,T,()=>{T=!T,X(),U()}),t=Be(oe,E?`Restore folders`:`Collapse all folders`,E,()=>{if(E=!E,E)D=[...O],O.clear();else for(let e of D)O.add(e);X(),U()});if(!b.branch){M.replaceChildren(g(`div`,`flex items-center gap-1.5`,g(`span`,`min-w-0 flex-1 text-[11px] leading-snug text-neutral-400`,`Not a git repository — nothing to compare.`),t));return}let n=ie(ze(),`${re} font-mono`);if(Y()){let e=b.commits.find(e=>e.hash===Y());ue(n,()=>e?le(e):``)}else n.title=`Choose what to diff`;n.onclick=async()=>{await We(),de(n,b,{base:x,head:S},(e,t)=>{x=e,S=t,Z()},$())};let[r,i]=[0,0];for(let e of w.values())r+=e.add,i+=e.del;M.replaceChildren(n,g(`div`,`flex items-center gap-1.5 pl-0.5`,g(`span`,`text-[11px] text-neutral-400`,`${w.size} changed file${w.size===1?``:`s`}`),...De(r,i),g(`span`,`ml-auto`),e,t))}let Ve=()=>{y&&v&&he(y,{cwd:v,base:x,head:S})},He=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||b.refs.some(e=>e.name===t)||b.commits.some(e=>e.hash===t)};async function Ue(){if(w=new Map,!b.branch)return;let{files:t}=await e(L(`diff`,{base:x,head:S}),`could not read what changed`);for(let e of t)w.set(e.path,{status:e.status,add:e.add,del:e.del});if(w.size<=pe)for(let e of w.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)O.add(t.slice(0,e).join(`/`))}}async function Z(){Ve();try{await Ue()}catch(e){X(),N.replaceChildren(z(String(e),`text-red-600`));return}X(),U(),k&&J(k)}function Q(){let e=_e(``,`neutral`);e.className+=` max-w-72`,e.append(g(`span`,`truncate max-md:hidden`,v||`Choose a folder…`),g(`span`,`truncate md:hidden`,v?r(v):`Choose a folder…`)),e.title=v?`${v} — switch folder`:`Choose a folder`,e.onclick=()=>m(e,b.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),v||void 0,l);let t=g(`button`,`icon-btn ml-auto`,s(te));t.type=`button`,t.title=`Close Files`,t.setAttribute(`aria-label`,`Close Files`),t.onclick=ne;let n=b.branch?[g(`span`,`${ge} bg-neutral-100 text-neutral-600`,s(ce),g(`span`,`truncate`,b.branch))]:[];ve.replaceChildren(p(`Files`),e,...n,t)}async function We(){if(v)try{b=await e(L(`git`,{}),`could not read this repository`),Q()}catch(e){F.replaceChildren(B(String(e)))}}let $=()=>b.refs.some(e=>e.name===`main`)?`main`:b.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ge(t){if(Q(),k=null,A=null,O.clear(),j=t||null,t){T=!1;let e=t.split(`/`);for(let t=1;t<e.length;t++)O.add(e.slice(0,t).join(`/`))}if(K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`)),!v){M.replaceChildren(),N.replaceChildren(),F.replaceChildren(B(`No folder yet — pick one from the chip above.`));return}try{b=await e(L(`git`,{}),`could not read this repository`)}catch(e){b={branch:null,refs:[],commits:[],worktrees:[]},F.replaceChildren(B(String(e)))}let n=me(y),r=n&&n.cwd===v&&He(n.base)&&He(n.head)?n:null;x=r?.base??$(),S=r?.head??``,Q(),await Z()}return a(i,(e,t)=>{let n=c(),r=n?.id??``,i=new URLSearchParams(t).get(`select`)??void 0,a=e?.startsWith(`/`)?e:(r===y?v:``)||me(r)?.cwd||n?.cwd||``;if(a===v&&r===y&&N.childElementCount&&(!i||i===k)){Q(),We().then(Z);return}y=r,v=a,Ge(i)},()=>{K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`))})}export{D as createExplorerView};
|
|
Binary file
|
|
Binary file
|