@hansenexus/hud 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +186 -4
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
  5. package/dist/env-DfxpPfs9.js +28 -0
  6. package/dist/env-vEjyJfPW.d.ts +17 -0
  7. package/dist/force-state-BaqWsmrf.js +153 -0
  8. package/dist/force-state-CCqoTfVB.d.ts +23 -0
  9. package/dist/handler-WmE4Lu7l.d.ts +39 -0
  10. package/dist/index-BYsdS-7d.d.ts +55 -0
  11. package/dist/index.d.ts +6 -36
  12. package/dist/index.js +119 -202
  13. package/dist/marker-C66LxOhJ.d.ts +9 -0
  14. package/dist/marker-CMrvxPmS.d.ts +12 -0
  15. package/dist/marker-DjeGvL-F.js +9 -0
  16. package/dist/marker-uZYPOJ4M.js +12 -0
  17. package/dist/markers.d.ts +3 -0
  18. package/dist/markers.js +3 -0
  19. package/dist/next.d.ts +3 -18
  20. package/dist/next.js +3 -28
  21. package/dist/payload-CLKDVZWl.d.ts +68 -0
  22. package/dist/plugins/access/server.d.ts +122 -0
  23. package/dist/plugins/access/server.js +339 -0
  24. package/dist/plugins/access.d.ts +41 -0
  25. package/dist/plugins/access.js +250 -0
  26. package/dist/plugins/agent/server.d.ts +129 -0
  27. package/dist/plugins/agent/server.js +429 -0
  28. package/dist/plugins/agent.d.ts +37 -0
  29. package/dist/plugins/agent.js +411 -0
  30. package/dist/plugins/design.d.ts +125 -0
  31. package/dist/plugins/design.js +619 -0
  32. package/dist/plugins/observe.d.ts +64 -0
  33. package/dist/plugins/observe.js +617 -0
  34. package/dist/plugins/ops/server.d.ts +82 -0
  35. package/dist/plugins/ops/server.js +380 -0
  36. package/dist/plugins/ops.d.ts +26 -0
  37. package/dist/plugins/ops.js +362 -0
  38. package/dist/plugins/vitals.js +3 -86
  39. package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
  40. package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
  41. package/dist/review.d.ts +181 -0
  42. package/dist/review.js +1017 -0
  43. package/dist/selector-NSYSe7AZ.js +28 -0
  44. package/dist/server.d.ts +3 -39
  45. package/dist/server.js +1 -1
  46. package/dist/shortcut-U6ftVY06.js +222 -0
  47. package/dist/state-boundary.d.ts +26 -0
  48. package/dist/state-boundary.js +61 -0
  49. package/dist/styles--slk86ZQ.js +332 -0
  50. package/dist/types-DADBRiGk.d.ts +50 -0
  51. package/dist/types-QJUajwWq.js +35 -0
  52. package/dist/types-f3jlNxzw.d.ts +92 -0
  53. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  54. package/package.json +23 -3
@@ -0,0 +1,362 @@
1
+ import { i as definePlugin, n as HudApiError } from "../plugins-DVy_vaYr.js";
2
+ import { i as OPS_CAPABILITY, r as stateRating, t as TELEMETRY_RANGES } from "../types-QJUajwWq.js";
3
+ import { useCallback, useState, useSyncExternalStore } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/plugins/ops/poll.ts
6
+ /** Entries per plugin context, so the pill and the panel share one poller per path. */
7
+ const entries = /* @__PURE__ */ new WeakMap();
8
+ function entryFor(ctx, path) {
9
+ let byPath = entries.get(ctx);
10
+ if (!byPath) {
11
+ byPath = /* @__PURE__ */ new Map();
12
+ entries.set(ctx, byPath);
13
+ }
14
+ let entry = byPath.get(path);
15
+ if (!entry) {
16
+ entry = {
17
+ state: { loading: true },
18
+ listeners: /* @__PURE__ */ new Set(),
19
+ inflight: false
20
+ };
21
+ byPath.set(path, entry);
22
+ }
23
+ return entry;
24
+ }
25
+ /** A readable message for a failed poll; 502 bodies carry the provider's own. */
26
+ function describeError(error) {
27
+ if (error instanceof HudApiError) {
28
+ if (error.status === 404) return "route not mounted: add ops() to createHudHandler";
29
+ const body = error.body;
30
+ if (typeof body?.error === "string") return body.error;
31
+ return `HTTP ${error.status}`;
32
+ }
33
+ return error instanceof Error ? error.message : "request failed";
34
+ }
35
+ function set(entry, state) {
36
+ entry.state = state;
37
+ for (const listener of entry.listeners) listener();
38
+ }
39
+ async function tick(ctx, path, entry, force = false) {
40
+ if (entry.inflight || !force && typeof document !== "undefined" && document.hidden) return;
41
+ entry.inflight = true;
42
+ try {
43
+ set(entry, {
44
+ data: await ctx.api(path),
45
+ loading: false
46
+ });
47
+ } catch (error) {
48
+ set(entry, {
49
+ ...entry.state,
50
+ error: describeError(error),
51
+ loading: false
52
+ });
53
+ } finally {
54
+ entry.inflight = false;
55
+ }
56
+ }
57
+ /**
58
+ * Polls one of the plugin's routes while anything renders it. Pauses while
59
+ * the tab is hidden; subscribers of the same path share one request.
60
+ */
61
+ function usePoll(ctx, path, intervalMs) {
62
+ const entry = entryFor(ctx, path);
63
+ const subscribe = useCallback((listener) => {
64
+ entry.listeners.add(listener);
65
+ if (entry.listeners.size === 1) {
66
+ tick(ctx, path, entry);
67
+ entry.timer = setInterval(() => void tick(ctx, path, entry), intervalMs);
68
+ }
69
+ return () => {
70
+ entry.listeners.delete(listener);
71
+ if (entry.listeners.size === 0) clearInterval(entry.timer);
72
+ };
73
+ }, [
74
+ ctx,
75
+ path,
76
+ entry,
77
+ intervalMs
78
+ ]);
79
+ const state = useSyncExternalStore(subscribe, () => entry.state, () => entry.state);
80
+ const refresh = useCallback(() => void tick(ctx, path, entry, true), [
81
+ ctx,
82
+ path,
83
+ entry
84
+ ]);
85
+ return {
86
+ ...state,
87
+ refresh
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region src/plugins/ops/styles.ts
92
+ /** Panel styles, rendered as a `<style>` inside the panel, so inside the shadow root. */
93
+ const OPS_CSS = `
94
+ .ops { display: grid; gap: 12px; }
95
+ .ops section { display: grid; gap: 6px; }
96
+ .ops h3 {
97
+ margin: 0;
98
+ font-size: 10px;
99
+ font-weight: 600;
100
+ letter-spacing: 0.06em;
101
+ text-transform: uppercase;
102
+ color: var(--hud-fg-muted);
103
+ }
104
+ .ops .head { display: flex; align-items: center; gap: 6px; font-family: var(--hud-mono); }
105
+ .ops .head .branch { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
106
+ .ops .warn { color: var(--hud-warn); margin: 0; }
107
+ .ops .muted { color: var(--hud-fg-muted); }
108
+ .ops ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
109
+ .ops li a, .ops li > span {
110
+ display: grid;
111
+ grid-template-columns: 6px minmax(0, 1fr) auto auto;
112
+ align-items: center;
113
+ gap: 8px;
114
+ padding: 3px 4px;
115
+ border-radius: 4px;
116
+ color: inherit;
117
+ text-decoration: none;
118
+ }
119
+ .ops li a:hover { background: var(--hud-bg-subtle); }
120
+ .ops .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
121
+ .ops .title small { color: var(--hud-fg-muted); margin-left: 6px; }
122
+ .ops .mono { font-family: var(--hud-mono); color: var(--hud-fg-muted); }
123
+ .ops .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
124
+ .ops .stat { padding: 6px 8px; border: 1px solid var(--hud-border); border-radius: 6px; }
125
+ .ops .stat b { display: block; font-family: var(--hud-mono); font-size: 13px; }
126
+ .ops .stat span { font-size: 10px; color: var(--hud-fg-muted); }
127
+ .ops .row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
128
+ .ops .ranges { display: flex; gap: 2px; }
129
+ .ops button.chip { padding: 1px 6px; border-radius: 999px; font-family: var(--hud-mono); }
130
+ .ops button.chip:hover, .ops button.chip[aria-pressed="true"] { background: var(--hud-bg-subtle); }
131
+ `;
132
+ //#endregion
133
+ //#region src/plugins/ops/ops-panel.tsx
134
+ const CI_POLL_MS = 6e4;
135
+ const TELEMETRY_POLL_MS = 6e4;
136
+ const STATE_LABEL = {
137
+ success: "passing",
138
+ failure: "failing",
139
+ cancelled: "cancelled",
140
+ running: "running",
141
+ queued: "queued",
142
+ neutral: "neutral"
143
+ };
144
+ function stateLabel(state) {
145
+ return state ? STATE_LABEL[state] : "no runs";
146
+ }
147
+ /** `3m`, `5h`, `2d` since `iso`. */
148
+ function age(iso, now = Date.now()) {
149
+ if (!iso) return "";
150
+ const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
151
+ if (seconds < 60) return `${seconds}s`;
152
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
153
+ if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
154
+ return `${Math.round(seconds / 86400)}d`;
155
+ }
156
+ function Missing({ name }) {
157
+ return /* @__PURE__ */ jsxs("p", {
158
+ className: "warn",
159
+ children: [
160
+ "Not configured: set ",
161
+ /* @__PURE__ */ jsx("code", { children: name ?? "its env" }),
162
+ " in the app's server env."
163
+ ]
164
+ });
165
+ }
166
+ function RunRow({ run }) {
167
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ /* @__PURE__ */ jsx("span", {
169
+ className: "dot",
170
+ "data-rating": stateRating(run.state),
171
+ title: STATE_LABEL[run.state]
172
+ }),
173
+ /* @__PURE__ */ jsxs("span", {
174
+ className: "title",
175
+ title: run.title,
176
+ children: [run.name, /* @__PURE__ */ jsx("small", { children: run.title })]
177
+ }),
178
+ /* @__PURE__ */ jsx("span", {
179
+ className: "mono",
180
+ children: run.sha
181
+ }),
182
+ /* @__PURE__ */ jsx("span", {
183
+ className: "mono",
184
+ children: age(run.startedAt)
185
+ })
186
+ ] });
187
+ return /* @__PURE__ */ jsx("li", { children: run.url ? /* @__PURE__ */ jsx("a", {
188
+ href: run.url,
189
+ target: "_blank",
190
+ rel: "noreferrer",
191
+ children: content
192
+ }) : /* @__PURE__ */ jsx("span", { children: content }) });
193
+ }
194
+ function Deployment({ deployment }) {
195
+ if (!deployment) return /* @__PURE__ */ jsx("p", {
196
+ className: "muted",
197
+ children: "Nothing deployed."
198
+ });
199
+ const rows = [
200
+ ["Environment", deployment.environment],
201
+ ["Image", deployment.image],
202
+ ["Sha", deployment.sha],
203
+ ["Ready", deployment.ready],
204
+ ["Since", deployment.at && `${age(deployment.at)} ago`]
205
+ ];
206
+ return /* @__PURE__ */ jsx("table", {
207
+ className: "vitals",
208
+ children: /* @__PURE__ */ jsx("tbody", { children: rows.flatMap(([label, value]) => value ? /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
209
+ className: "label",
210
+ children: label
211
+ }), /* @__PURE__ */ jsx("td", { children: value })] }, label) : []) })
212
+ });
213
+ }
214
+ function CiSection({ ctx }) {
215
+ const { data, error, loading, refresh } = usePoll(ctx, "/ci", CI_POLL_MS);
216
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("section", {
217
+ "aria-label": "CI",
218
+ children: [
219
+ /* @__PURE__ */ jsxs("div", {
220
+ className: "row",
221
+ children: [/* @__PURE__ */ jsxs("h3", { children: ["CI", data ? ` · ${data.repo}` : ""] }), /* @__PURE__ */ jsx("button", {
222
+ type: "button",
223
+ className: "chip",
224
+ onClick: refresh,
225
+ children: "refresh"
226
+ })]
227
+ }),
228
+ loading && /* @__PURE__ */ jsx("p", {
229
+ className: "muted",
230
+ children: "Loading…"
231
+ }),
232
+ error && /* @__PURE__ */ jsx("p", {
233
+ className: "warn",
234
+ children: error
235
+ }),
236
+ data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
237
+ data?.configured && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
238
+ className: "head",
239
+ children: [
240
+ /* @__PURE__ */ jsx("span", {
241
+ className: "dot",
242
+ "data-rating": stateRating(data.state)
243
+ }),
244
+ /* @__PURE__ */ jsx("span", {
245
+ className: "branch",
246
+ children: data.branch ?? "no branch"
247
+ }),
248
+ /* @__PURE__ */ jsxs("span", {
249
+ className: "muted",
250
+ children: [data.branch ? stateLabel(data.state) : "latest runs, all branches", data.sha ? ` @ ${data.sha}` : ""]
251
+ })
252
+ ]
253
+ }), data.runs.length > 0 ? /* @__PURE__ */ jsx("ul", { children: data.runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) }) : /* @__PURE__ */ jsxs("p", {
254
+ className: "muted",
255
+ children: [
256
+ "No runs",
257
+ data.branch ? ` on ${data.branch}` : "",
258
+ "."
259
+ ]
260
+ })] })
261
+ ]
262
+ }), data && data.deployment !== void 0 && /* @__PURE__ */ jsxs("section", {
263
+ "aria-label": "Deployment",
264
+ children: [/* @__PURE__ */ jsx("h3", { children: "Deployment" }), /* @__PURE__ */ jsx(Deployment, { deployment: data.deployment })]
265
+ })] });
266
+ }
267
+ function formatStat(value, unit) {
268
+ if (value === null) return "–";
269
+ return `${value.toFixed(value < 10 ? 2 : 0)}${unit}`;
270
+ }
271
+ function TelemetrySection({ ctx }) {
272
+ const [range, setRange] = useState("1h");
273
+ const { data, error, loading } = usePoll(ctx, `/telemetry?range=${range}`, TELEMETRY_POLL_MS);
274
+ return /* @__PURE__ */ jsxs("section", {
275
+ "aria-label": "Telemetry",
276
+ children: [
277
+ /* @__PURE__ */ jsxs("div", {
278
+ className: "row",
279
+ children: [/* @__PURE__ */ jsxs("h3", { children: ["Telemetry", data ? ` · ${data.service}` : ""] }), /* @__PURE__ */ jsx("div", {
280
+ className: "ranges",
281
+ children: TELEMETRY_RANGES.map((r) => /* @__PURE__ */ jsx("button", {
282
+ type: "button",
283
+ className: "chip",
284
+ "aria-pressed": r === range,
285
+ onClick: () => setRange(r),
286
+ children: r
287
+ }, r))
288
+ })]
289
+ }),
290
+ loading && !data && /* @__PURE__ */ jsx("p", {
291
+ className: "muted",
292
+ children: "Loading…"
293
+ }),
294
+ error && /* @__PURE__ */ jsx("p", {
295
+ className: "warn",
296
+ children: error
297
+ }),
298
+ data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
299
+ data?.configured && /* @__PURE__ */ jsxs("div", {
300
+ className: "stats",
301
+ children: [
302
+ /* @__PURE__ */ jsxs("div", {
303
+ className: "stat",
304
+ children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.errorRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "errors" })]
305
+ }),
306
+ /* @__PURE__ */ jsxs("div", {
307
+ className: "stat",
308
+ children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.p95Ms, " ms") }), /* @__PURE__ */ jsx("span", { children: "p95" })]
309
+ }),
310
+ /* @__PURE__ */ jsxs("div", {
311
+ className: "stat",
312
+ children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.requestRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "requests" })]
313
+ })
314
+ ]
315
+ })
316
+ ]
317
+ });
318
+ }
319
+ function createOpsPanel({ ci, telemetry }) {
320
+ function OpsPanel({ ctx }) {
321
+ return /* @__PURE__ */ jsxs("div", {
322
+ className: "ops",
323
+ children: [
324
+ /* @__PURE__ */ jsx("style", { children: OPS_CSS }),
325
+ ci && /* @__PURE__ */ jsx(CiSection, { ctx }),
326
+ telemetry && /* @__PURE__ */ jsx(TelemetrySection, { ctx })
327
+ ]
328
+ });
329
+ }
330
+ return OpsPanel;
331
+ }
332
+ //#endregion
333
+ //#region src/plugins/ops/index.tsx
334
+ function OpsPill({ ctx }) {
335
+ const { data, error } = usePoll(ctx, "/ci", CI_POLL_MS);
336
+ const label = !data && error ? "error" : !data ? "–" : !data.configured ? "off" : stateLabel(data.state);
337
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
338
+ className: "dot",
339
+ "data-rating": data ? stateRating(data.state) : void 0
340
+ }), `CI ${label}`] });
341
+ }
342
+ /**
343
+ * CI and telemetry: the pill shows the CI state of the branch the server runs
344
+ * on, the panel lists recent runs, the deployment (when the CI provider knows
345
+ * it) and error rate, p95 and request rate. Client half; the providers live in
346
+ * the server half, `@hansenexus/hud/plugins/ops/server`, which reads tokens
347
+ * from env and never sends them to the browser.
348
+ */
349
+ function ops({ ci = true, telemetry = true } = {}) {
350
+ return definePlugin({
351
+ id: "ops",
352
+ title: "Ops",
353
+ capability: OPS_CAPABILITY,
354
+ panel: createOpsPanel({
355
+ ci,
356
+ telemetry
357
+ }),
358
+ ...ci ? { pill: OpsPill } : {}
359
+ });
360
+ }
361
+ //#endregion
362
+ export { ops };
@@ -1,90 +1,7 @@
1
- import { i as definePlugin } from "../plugins-CzUiTn_r.js";
2
- import { useEffect, useSyncExternalStore } from "react";
1
+ import { i as definePlugin } from "../plugins-DVy_vaYr.js";
2
+ import { i as useVitals, n as formatVital, r as startVitals, t as VitalsPanel } from "../vitals-panel-Dvc5dnR-.js";
3
+ import { useEffect } from "react";
3
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
5
- //#region src/plugins/vitals/store.ts
6
- /** Display order: the three Core Web Vitals first, then the diagnostics. */
7
- const VITAL_ORDER = [
8
- "LCP",
9
- "INP",
10
- "CLS",
11
- "FCP",
12
- "TTFB"
13
- ];
14
- let snapshot = {};
15
- const listeners = /* @__PURE__ */ new Set();
16
- let started = false;
17
- function record(metric) {
18
- snapshot = {
19
- ...snapshot,
20
- [metric.name]: {
21
- name: metric.name,
22
- value: metric.value,
23
- rating: metric.rating
24
- }
25
- };
26
- for (const listener of listeners) listener();
27
- }
28
- /**
29
- * Starts collection once per page. Called when the HUD mounts, not when the
30
- * panel opens: LCP and FCP are only reported for the initial load, so a lazy
31
- * start would miss them.
32
- */
33
- function startVitals() {
34
- if (started) return;
35
- started = true;
36
- const opts = { reportAllChanges: true };
37
- try {
38
- onLCP(record, opts);
39
- onINP(record, opts);
40
- onCLS(record, opts);
41
- onFCP(record, opts);
42
- onTTFB(record, opts);
43
- } catch {}
44
- }
45
- function getVitals() {
46
- return snapshot;
47
- }
48
- function subscribeVitals(listener) {
49
- listeners.add(listener);
50
- return () => listeners.delete(listener);
51
- }
52
- function useVitals() {
53
- return useSyncExternalStore(subscribeVitals, getVitals, getVitals);
54
- }
55
- function formatVital(name, value) {
56
- if (name === "CLS") return value.toFixed(2);
57
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)} s`;
58
- return `${Math.round(value)} ms`;
59
- }
60
- //#endregion
61
- //#region src/plugins/vitals/vitals-panel.tsx
62
- function VitalsPanel() {
63
- const vitals = useVitals();
64
- const rows = VITAL_ORDER.flatMap((name) => {
65
- const vital = vitals[name];
66
- return vital ? [vital] : [];
67
- });
68
- if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
69
- className: "empty",
70
- children: "No metrics yet. LCP reports after the first interaction or tab switch."
71
- });
72
- return /* @__PURE__ */ jsx("table", {
73
- className: "vitals",
74
- children: /* @__PURE__ */ jsx("tbody", { children: rows.map((vital) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
75
- className: "label",
76
- children: vital.name
77
- }), /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("span", {
78
- className: "dot",
79
- "data-rating": vital.rating,
80
- style: {
81
- display: "inline-block",
82
- marginRight: 6
83
- }
84
- }), formatVital(vital.name, vital.value)] })] }, vital.name)) })
85
- });
86
- }
87
- //#endregion
88
5
  //#region src/plugins/vitals/index.tsx
89
6
  function VitalsPill() {
90
7
  useEffect(() => {
@@ -1,4 +1,24 @@
1
+ import { r as GrabPayload } from "./payload-CLKDVZWl.js";
1
2
  import { ComponentType } from "react";
3
+ //#region src/core/grabber/store.d.ts
4
+ interface HudGrabState {
5
+ /** The picker is active: the next click on the page completes the grab. */
6
+ grabbing: boolean;
7
+ /** The last completed grab, until a plugin clears it. */
8
+ payload: GrabPayload | null;
9
+ }
10
+ /** Grab mode for one HUD, shared by the shell and every plugin's `ctx.grab`. */
11
+ interface HudGrab {
12
+ start: () => void;
13
+ cancel: () => void;
14
+ /** Forgets the last payload, e.g. once it has been dispatched. */
15
+ clear: () => void;
16
+ getState: () => HudGrabState;
17
+ subscribe: (listener: () => void) => () => void;
18
+ }
19
+ /** Subscribes a component to a HUD's grab state: `useGrab(ctx.grab)`. */
20
+ declare function useGrab(grab: HudGrab): HudGrabState;
21
+ //#endregion
2
22
  //#region src/core/plugins.d.ts
3
23
  /**
4
24
  * A capability a plugin needs, such as `"cicd.read"`. Core assumes development
@@ -21,6 +41,10 @@ interface HudPluginContext {
21
41
  pin: (edge: PinEdge) => void;
22
42
  /** Closes this plugin's panel. */
23
43
  close: () => void;
44
+ /** Opens this plugin's panel. */
45
+ open: () => void;
46
+ /** The HUD's element grabber: start a grab, read or clear the last payload. See `useGrab`. */
47
+ grab: HudGrab;
24
48
  }
25
49
  interface HudPluginProps {
26
50
  ctx: HudPluginContext;
@@ -63,7 +87,21 @@ interface HudPlugin {
63
87
  /** Live content inside the plugin's pill item. Keep it inline and non-interactive. */
64
88
  pill?: ComponentType<HudPluginProps>;
65
89
  commands?: readonly HudCommand[];
90
+ /** Called when a grab (⌘⇧G or the pill's crosshair) completes. */
91
+ onGrab?: (payload: GrabPayload, ctx: HudPluginContext) => void;
66
92
  routes?: HudRoutes;
93
+ /** Opens deployed tiers (the `access` plugin). Without one the HUD renders only in development. */
94
+ access?: HudAccessControl;
95
+ }
96
+ /**
97
+ * What the `access` plugin hands `defineHud`. `env` is the loader's tier
98
+ * (`development`, `preview`, `staging`, `production`, `disabled`).
99
+ */
100
+ interface HudAccessControl {
101
+ /** Capability check for the tier; outside development it narrows `can`. */
102
+ can: (env: string, capability: HudCapability) => boolean;
103
+ /** Hook: whether the operator has unlocked this tier. Until then only the access plugin shows. */
104
+ useUnlocked: (env: string, apiBase: string) => boolean;
67
105
  }
68
106
  /** Identity helper that types a plugin object without widening it. */
69
107
  declare function definePlugin<const P extends HudPlugin>(plugin: P): P;
@@ -73,4 +111,4 @@ declare class HudApiError extends Error {
73
111
  constructor(status: number, body: unknown, url: string);
74
112
  }
75
113
  //#endregion
76
- export { HudPlugin as a, HudRouteContext as c, PinEdge as d, definePlugin as f, HudHttpMethod as i, HudRouteHandler as l, HudCapability as n, HudPluginContext as o, HudCommand as r, HudPluginProps as s, HudApiError as t, HudRoutes as u };
114
+ export { HudHttpMethod as a, HudPluginProps as c, HudRoutes as d, PinEdge as f, useGrab as g, HudGrabState as h, HudCommand as i, HudRouteContext as l, HudGrab as m, HudApiError as n, HudPlugin as o, definePlugin as p, HudCapability as r, HudPluginContext as s, HudAccessControl as t, HudRouteHandler as u };
@@ -34,13 +34,15 @@ var HudApiError = class extends Error {
34
34
  this.name = "HudApiError";
35
35
  }
36
36
  };
37
- function createPluginContext(plugin, { app, apiBase, can, pin, close }) {
37
+ function createPluginContext(plugin, { app, apiBase, can, pin, close, open, grab }) {
38
38
  const prefix = `${apiBase.replace(/\/+$/, "")}/${plugin.id}`;
39
39
  return {
40
40
  app,
41
41
  can,
42
42
  pin,
43
43
  close,
44
+ open,
45
+ grab,
44
46
  api: async (path, init) => {
45
47
  const url = `${prefix}/${path.replace(/^\/+/, "")}`;
46
48
  const headers = new Headers(init?.headers);