@hansenexus/hud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/chunk-0cj8dkk7.js +127 -0
- package/chunk-1atxwdhw.js +48 -0
- package/chunk-24vrg16w.js +39 -0
- package/chunk-3j9sm249.js +26 -0
- package/chunk-50pq4sg6.js +41 -0
- package/chunk-5gtx3pza.js +9 -0
- package/chunk-6gw4x08j.js +264 -0
- package/chunk-6jt8yg4m.js +117 -0
- package/chunk-71se605m.js +85 -0
- package/chunk-82nm9wcg.js +316 -0
- package/chunk-8tam7r3h.js +68 -0
- package/chunk-babjpqkb.js +52 -0
- package/chunk-c5y856ad.js +98 -0
- package/chunk-ghxh8926.js +172 -0
- package/chunk-hc2g0n35.js +24 -0
- package/chunk-k0vr8dsy.js +179 -0
- package/chunk-k50yfp5d.js +166 -0
- package/chunk-kyeb6qeq.js +95 -0
- package/chunk-msqkkqw7.js +140 -0
- package/chunk-nm82wr8r.js +58 -0
- package/chunk-r06bpmy6.js +79 -0
- package/chunk-t9eh4t0b.js +89 -0
- package/chunk-tf774c4w.js +115 -0
- package/chunk-v086j53b.js +19 -0
- package/chunk-wspz08q7.js +67 -0
- package/chunk-xezxydm3.js +47 -0
- package/chunk-xjn0896g.js +574 -0
- package/chunk-xztw0xyt.js +113 -0
- package/chunk-zzrpd92r.js +172 -0
- package/core/access.d.mts +53 -0
- package/core/access.js +27 -0
- package/core/api-types.d.mts +146 -0
- package/core/api-types.js +9 -0
- package/core/capabilities.d.mts +27 -0
- package/core/capabilities.js +14 -0
- package/core/capture-buffer.d.mts +37 -0
- package/core/capture-buffer.js +9 -0
- package/core/force-state.d.mts +68 -0
- package/core/force-state.js +21 -0
- package/core/mock-cookie.d.mts +30 -0
- package/core/mock-cookie.js +15 -0
- package/core/types.d.mts +59 -0
- package/core/types.js +9 -0
- package/hud-provider.d.mts +38 -0
- package/hud-provider.js +15 -0
- package/hud.d.mts +15 -0
- package/hud.js +18 -0
- package/index.d.mts +94 -0
- package/index.js +170 -0
- package/lazy-panel.d.mts +28 -0
- package/lazy-panel.js +11 -0
- package/package.json +153 -0
- package/panels/cicd-panel.d.mts +9 -0
- package/panels/cicd-panel.js +13 -0
- package/panels/debug-info-panel.d.mts +21 -0
- package/panels/debug-info-panel.js +9 -0
- package/panels/dispatch-panel.d.mts +10 -0
- package/panels/dispatch-panel.js +14 -0
- package/panels/env-info-panel.d.mts +9 -0
- package/panels/env-info-panel.js +12 -0
- package/panels/feature-flags-panel.d.mts +26 -0
- package/panels/feature-flags-panel.js +9 -0
- package/panels/force-state-panel.d.mts +11 -0
- package/panels/force-state-panel.js +113 -0
- package/panels/info-row.d.mts +21 -0
- package/panels/info-row.js +14 -0
- package/panels/mock-seed-panel.d.mts +15 -0
- package/panels/mock-seed-panel.js +14 -0
- package/panels/network-console-panel.d.mts +6 -0
- package/panels/network-console-panel.js +9 -0
- package/panels/role-switcher-panel.d.mts +28 -0
- package/panels/role-switcher-panel.js +9 -0
- package/panels/sessions-panel.d.mts +6 -0
- package/panels/sessions-panel.js +13 -0
- package/panels/tokens-panel.d.mts +38 -0
- package/panels/tokens-panel.js +9 -0
- package/panels/unlock-panel.d.mts +33 -0
- package/panels/unlock-panel.js +14 -0
- package/panels/variants-panel.d.mts +51 -0
- package/panels/variants-panel.js +12 -0
- package/panels/vitals-panel.d.mts +11 -0
- package/panels/vitals-panel.js +15 -0
- package/server/guard.d.mts +42 -0
- package/server/guard.js +12 -0
- package/server/unlock.d.mts +28 -0
- package/server/unlock.js +108 -0
- package/state-boundary.d.mts +25 -0
- package/state-boundary.js +54 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button
|
|
3
|
+
} from "./chunk-xztw0xyt.js";
|
|
4
|
+
|
|
5
|
+
// panels/info-row.tsx
|
|
6
|
+
import { Copy } from "lucide-react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
"use client";
|
|
9
|
+
function PanelSection({
|
|
10
|
+
icon: Icon,
|
|
11
|
+
title,
|
|
12
|
+
children
|
|
13
|
+
}) {
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
15
|
+
children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("h4", {
|
|
17
|
+
className: "mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
18
|
+
children: [
|
|
19
|
+
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
20
|
+
className: "h-3 w-3"
|
|
21
|
+
}),
|
|
22
|
+
title
|
|
23
|
+
]
|
|
24
|
+
}),
|
|
25
|
+
/* @__PURE__ */ jsx("div", {
|
|
26
|
+
className: "space-y-1.5",
|
|
27
|
+
children
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function InfoRow({
|
|
33
|
+
label,
|
|
34
|
+
value,
|
|
35
|
+
copyable = false
|
|
36
|
+
}) {
|
|
37
|
+
function handleCopy() {
|
|
38
|
+
navigator.clipboard.writeText(value);
|
|
39
|
+
}
|
|
40
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
41
|
+
className: "flex items-center justify-between px-2 py-1 hover:bg-muted/50",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", {
|
|
44
|
+
className: "text-[10px] text-muted-foreground",
|
|
45
|
+
children: label
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", {
|
|
48
|
+
className: "flex items-center gap-1",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx("span", {
|
|
51
|
+
className: "max-w-[220px] truncate font-mono text-[10px]",
|
|
52
|
+
children: value
|
|
53
|
+
}),
|
|
54
|
+
copyable && /* @__PURE__ */ jsx(Button, {
|
|
55
|
+
variant: "ghost",
|
|
56
|
+
size: "icon",
|
|
57
|
+
className: "h-4 w-4 shrink-0",
|
|
58
|
+
onClick: handleCopy,
|
|
59
|
+
children: /* @__PURE__ */ jsx(Copy, {
|
|
60
|
+
className: "h-2.5 w-2.5"
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function ShortcutRow({ keys, label }) {
|
|
69
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
className: "flex items-center justify-between px-2 py-1",
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsx("span", {
|
|
73
|
+
className: "text-[10px] text-muted-foreground",
|
|
74
|
+
children: label
|
|
75
|
+
}),
|
|
76
|
+
/* @__PURE__ */ jsx("div", {
|
|
77
|
+
className: "flex gap-0.5",
|
|
78
|
+
children: keys.map((key) => /* @__PURE__ */ jsx("kbd", {
|
|
79
|
+
className: "flex h-5 min-w-[20px] items-center justify-center rounded border bg-muted/50 px-1 font-mono text-[10px]",
|
|
80
|
+
children: key
|
|
81
|
+
}, key))
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function LoadingSpinner() {
|
|
87
|
+
return /* @__PURE__ */ jsx("div", {
|
|
88
|
+
className: "flex items-center justify-center py-8",
|
|
89
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
90
|
+
className: "h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { PanelSection, InfoRow, ShortcutRow, LoadingSpinner };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hudRequiresUnlock
|
|
3
|
+
} from "./chunk-r06bpmy6.js";
|
|
4
|
+
import {
|
|
5
|
+
PanelSection
|
|
6
|
+
} from "./chunk-kyeb6qeq.js";
|
|
7
|
+
import {
|
|
8
|
+
Button
|
|
9
|
+
} from "./chunk-xztw0xyt.js";
|
|
10
|
+
|
|
11
|
+
// panels/unlock-panel.tsx
|
|
12
|
+
import { Lock, ShieldAlert } from "lucide-react";
|
|
13
|
+
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
"use client";
|
|
16
|
+
function useHudUnlock(apiBase, access) {
|
|
17
|
+
const required = hudRequiresUnlock(access);
|
|
18
|
+
const [state, setState] = useState(required ? "checking" : "unlocked");
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!required) {
|
|
22
|
+
setState("unlocked");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${apiBase}/unlock`, { cache: "no-store" });
|
|
29
|
+
const body = await res.json().catch(() => ({}));
|
|
30
|
+
if (cancelled)
|
|
31
|
+
return;
|
|
32
|
+
if (body.configured === false)
|
|
33
|
+
setState("unconfigured");
|
|
34
|
+
else
|
|
35
|
+
setState(body.unlocked ? "unlocked" : "locked");
|
|
36
|
+
} catch {
|
|
37
|
+
if (!cancelled)
|
|
38
|
+
setState("locked");
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [apiBase, required]);
|
|
45
|
+
const unlock = useCallback(async (password) => {
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${apiBase}/unlock`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify({ password })
|
|
52
|
+
});
|
|
53
|
+
const body = await res.json().catch(() => ({}));
|
|
54
|
+
if (res.ok && body.unlocked) {
|
|
55
|
+
setState("unlocked");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
setError(body.error ?? "unlock refused");
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
setError(cause instanceof Error ? cause.message : "unlock failed");
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}, [apiBase]);
|
|
64
|
+
return { state, error, unlock };
|
|
65
|
+
}
|
|
66
|
+
function HudUnlockPanel({ env, state, error, onUnlock }) {
|
|
67
|
+
const [password, setPassword] = useState("");
|
|
68
|
+
const [submitting, setSubmitting] = useState(false);
|
|
69
|
+
async function handleSubmit(event) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
if (!password || submitting)
|
|
72
|
+
return;
|
|
73
|
+
setSubmitting(true);
|
|
74
|
+
const ok = await onUnlock(password);
|
|
75
|
+
setSubmitting(false);
|
|
76
|
+
if (ok)
|
|
77
|
+
setPassword("");
|
|
78
|
+
}
|
|
79
|
+
if (state === "unconfigured") {
|
|
80
|
+
return /* @__PURE__ */ jsx("div", {
|
|
81
|
+
className: "space-y-3 p-3",
|
|
82
|
+
children: /* @__PURE__ */ jsx(PanelSection, {
|
|
83
|
+
icon: ShieldAlert,
|
|
84
|
+
title: "hud locked",
|
|
85
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
86
|
+
className: "px-2 text-[11px] leading-relaxed text-muted-foreground",
|
|
87
|
+
children: [
|
|
88
|
+
"No ",
|
|
89
|
+
/* @__PURE__ */ jsx("code", {
|
|
90
|
+
className: "font-mono",
|
|
91
|
+
children: "HUD_ACCESS_PASSWORD"
|
|
92
|
+
}),
|
|
93
|
+
" is configured for this deployment, so the hud cannot be unlocked here. Set it from the app's 1Password item and restart the pod."
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return /* @__PURE__ */ jsx("div", {
|
|
100
|
+
className: "space-y-3 p-3",
|
|
101
|
+
children: /* @__PURE__ */ jsxs(PanelSection, {
|
|
102
|
+
icon: Lock,
|
|
103
|
+
title: `hud locked · ${env}`,
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx("p", {
|
|
106
|
+
className: "px-2 text-[11px] leading-relaxed text-muted-foreground",
|
|
107
|
+
children: env === "production" ? "Production is observe-only: vitals, logs and CI state, nothing that writes. The unlock lasts 2 hours." : "Enter the deployment's hud password to open the panels. The unlock lasts 12 hours."
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsxs("form", {
|
|
110
|
+
className: "flex items-center gap-2 px-2 pt-1",
|
|
111
|
+
onSubmit: handleSubmit,
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsx("input", {
|
|
114
|
+
type: "password",
|
|
115
|
+
autoComplete: "off",
|
|
116
|
+
value: password,
|
|
117
|
+
onChange: (event) => setPassword(event.target.value),
|
|
118
|
+
placeholder: "hud password",
|
|
119
|
+
"aria-label": "hud password",
|
|
120
|
+
className: "h-7 flex-1 rounded border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring"
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ jsx(Button, {
|
|
123
|
+
type: "submit",
|
|
124
|
+
size: "sm",
|
|
125
|
+
className: "h-7 text-[11px]",
|
|
126
|
+
disabled: !password || submitting,
|
|
127
|
+
children: submitting ? "Checking…" : "Unlock"
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
}),
|
|
131
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
132
|
+
className: "px-2 text-[10px] text-destructive",
|
|
133
|
+
children: error
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { useHudUnlock, HudUnlockPanel };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PanelSection
|
|
3
|
+
} from "./chunk-kyeb6qeq.js";
|
|
4
|
+
|
|
5
|
+
// panels/tokens-panel.tsx
|
|
6
|
+
import { Palette } from "lucide-react";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
"use client";
|
|
10
|
+
function TokensPanel({ adapter }) {
|
|
11
|
+
const { groups, sourceHint } = adapter;
|
|
12
|
+
const [copied, setCopied] = useState(null);
|
|
13
|
+
function copy(cssVar) {
|
|
14
|
+
navigator.clipboard?.writeText(`var(${cssVar})`).catch(() => {});
|
|
15
|
+
setCopied(cssVar);
|
|
16
|
+
window.setTimeout(() => setCopied((prev) => prev === cssVar ? null : prev), 1200);
|
|
17
|
+
}
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "space-y-4",
|
|
20
|
+
children: [
|
|
21
|
+
groups.map((group) => {
|
|
22
|
+
const scopeProps = group.scope ? { [group.scope.attribute]: group.scope.value ?? "" } : {};
|
|
23
|
+
return /* @__PURE__ */ jsx("div", {
|
|
24
|
+
...scopeProps,
|
|
25
|
+
children: /* @__PURE__ */ jsx(PanelSection, {
|
|
26
|
+
icon: Palette,
|
|
27
|
+
title: group.label,
|
|
28
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
29
|
+
className: "flex flex-wrap gap-1.5 px-2",
|
|
30
|
+
children: group.tokens.map((token) => /* @__PURE__ */ jsxs("button", {
|
|
31
|
+
type: "button",
|
|
32
|
+
onClick: () => copy(token.cssVar),
|
|
33
|
+
title: `var(${token.cssVar}) — click to copy`,
|
|
34
|
+
className: "group flex w-[104px] flex-col overflow-hidden rounded-md border text-left",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "block h-8 w-full border-b",
|
|
38
|
+
style: { background: `var(${token.cssVar})` }
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: "truncate px-1.5 py-1 font-mono text-[9px] text-muted-foreground group-hover:text-foreground",
|
|
42
|
+
children: copied === token.cssVar ? "copied!" : token.label ?? token.cssVar.replace(/^--[a-z]+-/, "")
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
}, token.cssVar))
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}, group.label);
|
|
49
|
+
}),
|
|
50
|
+
sourceHint && /* @__PURE__ */ jsx("p", {
|
|
51
|
+
className: "px-2 font-mono text-[9.5px] text-muted-foreground/50",
|
|
52
|
+
children: sourceHint
|
|
53
|
+
})
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { TokensPanel };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveHudEnv
|
|
3
|
+
} from "./chunk-babjpqkb.js";
|
|
4
|
+
|
|
5
|
+
// core/access.ts
|
|
6
|
+
var HUD_UNLOCK_COOKIE = "hud-unlock";
|
|
7
|
+
var HUD_UNLOCK_TTL_MS = {
|
|
8
|
+
open: 0,
|
|
9
|
+
password: 12 * 60 * 60 * 1000,
|
|
10
|
+
flag: 2 * 60 * 60 * 1000,
|
|
11
|
+
denied: 0
|
|
12
|
+
};
|
|
13
|
+
function resolveHudAccess(env = resolveHudEnv()) {
|
|
14
|
+
if (env === "disabled")
|
|
15
|
+
return "denied";
|
|
16
|
+
if (env === "development")
|
|
17
|
+
return "open";
|
|
18
|
+
if (env === "production")
|
|
19
|
+
return "flag";
|
|
20
|
+
return "password";
|
|
21
|
+
}
|
|
22
|
+
function hudRequiresUnlock(access) {
|
|
23
|
+
return access === "password" || access === "flag";
|
|
24
|
+
}
|
|
25
|
+
function hudUnlockTtlMs(access) {
|
|
26
|
+
return HUD_UNLOCK_TTL_MS[access];
|
|
27
|
+
}
|
|
28
|
+
var encoder = new TextEncoder;
|
|
29
|
+
async function hmacHex(password, message) {
|
|
30
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(password), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
31
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
32
|
+
return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
}
|
|
34
|
+
function timingSafeEqual(a, b) {
|
|
35
|
+
const length = Math.max(a.length, b.length);
|
|
36
|
+
let diff = a.length ^ b.length;
|
|
37
|
+
for (let i = 0;i < length; i++) {
|
|
38
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
39
|
+
}
|
|
40
|
+
return diff === 0;
|
|
41
|
+
}
|
|
42
|
+
async function signHudUnlockToken(password, issuedAt = Date.now()) {
|
|
43
|
+
return `${issuedAt}.${await hmacHex(password, String(issuedAt))}`;
|
|
44
|
+
}
|
|
45
|
+
async function verifyHudUnlockToken(token, password, options = {}) {
|
|
46
|
+
if (!token || !password)
|
|
47
|
+
return false;
|
|
48
|
+
const separator = token.indexOf(".");
|
|
49
|
+
if (separator <= 0)
|
|
50
|
+
return false;
|
|
51
|
+
const issuedAtRaw = token.slice(0, separator);
|
|
52
|
+
const signature = token.slice(separator + 1);
|
|
53
|
+
const issuedAt = Number(issuedAtRaw);
|
|
54
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt <= 0)
|
|
55
|
+
return false;
|
|
56
|
+
const now = options.now ?? Date.now();
|
|
57
|
+
const ttlMs = options.ttlMs ?? HUD_UNLOCK_TTL_MS.flag;
|
|
58
|
+
const age = now - issuedAt;
|
|
59
|
+
if (age < 0 || age > ttlMs)
|
|
60
|
+
return false;
|
|
61
|
+
return timingSafeEqual(signature, await hmacHex(password, issuedAtRaw));
|
|
62
|
+
}
|
|
63
|
+
function readCookie(header, name) {
|
|
64
|
+
if (!header)
|
|
65
|
+
return;
|
|
66
|
+
for (const part of header.split(";")) {
|
|
67
|
+
const trimmed = part.trim();
|
|
68
|
+
if (trimmed.startsWith(`${name}=`)) {
|
|
69
|
+
return decodeURIComponent(trimmed.slice(name.length + 1));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
function hudUnlockCookieString(token, ttlMs) {
|
|
75
|
+
const base = `${HUD_UNLOCK_COOKIE}=${token ?? ""}; Path=/; HttpOnly; SameSite=Strict; Secure`;
|
|
76
|
+
return token ? `${base}; Max-Age=${Math.floor(ttlMs / 1000)}` : `${base}; Max-Age=0`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { HUD_UNLOCK_COOKIE, HUD_UNLOCK_TTL_MS, resolveHudAccess, hudRequiresUnlock, hudUnlockTtlMs, timingSafeEqual, signHudUnlockToken, verifyHudUnlockToken, readCookie, hudUnlockCookieString };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bridgeFetch,
|
|
3
|
+
getBridgeInfo
|
|
4
|
+
} from "./chunk-50pq4sg6.js";
|
|
5
|
+
import {
|
|
6
|
+
useHud
|
|
7
|
+
} from "./chunk-wspz08q7.js";
|
|
8
|
+
import {
|
|
9
|
+
LoadingSpinner,
|
|
10
|
+
PanelSection
|
|
11
|
+
} from "./chunk-kyeb6qeq.js";
|
|
12
|
+
import {
|
|
13
|
+
Badge
|
|
14
|
+
} from "./chunk-xztw0xyt.js";
|
|
15
|
+
|
|
16
|
+
// panels/sessions-panel.tsx
|
|
17
|
+
import { CircleDot } from "lucide-react";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
20
|
+
"use client";
|
|
21
|
+
function SessionsPanel() {
|
|
22
|
+
const { config } = useHud();
|
|
23
|
+
const [state, setState] = useState({ kind: "loading" });
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
async function load() {
|
|
27
|
+
const info = await getBridgeInfo(config.apiBase ?? "/api/hud");
|
|
28
|
+
if (cancelled)
|
|
29
|
+
return;
|
|
30
|
+
if (!info) {
|
|
31
|
+
setState({ kind: "no-bridge" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const res = await bridgeFetch(info, "/sessions");
|
|
36
|
+
const sessions = await res.json();
|
|
37
|
+
if (!cancelled)
|
|
38
|
+
setState({ kind: "ready", sessions });
|
|
39
|
+
} catch {
|
|
40
|
+
if (!cancelled)
|
|
41
|
+
setState({ kind: "no-bridge" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
load();
|
|
45
|
+
const timer = setInterval(() => void load(), 30000);
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
};
|
|
50
|
+
}, [config.apiBase]);
|
|
51
|
+
if (state.kind === "loading")
|
|
52
|
+
return /* @__PURE__ */ jsx(LoadingSpinner, {});
|
|
53
|
+
if (state.kind === "no-bridge") {
|
|
54
|
+
return /* @__PURE__ */ jsx("p", {
|
|
55
|
+
className: "px-2 text-[10px] text-amber-500",
|
|
56
|
+
children: "bridge not reachable — run `bun hud:bridge` in the repo to see local Claude sessions."
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsxs(PanelSection, {
|
|
60
|
+
icon: CircleDot,
|
|
61
|
+
title: "Claude sessions · this machine",
|
|
62
|
+
children: [
|
|
63
|
+
state.sessions.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
64
|
+
className: "px-2 text-[10px] text-muted-foreground/60",
|
|
65
|
+
children: "No active sessions."
|
|
66
|
+
}),
|
|
67
|
+
state.sessions.map((session) => /* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: "flex items-center gap-2 px-2 py-1 font-mono text-[10.5px]",
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
71
|
+
variant: "outline",
|
|
72
|
+
className: `h-4 px-1.5 text-[9px] ${session.idleSeconds < 120 ? "text-emerald-500" : "text-muted-foreground"}`,
|
|
73
|
+
children: session.idleSeconds < 120 ? "active" : `idle ${Math.round(session.idleSeconds / 60)} m`
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsx("span", {
|
|
76
|
+
className: "min-w-0 flex-1 truncate",
|
|
77
|
+
children: session.name ?? session.sessionId.slice(0, 12)
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsx("span", {
|
|
80
|
+
className: "max-w-[200px] truncate text-muted-foreground/60",
|
|
81
|
+
children: session.branch || "—"
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
}, session.sessionId))
|
|
85
|
+
]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { SessionsPanel };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureBuffer
|
|
3
|
+
} from "./chunk-24vrg16w.js";
|
|
4
|
+
import {
|
|
5
|
+
Button
|
|
6
|
+
} from "./chunk-xztw0xyt.js";
|
|
7
|
+
|
|
8
|
+
// panels/network-console-panel.tsx
|
|
9
|
+
import { Trash2 } from "lucide-react";
|
|
10
|
+
import { useState, useSyncExternalStore } from "react";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
"use client";
|
|
13
|
+
var FILTERS = ["all", "log", "warn", "error", "fetch", "longtask"];
|
|
14
|
+
var KIND_CLASS = {
|
|
15
|
+
log: "text-muted-foreground",
|
|
16
|
+
warn: "text-amber-500",
|
|
17
|
+
error: "text-red-500",
|
|
18
|
+
fetch: "text-sky-500",
|
|
19
|
+
longtask: "text-violet-500"
|
|
20
|
+
};
|
|
21
|
+
function statusClass(status) {
|
|
22
|
+
if (status === undefined)
|
|
23
|
+
return "";
|
|
24
|
+
if (status === 0 || status >= 500)
|
|
25
|
+
return "text-red-500";
|
|
26
|
+
if (status >= 400)
|
|
27
|
+
return "text-amber-500";
|
|
28
|
+
return "text-emerald-500";
|
|
29
|
+
}
|
|
30
|
+
function NetworkConsolePanel() {
|
|
31
|
+
const entries = useSyncExternalStore(captureBuffer.subscribe, captureBuffer.getSnapshot, captureBuffer.getSnapshot);
|
|
32
|
+
const [filter, setFilter] = useState("all");
|
|
33
|
+
const visible = filter === "all" ? entries : entries.filter((e) => e.kind === filter);
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: "flex h-full flex-col gap-2",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("div", {
|
|
38
|
+
className: "flex items-center gap-1",
|
|
39
|
+
children: [
|
|
40
|
+
FILTERS.map((f) => /* @__PURE__ */ jsxs(Button, {
|
|
41
|
+
variant: filter === f ? "secondary" : "ghost",
|
|
42
|
+
size: "sm",
|
|
43
|
+
className: "h-6 px-2 font-mono text-[10px]",
|
|
44
|
+
onClick: () => setFilter(f),
|
|
45
|
+
children: [
|
|
46
|
+
f,
|
|
47
|
+
f !== "all" && /* @__PURE__ */ jsx("span", {
|
|
48
|
+
className: "ml-1 text-muted-foreground/60",
|
|
49
|
+
children: entries.filter((e) => e.kind === f).length
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
}, f)),
|
|
53
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
54
|
+
variant: "ghost",
|
|
55
|
+
size: "sm",
|
|
56
|
+
className: "ml-auto h-6 px-2 text-[10px]",
|
|
57
|
+
onClick: () => captureBuffer.clear(),
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Trash2, {
|
|
60
|
+
className: "mr-1 h-3 w-3"
|
|
61
|
+
}),
|
|
62
|
+
"clear"
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: "min-h-0 flex-1 overflow-auto rounded-md border bg-muted/10 p-2",
|
|
69
|
+
children: [
|
|
70
|
+
visible.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
71
|
+
className: "px-1 text-[10px] text-muted-foreground/60",
|
|
72
|
+
children: "Nothing captured yet — console, fetch, and long tasks land here."
|
|
73
|
+
}),
|
|
74
|
+
visible.slice(-200).map((entry) => /* @__PURE__ */ jsx(EntryRow, {
|
|
75
|
+
entry
|
|
76
|
+
}, entry.id))
|
|
77
|
+
]
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function EntryRow({ entry }) {
|
|
83
|
+
const time = new Date(entry.ts).toLocaleTimeString(undefined, { hour12: false });
|
|
84
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
85
|
+
className: "flex items-baseline gap-2 px-1 py-0.5 font-mono text-[10.5px] hover:bg-muted/40",
|
|
86
|
+
children: [
|
|
87
|
+
/* @__PURE__ */ jsx("span", {
|
|
88
|
+
className: "shrink-0 text-muted-foreground/50",
|
|
89
|
+
children: time
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ jsx("span", {
|
|
92
|
+
className: `w-14 shrink-0 uppercase ${KIND_CLASS[entry.kind]}`,
|
|
93
|
+
children: entry.kind
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ jsx("span", {
|
|
96
|
+
className: "min-w-0 flex-1 truncate",
|
|
97
|
+
title: entry.text,
|
|
98
|
+
children: entry.text
|
|
99
|
+
}),
|
|
100
|
+
entry.status !== undefined && /* @__PURE__ */ jsx("span", {
|
|
101
|
+
className: `shrink-0 ${statusClass(entry.status)}`,
|
|
102
|
+
children: entry.status || "ERR"
|
|
103
|
+
}),
|
|
104
|
+
entry.durationMs !== undefined && /* @__PURE__ */ jsxs("span", {
|
|
105
|
+
className: "shrink-0 text-muted-foreground/60",
|
|
106
|
+
children: [
|
|
107
|
+
entry.durationMs,
|
|
108
|
+
" ms"
|
|
109
|
+
]
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { NetworkConsolePanel };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// core/types.ts
|
|
2
|
+
var HUD_BUNDLE_MARKER = "__HN_HUD_BUNDLE_MARKER__";
|
|
3
|
+
var HUD_CAPABILITIES = [
|
|
4
|
+
"observe.app",
|
|
5
|
+
"observe.estate",
|
|
6
|
+
"cicd.read",
|
|
7
|
+
"cicd.trigger",
|
|
8
|
+
"inject.seed",
|
|
9
|
+
"mock.toggle",
|
|
10
|
+
"auth.impersonate",
|
|
11
|
+
"dispatch.local",
|
|
12
|
+
"dispatch.hermes",
|
|
13
|
+
"variants.switch",
|
|
14
|
+
"variants.vote",
|
|
15
|
+
"variants.generate",
|
|
16
|
+
"state.force"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export { HUD_BUNDLE_MARKER, HUD_CAPABILITIES };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasCapability
|
|
3
|
+
} from "./chunk-babjpqkb.js";
|
|
4
|
+
|
|
5
|
+
// hud-provider.tsx
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useState
|
|
13
|
+
} from "react";
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
"use client";
|
|
16
|
+
var HudContext = createContext(null);
|
|
17
|
+
function HudProvider({ config, env, panels: basePanels, children }) {
|
|
18
|
+
const [sources, setSources] = useState({});
|
|
19
|
+
const registerSource = useCallback((id, source) => {
|
|
20
|
+
setSources((prev) => ({ ...prev, [id]: source }));
|
|
21
|
+
}, []);
|
|
22
|
+
const unregisterSource = useCallback((id) => {
|
|
23
|
+
setSources((prev) => {
|
|
24
|
+
if (!(id in prev))
|
|
25
|
+
return prev;
|
|
26
|
+
const next = { ...prev };
|
|
27
|
+
delete next[id];
|
|
28
|
+
return next;
|
|
29
|
+
});
|
|
30
|
+
}, []);
|
|
31
|
+
const can = useCallback((capability) => hasCapability(env, capability), [env]);
|
|
32
|
+
const panels = useMemo(() => {
|
|
33
|
+
const registered = Object.values(sources).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).flatMap((source) => source.panels);
|
|
34
|
+
return [...basePanels ?? [], ...registered].filter((panel) => can(panel.capability)).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
35
|
+
}, [basePanels, sources, can]);
|
|
36
|
+
const value = useMemo(() => ({
|
|
37
|
+
config,
|
|
38
|
+
env,
|
|
39
|
+
name: config.name ?? "hud",
|
|
40
|
+
can,
|
|
41
|
+
panels,
|
|
42
|
+
registerSource,
|
|
43
|
+
unregisterSource
|
|
44
|
+
}), [config, env, can, panels, registerSource, unregisterSource]);
|
|
45
|
+
return /* @__PURE__ */ jsx(HudContext.Provider, {
|
|
46
|
+
value,
|
|
47
|
+
children
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function useHud() {
|
|
51
|
+
const ctx = useContext(HudContext);
|
|
52
|
+
if (!ctx)
|
|
53
|
+
throw new Error("useHud must be used within a HudProvider");
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
function useHudSafe() {
|
|
57
|
+
return useContext(HudContext);
|
|
58
|
+
}
|
|
59
|
+
function useRegisterHudPanels(id, panels, order = 0) {
|
|
60
|
+
const ctx = useContext(HudContext);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
ctx?.registerSource(id, { panels, order });
|
|
63
|
+
}, [ctx, id, panels, order]);
|
|
64
|
+
useEffect(() => () => ctx?.unregisterSource(id), [ctx, id]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { HudProvider, useHud, useHudSafe, useRegisterHudPanels };
|