@hansenexus/hud 0.2.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 (55) hide show
  1. package/README.md +253 -9
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/define-hud-BBzsSl8b.d.ts +36 -0
  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 -39
  12. package/dist/index.js +980 -264
  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 +33 -0
  20. package/dist/next.js +52 -0
  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.d.ts +25 -0
  39. package/dist/plugins/vitals.js +26 -0
  40. package/dist/plugins-CQ_0w1ot.d.ts +114 -0
  41. package/dist/plugins-DVy_vaYr.js +70 -0
  42. package/dist/review.d.ts +181 -0
  43. package/dist/review.js +1017 -0
  44. package/dist/selector-NSYSe7AZ.js +28 -0
  45. package/dist/server.d.ts +3 -0
  46. package/dist/server.js +106 -0
  47. package/dist/shortcut-U6ftVY06.js +222 -0
  48. package/dist/state-boundary.d.ts +26 -0
  49. package/dist/state-boundary.js +61 -0
  50. package/dist/styles--slk86ZQ.js +332 -0
  51. package/dist/types-DADBRiGk.d.ts +50 -0
  52. package/dist/types-QJUajwWq.js +35 -0
  53. package/dist/types-f3jlNxzw.d.ts +92 -0
  54. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  55. package/package.json +36 -3
@@ -0,0 +1,380 @@
1
+ import { i as definePlugin } from "../../plugins-DVy_vaYr.js";
2
+ import { i as OPS_CAPABILITY, n as combineStates, t as TELEMETRY_RANGES } from "../../types-QJUajwWq.js";
3
+ //#region src/plugins/ops/errors.ts
4
+ /**
5
+ * A provider failure whose message is safe to send to the client (it names
6
+ * the upstream and its status, never a URL with credentials or a token).
7
+ * The routes answer 502 with it; any other error becomes a generic 502.
8
+ */
9
+ var OpsProviderError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "OpsProviderError";
13
+ }
14
+ };
15
+ /** Upstream calls give up after this, so a dead host never hangs a panel. */
16
+ const UPSTREAM_TIMEOUT_MS = 5e3;
17
+ //#endregion
18
+ //#region src/plugins/ops/env.ts
19
+ /** Reads a server env var. Absent `process` (MSW in a browser) reads as unset. */
20
+ function readEnv(name) {
21
+ if (typeof process === "undefined") return void 0;
22
+ const value = process.env[name];
23
+ return value === "" ? void 0 : value;
24
+ }
25
+ /** `process.cwd()`, or undefined where there is none. */
26
+ function currentDir() {
27
+ const proc = globalThis.process;
28
+ return typeof proc?.cwd === "function" ? proc.cwd() : void 0;
29
+ }
30
+ //#endregion
31
+ //#region src/plugins/ops/git-head.ts
32
+ /**
33
+ * Branch named by a `HEAD` file: `ref: refs/heads/feat/x` → `feat/x`.
34
+ * A detached HEAD (a bare sha) names no branch.
35
+ */
36
+ function parseHead(head) {
37
+ return /^ref:\s*refs\/heads\/(.+)$/.exec(head.trim())?.[1] ?? null;
38
+ }
39
+ /** Git dir a worktree's `.git` file points at: `gitdir: /repo/.git/worktrees/x`. */
40
+ function parseGitFile(content) {
41
+ return /^gitdir:\s*(.+)$/m.exec(content)?.[1]?.trim() ?? null;
42
+ }
43
+ function parents(dir) {
44
+ const parts = dir.replace(/[\\/]+$/, "").split(/[\\/]/);
45
+ const dirs = [];
46
+ for (let i = parts.length; i > 0; i--) dirs.push(parts.slice(0, i).join("/") || "/");
47
+ return dirs;
48
+ }
49
+ /**
50
+ * Branch of the git checkout containing `cwd` (default `process.cwd()`), read
51
+ * from `.git/HEAD` without spawning git. Walks up, so an app inside a monorepo
52
+ * finds the root's `.git`; follows a worktree's `.git` file. Null when there
53
+ * is no checkout, no filesystem (edge runtime, browser) or HEAD is detached.
54
+ */
55
+ async function readGitBranch(cwd = currentDir()) {
56
+ if (!cwd) return null;
57
+ let fs;
58
+ try {
59
+ fs = await import("node:fs/promises");
60
+ } catch {
61
+ return null;
62
+ }
63
+ for (const dir of parents(cwd)) {
64
+ const dotGit = `${dir === "/" ? "" : dir}/.git`;
65
+ try {
66
+ return parseHead(await fs.readFile(`${dotGit}/HEAD`, "utf8"));
67
+ } catch {}
68
+ try {
69
+ const gitDir = parseGitFile(await fs.readFile(dotGit, "utf8"));
70
+ if (gitDir) {
71
+ const absolute = /^([\\/]|[A-Za-z]:)/.test(gitDir) ? gitDir : `${dir}/${gitDir}`;
72
+ return parseHead(await fs.readFile(`${absolute}/HEAD`, "utf8"));
73
+ }
74
+ } catch {}
75
+ }
76
+ return null;
77
+ }
78
+ //#endregion
79
+ //#region src/plugins/ops/github-actions.ts
80
+ const BRANCH_ENV = [
81
+ "HUD_GIT_BRANCH",
82
+ "VERCEL_GIT_COMMIT_REF",
83
+ "GITHUB_HEAD_REF",
84
+ "GITHUB_REF_NAME"
85
+ ];
86
+ const REPO = /^[\w.-]+\/[\w.-]+$/;
87
+ /** GitHub's status + conclusion pair as one state. */
88
+ function githubRunState(status, conclusion) {
89
+ if (status === "in_progress") return "running";
90
+ if (status !== "completed") return "queued";
91
+ switch (conclusion) {
92
+ case "success": return "success";
93
+ case "failure":
94
+ case "timed_out":
95
+ case "startup_failure":
96
+ case "action_required": return "failure";
97
+ case "cancelled":
98
+ case "stale": return "cancelled";
99
+ default: return "neutral";
100
+ }
101
+ }
102
+ function toRun(run) {
103
+ const name = run.name ?? `workflow ${run.workflow_id}`;
104
+ const title = run.display_title ?? run.head_commit?.message.split("\n")[0] ?? name;
105
+ return {
106
+ id: String(run.id),
107
+ name,
108
+ title,
109
+ state: githubRunState(run.status, run.conclusion),
110
+ ...run.head_branch ? { branch: run.head_branch } : {},
111
+ sha: run.head_sha.slice(0, 7),
112
+ url: run.html_url,
113
+ startedAt: run.created_at
114
+ };
115
+ }
116
+ /**
117
+ * State of the newest commit among `runs` (newest first): the latest run of
118
+ * each workflow on that commit, combined.
119
+ */
120
+ function headState(runs) {
121
+ const head = runs[0]?.head_sha;
122
+ if (!head) return {
123
+ sha: null,
124
+ state: null
125
+ };
126
+ const latest = /* @__PURE__ */ new Map();
127
+ for (const run of runs) if (run.head_sha === head && !latest.has(run.workflow_id)) latest.set(run.workflow_id, run);
128
+ const states = [...latest.values()].map((run) => githubRunState(run.status, run.conclusion));
129
+ return {
130
+ sha: head.slice(0, 7),
131
+ state: combineStates(states)
132
+ };
133
+ }
134
+ /**
135
+ * GitHub Actions as the `ops` plugin's CI provider. Server only: the token is
136
+ * read from env on each request and only ever sent to GitHub.
137
+ *
138
+ * ```ts
139
+ * ops({ ci: githubActions({ repo: "hansenexus/hud" }) })
140
+ * ```
141
+ */
142
+ function githubActions(options) {
143
+ const { repo, tokenEnv = "GITHUB_TOKEN", limit = 10, cacheSeconds = 15 } = options;
144
+ if (!REPO.test(repo)) throw new Error(`hud ops: githubActions repo "${repo}" must be owner/name`);
145
+ const apiUrl = (options.apiUrl ?? "https://api.github.com").replace(/\/+$/, "");
146
+ const cache = /* @__PURE__ */ new Map();
147
+ async function resolveBranch() {
148
+ const { branch } = options;
149
+ if (typeof branch === "string") return branch;
150
+ if (typeof branch === "function") return await branch() ?? null;
151
+ for (const name of BRANCH_ENV) {
152
+ const value = readEnv(name);
153
+ if (value) return value;
154
+ }
155
+ return readGitBranch();
156
+ }
157
+ async function fetchRuns(token, branch) {
158
+ const params = new URLSearchParams({ per_page: String(Math.max(limit, 20)) });
159
+ if (branch) params.set("branch", branch);
160
+ let res;
161
+ try {
162
+ res = await fetch(`${apiUrl}/repos/${repo}/actions/runs?${params}`, {
163
+ headers: {
164
+ authorization: `Bearer ${token}`,
165
+ accept: "application/vnd.github+json",
166
+ "x-github-api-version": "2022-11-28"
167
+ },
168
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
169
+ });
170
+ } catch {
171
+ throw new OpsProviderError("GitHub unreachable");
172
+ }
173
+ if (!res.ok) throw new OpsProviderError(`GitHub answered ${res.status}`);
174
+ return (await res.json()).workflow_runs ?? [];
175
+ }
176
+ function cachedRuns(token, branch) {
177
+ const key = branch ?? "";
178
+ const hit = cache.get(key);
179
+ if (hit && Date.now() - hit.at < cacheSeconds * 1e3) return hit.value;
180
+ const value = fetchRuns(token, branch);
181
+ cache.set(key, {
182
+ at: Date.now(),
183
+ value
184
+ });
185
+ value.catch(() => cache.delete(key));
186
+ return value;
187
+ }
188
+ return {
189
+ name: "github-actions",
190
+ async snapshot() {
191
+ const branch = await resolveBranch();
192
+ const base = {
193
+ provider: "github-actions",
194
+ repo,
195
+ branch
196
+ };
197
+ const token = readEnv(tokenEnv);
198
+ if (!token) return {
199
+ ...base,
200
+ configured: false,
201
+ missing: tokenEnv,
202
+ state: null,
203
+ sha: null,
204
+ runs: []
205
+ };
206
+ const runs = await cachedRuns(token, branch);
207
+ const head = branch ? headState(runs) : {
208
+ sha: null,
209
+ state: null
210
+ };
211
+ return {
212
+ ...base,
213
+ configured: true,
214
+ ...head,
215
+ runs: runs.slice(0, limit).map(toRun)
216
+ };
217
+ }
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/plugins/ops/signoz.ts
222
+ const SERVICE = /^[\w.-]{1,128}$/;
223
+ const RANGE_SECONDS = {
224
+ "1h": 3600,
225
+ "24h": 86400,
226
+ "7d": 604800
227
+ };
228
+ const RANGE_STEP = {
229
+ "1h": "5m",
230
+ "24h": "15m",
231
+ "7d": "1h"
232
+ };
233
+ /** PromQL for the three numbers, from SigNoz's span metrics. */
234
+ function signozQueries(service) {
235
+ const label = `service_name="${service}"`;
236
+ return {
237
+ errorRate: `sum(rate(signoz_calls_total{${label},status_code="STATUS_CODE_ERROR"}[5m]))`,
238
+ p95Ms: `histogram_quantile(0.95, sum(rate(signoz_latency_bucket{${label}}[5m])) by (le))`,
239
+ requestRate: `sum(rate(signoz_calls_total{${label}}[5m]))`
240
+ };
241
+ }
242
+ /** Latest sample of the first series; null when there is none or it is not a number. */
243
+ function latestValue(result) {
244
+ const values = result.data?.result?.[0]?.values;
245
+ const last = values?.[values.length - 1];
246
+ if (!last) return null;
247
+ const value = Number.parseFloat(last[1]);
248
+ return Number.isFinite(value) ? value : null;
249
+ }
250
+ /**
251
+ * SigNoz as the `ops` plugin's telemetry provider: error rate, p95 latency and
252
+ * request rate of one service over its PromQL `query_range` API. Server only:
253
+ * the URL and API key come from env on each request.
254
+ *
255
+ * ```ts
256
+ * ops({ telemetry: signoz({ service: "hansenexus" }) })
257
+ * ```
258
+ */
259
+ function signoz(options) {
260
+ const { service, urlEnv = "SIGNOZ_QUERY_URL", apiKeyEnv = "SIGNOZ_API_KEY" } = options;
261
+ if (!SERVICE.test(service)) throw new Error(`hud ops: signoz service "${service}" is not a valid name`);
262
+ const queries = signozQueries(service);
263
+ async function query(baseUrl, apiKey, promql, range) {
264
+ const end = Math.floor(Date.now() / 1e3);
265
+ const params = new URLSearchParams({
266
+ query: promql,
267
+ start: String(end - RANGE_SECONDS[range]),
268
+ end: String(end),
269
+ step: RANGE_STEP[range]
270
+ });
271
+ const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/v1/query_range?${params}`, {
272
+ headers: apiKey ? { "SIGNOZ-API-KEY": apiKey } : {},
273
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
274
+ });
275
+ if (!res.ok) throw new OpsProviderError(`SigNoz answered ${res.status}`);
276
+ return latestValue(await res.json());
277
+ }
278
+ return {
279
+ name: "signoz",
280
+ async snapshot(range) {
281
+ const base = {
282
+ provider: "signoz",
283
+ service,
284
+ range
285
+ };
286
+ const url = readEnv(urlEnv);
287
+ if (!url) return {
288
+ ...base,
289
+ configured: false,
290
+ missing: urlEnv,
291
+ errorRate: null,
292
+ p95Ms: null,
293
+ requestRate: null
294
+ };
295
+ const apiKey = readEnv(apiKeyEnv);
296
+ const [errorRate, p95Ms, requestRate] = await Promise.allSettled([
297
+ query(url, apiKey, queries.errorRate, range),
298
+ query(url, apiKey, queries.p95Ms, range),
299
+ query(url, apiKey, queries.requestRate, range)
300
+ ]);
301
+ if (errorRate.status === "rejected" && p95Ms.status === "rejected" && requestRate.status === "rejected") throw errorRate.reason instanceof OpsProviderError ? errorRate.reason : new OpsProviderError("SigNoz unreachable");
302
+ const value = (r) => r.status === "fulfilled" ? r.value : null;
303
+ return {
304
+ ...base,
305
+ configured: true,
306
+ errorRate: value(errorRate),
307
+ p95Ms: value(p95Ms),
308
+ requestRate: value(requestRate)
309
+ };
310
+ }
311
+ };
312
+ }
313
+ //#endregion
314
+ //#region src/plugins/ops/server.ts
315
+ const NO_STORE = { "cache-control": "no-store" };
316
+ function failed(error, what) {
317
+ if (error instanceof OpsProviderError) return Response.json({
318
+ ok: false,
319
+ error: error.message
320
+ }, {
321
+ status: 502,
322
+ headers: NO_STORE
323
+ });
324
+ console.error(`hud ops: ${what} failed`, error);
325
+ return Response.json({
326
+ ok: false,
327
+ error: `${what} failed`
328
+ }, {
329
+ status: 502,
330
+ headers: NO_STORE
331
+ });
332
+ }
333
+ function isRange(value) {
334
+ return TELEMETRY_RANGES.includes(value);
335
+ }
336
+ /**
337
+ * Server half of the `ops` plugin. Pass it to `createHudHandler`; each
338
+ * provider adds its route, so an app without telemetry serves no
339
+ * `/telemetry`. Tokens and keys stay in the providers: responses carry
340
+ * run data, numbers and the name of a missing env var, never a secret.
341
+ */
342
+ function ops({ ci, telemetry } = {}) {
343
+ const routes = {};
344
+ if (ci) routes["/ci"] = { GET: async () => {
345
+ try {
346
+ const [snapshot, deployment] = await Promise.all([ci.snapshot(), ci.deployment?.()]);
347
+ const body = {
348
+ ok: true,
349
+ ...snapshot,
350
+ ...ci.deployment ? { deployment: deployment ?? null } : {}
351
+ };
352
+ return Response.json(body, { headers: NO_STORE });
353
+ } catch (error) {
354
+ return failed(error, ci.name);
355
+ }
356
+ } };
357
+ if (telemetry) routes["/telemetry"] = { GET: async (request) => {
358
+ const range = new URL(request.url).searchParams.get("range") ?? "1h";
359
+ if (!isRange(range)) return Response.json({
360
+ ok: false,
361
+ error: `range must be one of ${TELEMETRY_RANGES.join(", ")}`
362
+ }, { status: 400 });
363
+ try {
364
+ const body = {
365
+ ok: true,
366
+ ...await telemetry.snapshot(range)
367
+ };
368
+ return Response.json(body, { headers: NO_STORE });
369
+ } catch (error) {
370
+ return failed(error, telemetry.name);
371
+ }
372
+ } };
373
+ return definePlugin({
374
+ id: "ops",
375
+ capability: OPS_CAPABILITY,
376
+ routes
377
+ });
378
+ }
379
+ //#endregion
380
+ export { OpsProviderError, githubActions, ops, signoz };
@@ -0,0 +1,26 @@
1
+ import { c as HudPluginProps } from "../plugins-CQ_0w1ot.js";
2
+ import { c as TelemetryRange, i as CiRun, l as TelemetryResponse, o as CiState, r as CiResponse, t as CiDeployment } from "../types-f3jlNxzw.js";
3
+ //#region src/plugins/ops/index.d.ts
4
+ declare function OpsPill({ ctx }: HudPluginProps): import("react").JSX.Element;
5
+ export interface OpsOptions {
6
+ /** Show the CI section and the CI state in the pill. Default true. */
7
+ ci?: boolean;
8
+ /** Show the telemetry section. Default true. */
9
+ telemetry?: boolean;
10
+ }
11
+ /**
12
+ * CI and telemetry: the pill shows the CI state of the branch the server runs
13
+ * on, the panel lists recent runs, the deployment (when the CI provider knows
14
+ * it) and error rate, p95 and request rate. Client half; the providers live in
15
+ * the server half, `@hansenexus/hud/plugins/ops/server`, which reads tokens
16
+ * from env and never sends them to the browser.
17
+ */
18
+ export declare function ops({ ci, telemetry }?: OpsOptions): {
19
+ readonly id: "ops";
20
+ readonly title: "Ops";
21
+ readonly capability: "cicd.read";
22
+ readonly panel: ({ ctx }: HudPluginProps) => import("react").JSX.Element;
23
+ readonly pill?: typeof OpsPill;
24
+ };
25
+ //#endregion
26
+ export type { CiDeployment, CiResponse, CiRun, CiState, TelemetryRange, TelemetryResponse };