@ninhache/probe 0.8.0 → 0.10.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.
@@ -1,13 +1,24 @@
1
1
  import { createRequire as __createRequire } from "node:module";
2
2
  const require = __createRequire(import.meta.url);
3
3
  import {
4
+ DASHBOARD_HISTORY_LENGTH,
4
5
  DASHBOARD_INTERVAL_CHOICES_SECONDS,
5
6
  DASHBOARD_TIMEOUT_CHOICES_SECONDS,
6
7
  createColors,
8
+ curlCommand,
9
+ expectProblem,
7
10
  formatCheckResults,
11
+ hostProblem,
12
+ nameProblem,
13
+ openUrl,
14
+ openerCommand,
15
+ parseExpectList,
16
+ pathProblem,
17
+ portProblem,
8
18
  runChecks,
19
+ targetUrl,
9
20
  truncate
10
- } from "./chunk-Z4F2X5IS.js";
21
+ } from "./chunk-VSQVZK7F.js";
11
22
  import {
12
23
  __commonJS,
13
24
  __export,
@@ -1768,34 +1779,34 @@ var require_react_reconciler_production = __commonJS({
1768
1779
  var fiber = workInProgress2.child;
1769
1780
  null !== fiber && (fiber.return = workInProgress2);
1770
1781
  for (; null !== fiber; ) {
1771
- var list = fiber.dependencies;
1772
- if (null !== list) {
1782
+ var list2 = fiber.dependencies;
1783
+ if (null !== list2) {
1773
1784
  var nextFiber = fiber.child;
1774
- list = list.firstContext;
1775
- a: for (; null !== list; ) {
1776
- var dependency = list;
1777
- list = fiber;
1785
+ list2 = list2.firstContext;
1786
+ a: for (; null !== list2; ) {
1787
+ var dependency = list2;
1788
+ list2 = fiber;
1778
1789
  for (var i = 0; i < contexts.length; i++)
1779
1790
  if (dependency.context === contexts[i]) {
1780
- list.lanes |= renderLanes2;
1781
- dependency = list.alternate;
1791
+ list2.lanes |= renderLanes2;
1792
+ dependency = list2.alternate;
1782
1793
  null !== dependency && (dependency.lanes |= renderLanes2);
1783
1794
  scheduleContextWorkOnParentPath(
1784
- list.return,
1795
+ list2.return,
1785
1796
  renderLanes2,
1786
1797
  workInProgress2
1787
1798
  );
1788
1799
  forcePropagateEntireTree || (nextFiber = null);
1789
1800
  break a;
1790
1801
  }
1791
- list = dependency.next;
1802
+ list2 = dependency.next;
1792
1803
  }
1793
1804
  } else if (18 === fiber.tag) {
1794
1805
  nextFiber = fiber.return;
1795
1806
  if (null === nextFiber) throw Error(formatProdErrorMessage(341));
1796
1807
  nextFiber.lanes |= renderLanes2;
1797
- list = nextFiber.alternate;
1798
- null !== list && (list.lanes |= renderLanes2);
1808
+ list2 = nextFiber.alternate;
1809
+ null !== list2 && (list2.lanes |= renderLanes2);
1799
1810
  scheduleContextWorkOnParentPath(nextFiber, renderLanes2, workInProgress2);
1800
1811
  nextFiber = null;
1801
1812
  } else
@@ -18586,7 +18597,7 @@ var Box_default = Box;
18586
18597
 
18587
18598
  // node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.1/node_modules/ink/build/components/Text.js
18588
18599
  var import_react12 = __toESM(require_react(), 1);
18589
- function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
18600
+ function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap: wrap2 = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
18590
18601
  const { isScreenReaderEnabled } = (0, import_react12.use)(AccessibilityContext);
18591
18602
  const inheritedBackgroundColor = (0, import_react12.use)(BackgroundContext);
18592
18603
  const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
@@ -18624,7 +18635,7 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
18624
18635
  flexGrow: 0,
18625
18636
  flexShrink: 1,
18626
18637
  flexDirection: "row",
18627
- textWrap: wrap
18638
+ textWrap: wrap2
18628
18639
  }, internal_transform: isScreenReaderEnabled ? void 0 : transform }, childrenOrAriaLabel)
18629
18640
  );
18630
18641
  }
@@ -20243,11 +20254,368 @@ var use_window_size_default = useWindowSize;
20243
20254
  // node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.1/node_modules/ink/build/hooks/use-box-metrics.js
20244
20255
  var import_react35 = __toESM(require_react(), 1);
20245
20256
 
20257
+ // src/clipboard.ts
20258
+ import { spawn as nodeSpawn } from "child_process";
20259
+ var WL_COPY = ["wl-copy"];
20260
+ var XCLIP = ["xclip", "-selection", "clipboard"];
20261
+ function clipboardTools(platform2, env3) {
20262
+ if (platform2 === "darwin") return [["pbcopy"]];
20263
+ return env3.WAYLAND_DISPLAY ? [WL_COPY, XCLIP] : [XCLIP, WL_COPY];
20264
+ }
20265
+ function copiedWith([command, ...args], text, spawn) {
20266
+ return new Promise((resolve) => {
20267
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
20268
+ child.once("error", () => resolve(false));
20269
+ child.once("exit", (code) => resolve(code === 0));
20270
+ child.stdin?.on("error", () => {
20271
+ });
20272
+ child.stdin?.end(text);
20273
+ });
20274
+ }
20275
+ async function copyText(text, platform2 = process.platform, env3 = process.env, spawn = nodeSpawn) {
20276
+ for (const tool of clipboardTools(platform2, env3)) {
20277
+ if (await copiedWith(tool, text, spawn)) return true;
20278
+ }
20279
+ return false;
20280
+ }
20281
+
20282
+ // src/dashboard/actions.ts
20283
+ var system = () => ({
20284
+ platform: process.platform,
20285
+ env: process.env,
20286
+ openUrl,
20287
+ copyText
20288
+ });
20289
+ function serviceActions(config, deps = system()) {
20290
+ const service = (name) => config().services[name];
20291
+ const gone = (name) => `${name} is no longer in the config`;
20292
+ return {
20293
+ async onOpen(name) {
20294
+ const found = service(name);
20295
+ if (!found) return gone(name);
20296
+ const url = targetUrl(found);
20297
+ if (await deps.openUrl(url)) return `Opened ${url}`;
20298
+ return `${openerCommand(deps.platform)} not found. The URL:
20299
+ ${url}`;
20300
+ },
20301
+ async onCopy(name, timeout) {
20302
+ const found = service(name);
20303
+ if (!found) return gone(name);
20304
+ const command = curlCommand({ ...found, timeout });
20305
+ if (await deps.copyText(command)) return `Copied: ${command}`;
20306
+ const tools = clipboardTools(deps.platform, deps.env).map(([tool]) => tool);
20307
+ return `No ${tools.join(" or ")} to copy with. The command:
20308
+ ${command}`;
20309
+ }
20310
+ };
20311
+ }
20312
+
20246
20313
  // src/dashboard/Dashboard.tsx
20247
20314
  var import_react36 = __toESM(require_react(), 1);
20248
20315
 
20316
+ // src/dashboard/detail.ts
20317
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
20318
+ var HEADER_LINES = 6;
20319
+ var BODY_LINES = 5;
20320
+ var HINT_LINES = 3;
20321
+ var TRANSPORT_HEADERS = /* @__PURE__ */ new Set([
20322
+ "connection",
20323
+ "content-length",
20324
+ "date",
20325
+ "keep-alive",
20326
+ "transfer-encoding"
20327
+ ]);
20328
+ function recordLatencies(history, results) {
20329
+ return Object.fromEntries(
20330
+ results.map(({ name, status, skipped, latencyMs }) => {
20331
+ const before = history[name] ?? [];
20332
+ const answered = status !== void 0 && !skipped;
20333
+ return [name, answered ? [...before, latencyMs].slice(-DASHBOARD_HISTORY_LENGTH) : before];
20334
+ })
20335
+ );
20336
+ }
20337
+ function sparkline(values, unicode, width) {
20338
+ if (values.length === 0) return "";
20339
+ const low = Math.min(...values);
20340
+ const high = Math.max(...values);
20341
+ const range = low === high ? `${low}ms` : `${low}${unicode ? "\u2013" : "-"}${high}ms`;
20342
+ if (!unicode) return `${range} over ${values.length} ${values.length === 1 ? "check" : "checks"}`;
20343
+ const room = width - range.length - 1;
20344
+ if (room < 1) return range;
20345
+ const top = BLOCKS.length - 1;
20346
+ const bars = values.slice(-room).map((value) => BLOCKS[high === low ? 0 : Math.round((value - low) / (high - low) * top)]).join("");
20347
+ return `${bars} ${range}`;
20348
+ }
20349
+ function wrap(text, width, maxLines) {
20350
+ const lines = [];
20351
+ let line = "";
20352
+ for (const word of text.split(" ")) {
20353
+ if (line && line.length + 1 + word.length > width) {
20354
+ lines.push(line);
20355
+ line = word;
20356
+ } else line = line ? `${line} ${word}` : word;
20357
+ }
20358
+ lines.push(line);
20359
+ const kept = lines.slice(0, maxLines).map((kept2) => truncate(kept2, width));
20360
+ if (lines.length > maxLines) kept[maxLines - 1] = truncate(`${kept[maxLines - 1]} \u2026`, width);
20361
+ return kept;
20362
+ }
20363
+ function detailLines(result, history, { unicode, width, room }) {
20364
+ const dot = unicode ? "\xB7" : "-";
20365
+ const more = unicode ? "\u2026" : "...";
20366
+ const cut = (text) => truncate(text, width);
20367
+ if (result.skipped) return [{ id: "status", text: cut("skipped: not checked"), tone: "dim" }];
20368
+ const { response, status } = result;
20369
+ const tone = result.ok ? "good" : "bad";
20370
+ const lines = [];
20371
+ if (status === void 0) lines.push({ id: "status", text: cut(result.error ?? "down"), tone });
20372
+ else {
20373
+ const answer = `${status} ${response?.statusText ?? ""}`.trim();
20374
+ lines.push({ id: "status", text: cut(`${answer} ${dot} ${result.latencyMs}ms`), tone });
20375
+ if (result.error) lines.push({ id: "error", text: cut(result.error), tone });
20376
+ }
20377
+ if (result.hint) {
20378
+ for (const [index, text] of wrap(result.hint, width, HINT_LINES).entries())
20379
+ lines.push({ id: `hint ${index}`, text, tone: "dim" });
20380
+ }
20381
+ const latencies = sparkline(history, unicode, width);
20382
+ if (latencies) lines.push({ id: "latencies", text: cut(latencies), tone: "dim" });
20383
+ if (!response) return lines;
20384
+ const transport = ([name]) => TRANSPORT_HEADERS.has(name.toLowerCase());
20385
+ const headers = [
20386
+ ...response.headers.filter((header) => !transport(header)),
20387
+ ...response.headers.filter(transport)
20388
+ ].map(([name, value]) => `${name}: ${value}`);
20389
+ const body = response.bodyNote ? [response.bodyNote] : response.body?.split("\n") ?? [];
20390
+ let headerRoom = Math.min(headers.length, HEADER_LINES);
20391
+ let bodyRoom = Math.min(body.length, BODY_LINES);
20392
+ const left = Math.max(0, room - lines.length);
20393
+ while (headerRoom + bodyRoom > left) {
20394
+ if (bodyRoom > 1 && bodyRoom >= headerRoom) bodyRoom -= 1;
20395
+ else if (headerRoom > 0) headerRoom -= 1;
20396
+ else bodyRoom -= 1;
20397
+ }
20398
+ const hiddenHeaders = headers.length - headerRoom;
20399
+ for (const [index, header] of headers.slice(0, headerRoom).entries()) {
20400
+ const last = index === headerRoom - 1 && hiddenHeaders > 0;
20401
+ lines.push(
20402
+ last ? {
20403
+ id: `header ${index}`,
20404
+ text: cut(`${more} ${hiddenHeaders + 1} more headers`),
20405
+ tone: "dim"
20406
+ } : { id: `header ${index}`, text: cut(header), tone: "dim" }
20407
+ );
20408
+ }
20409
+ const cutBody = body.length > bodyRoom || response.bodyCut;
20410
+ for (const [index, text] of body.slice(0, bodyRoom).entries()) {
20411
+ const last = index === bodyRoom - 1 && cutBody;
20412
+ lines.push({
20413
+ id: `body ${index}`,
20414
+ text: last ? `${truncate(text, Math.max(1, width - more.length - 1))} ${more}`.trim() : cut(text),
20415
+ tone: response.bodyNote ? "dim" : void 0
20416
+ });
20417
+ }
20418
+ return lines;
20419
+ }
20420
+
20421
+ // src/dashboard/form.ts
20422
+ var ROWS = [
20423
+ { key: "name", label: "Name", placeholder: "api" },
20424
+ { key: "port", label: "Port", placeholder: "3000" },
20425
+ { key: "host", label: "Host", placeholder: "localhost" },
20426
+ { key: "path", label: "Path", placeholder: "/" },
20427
+ { key: "expect", label: "Expect", placeholder: "any 2xx" }
20428
+ ];
20429
+ var EMPTY_FORM = { name: "", port: "", host: "", path: "", expect: "" };
20430
+ function formRows(adding) {
20431
+ return adding ? ROWS : ROWS.filter(({ key }) => key !== "name");
20432
+ }
20433
+ function formValues({ host, port, path, expect }) {
20434
+ return { name: "", port: String(port), host, path, expect: [expect ?? []].flat().join(",") };
20435
+ }
20436
+ function formFields(values) {
20437
+ return {
20438
+ port: Number(values.port),
20439
+ host: values.host || "localhost",
20440
+ path: values.path || "/",
20441
+ expect: values.expect ? parseExpectList(values.expect) : void 0
20442
+ };
20443
+ }
20444
+ function formProblem(rows, values, taken) {
20445
+ const problems = {
20446
+ name: () => nameProblem(values.name, taken),
20447
+ port: () => portProblem(values.port),
20448
+ host: () => values.host ? hostProblem(values.host) : void 0,
20449
+ path: () => values.path ? pathProblem(values.path) : void 0,
20450
+ expect: () => values.expect ? expectProblem(values.expect) : void 0
20451
+ };
20452
+ for (const [row, { key }] of rows.entries()) {
20453
+ const message = problems[key]();
20454
+ if (message) return { row, message };
20455
+ }
20456
+ return void 0;
20457
+ }
20458
+
20459
+ // src/dashboard/keymap.ts
20460
+ var list = (binding) => ({
20461
+ screens: ["list"],
20462
+ section: "List",
20463
+ ...binding
20464
+ });
20465
+ var BINDINGS = [
20466
+ list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop: 2 }),
20467
+ list({ keys: "r", action: "check", help: "Check now", barless: true, command: "check" }),
20468
+ list({
20469
+ keys: "\u21B5",
20470
+ ascii: "enter",
20471
+ action: "service",
20472
+ help: "Service detail",
20473
+ drop: 5,
20474
+ command: "detail"
20475
+ }),
20476
+ list({ keys: "u", action: "open", help: "Open its URL", barless: true, command: "open" }),
20477
+ list({ keys: "c", action: "copy", help: "Copy curl command", barless: true, command: "copy" }),
20478
+ list({ keys: "e", action: "edit", help: "Edit the service", barless: true, command: "edit" }),
20479
+ list({ keys: "d", action: "remove", help: "Remove it", barless: true, command: "remove" }),
20480
+ list({ keys: "a", action: "add", help: "Add a service", drop: 3, command: "add" }),
20481
+ list({ keys: "/", action: "filter", help: "Filter by name", drop: 4, command: "filter" }),
20482
+ list({ keys: ":", action: "commands", help: "Find a command", drop: 1 }),
20483
+ list({ keys: "o", action: "options", help: "Options", barless: true, command: "options" }),
20484
+ {
20485
+ screens: ["panel"],
20486
+ section: "Panels",
20487
+ keys: "\u2191\u2193 jk",
20488
+ ascii: "jk",
20489
+ action: "move",
20490
+ help: "Move",
20491
+ drop: 1
20492
+ },
20493
+ {
20494
+ screens: ["panel"],
20495
+ section: "Panels",
20496
+ keys: "\u2190\u2192 hl",
20497
+ ascii: "hl",
20498
+ action: "change",
20499
+ help: "Change the value",
20500
+ drop: 2
20501
+ },
20502
+ { screens: ["panel"], section: "Panels", keys: "esc", action: "close", help: "Close" },
20503
+ {
20504
+ screens: ["filter"],
20505
+ section: "Filter",
20506
+ keys: "\u2191\u2193",
20507
+ ascii: "arrows",
20508
+ action: "move",
20509
+ help: "Move",
20510
+ drop: 1
20511
+ },
20512
+ {
20513
+ screens: ["filter"],
20514
+ section: "Filter",
20515
+ keys: "\u21B5",
20516
+ ascii: "enter",
20517
+ action: "keep",
20518
+ help: "Keep the filter"
20519
+ },
20520
+ { screens: ["filter"], section: "Filter", keys: "esc", action: "clear", help: "Clear it" },
20521
+ { screens: ["palette"], keys: "\u2191\u2193", ascii: "arrows", action: "move", drop: 1 },
20522
+ { screens: ["palette"], keys: "\u21B5", ascii: "enter", action: "run" },
20523
+ { screens: ["palette"], keys: "esc", action: "close" },
20524
+ {
20525
+ screens: ["form"],
20526
+ section: "Add and edit",
20527
+ keys: "\u2191\u2193 tab",
20528
+ ascii: "tab",
20529
+ action: "field",
20530
+ help: "Change field",
20531
+ drop: 1
20532
+ },
20533
+ {
20534
+ screens: ["form"],
20535
+ section: "Add and edit",
20536
+ keys: "\u21B5",
20537
+ ascii: "enter",
20538
+ action: "save",
20539
+ help: "Save"
20540
+ },
20541
+ { screens: ["form"], section: "Add and edit", keys: "esc", action: "cancel", help: "Cancel" },
20542
+ { screens: ["confirm"], keys: "y", action: "remove" },
20543
+ { screens: ["confirm"], keys: "n esc", action: "keep" },
20544
+ { screens: ["help"], keys: "? esc", action: "close" },
20545
+ {
20546
+ screens: ["list", "panel"],
20547
+ section: "Outside a text field",
20548
+ keys: "?",
20549
+ action: "help",
20550
+ help: "This help",
20551
+ command: "help"
20552
+ },
20553
+ {
20554
+ screens: ["list", "panel", "help"],
20555
+ section: "Outside a text field",
20556
+ keys: "q",
20557
+ action: "quit",
20558
+ help: "Quit",
20559
+ command: "quit"
20560
+ }
20561
+ ];
20562
+ var SECTIONS = [
20563
+ "List",
20564
+ "Panels",
20565
+ "Filter",
20566
+ "Add and edit",
20567
+ "Outside a text field"
20568
+ ];
20569
+ var written = (binding, unicode) => unicode ? binding.keys : binding.ascii ?? binding.keys;
20570
+ function helpBar(screen, unicode, columns) {
20571
+ const shown = BINDINGS.filter(({ screens, barless }) => screens.includes(screen) && !barless);
20572
+ const line = () => shown.map((binding) => `${written(binding, unicode)} ${binding.action}`).join(unicode ? " \xB7 " : " - ");
20573
+ while (line().length > columns) {
20574
+ const droppable = shown.filter(({ drop }) => drop !== void 0);
20575
+ if (droppable.length === 0) break;
20576
+ const first = droppable.reduce((a, b) => a.drop <= b.drop ? a : b);
20577
+ shown.splice(shown.indexOf(first), 1);
20578
+ }
20579
+ return line();
20580
+ }
20581
+ function keyCommand(input, enter) {
20582
+ return BINDINGS.find(({ keys, command }) => command && (enter ? keys === "\u21B5" : keys === input))?.command;
20583
+ }
20584
+ function paletteEntries(unicode, query = "") {
20585
+ const wanted = query.trim().toLowerCase();
20586
+ return BINDINGS.flatMap((binding) => {
20587
+ const { command, help, action } = binding;
20588
+ if (!command || !help || !`${help} ${action}`.toLowerCase().includes(wanted)) return [];
20589
+ return [{ command, label: help, keys: written(binding, unicode) }];
20590
+ });
20591
+ }
20592
+ var HELP_GAP = 3;
20593
+ function sectionLines(sections, unicode) {
20594
+ const described = BINDINGS.filter(({ help }) => help !== void 0);
20595
+ const width = Math.max(...described.map((binding) => written(binding, unicode).length));
20596
+ return sections.flatMap((title) => [
20597
+ { id: title, text: title, heading: true },
20598
+ ...described.filter(({ section }) => section === title).map((binding) => ({
20599
+ id: `${title} ${binding.keys}`,
20600
+ text: ` ${written(binding, unicode).padEnd(width)} ${binding.help}`,
20601
+ heading: false
20602
+ }))
20603
+ ]);
20604
+ }
20605
+ function helpLines(unicode) {
20606
+ return sectionLines(SECTIONS, unicode);
20607
+ }
20608
+ function helpColumns(unicode, columns) {
20609
+ const [first, ...others] = SECTIONS;
20610
+ const sides = [sectionLines([first], unicode), sectionLines(others, unicode)];
20611
+ const widths = sides.map((side) => Math.max(...side.map(({ text }) => text.length)));
20612
+ const fits = widths[0] + HELP_GAP + widths[1] <= columns;
20613
+ return fits ? sides : [helpLines(unicode)];
20614
+ }
20615
+
20249
20616
  // src/dashboard/keys.ts
20250
20617
  var ESCAPE2 = String.fromCharCode(27);
20618
+ var DELETE = String.fromCharCode(127);
20251
20619
  var ARROWS = { A: "up", B: "down", C: "right", D: "left" };
20252
20620
  var KEY_PATTERN = new RegExp(`${ESCAPE2}\\[([ABCD])|[\\s\\S]`, "g");
20253
20621
  function named(key) {
@@ -20257,8 +20625,13 @@ function named(key) {
20257
20625
  if (key.rightArrow) return "right";
20258
20626
  if (key.return) return "return";
20259
20627
  if (key.escape) return "escape";
20628
+ if (key.backspace || key.delete) return "backspace";
20629
+ if (key.tab) return "tab";
20260
20630
  return void 0;
20261
20631
  }
20632
+ function typedCharacter({ input, key }) {
20633
+ return key === void 0 && input.length === 1 && input >= " " && input !== DELETE ? input : void 0;
20634
+ }
20262
20635
  function splitKeys(input, key) {
20263
20636
  const name = named(key);
20264
20637
  if (name || input.length <= 1) return [{ input, key: name }];
@@ -20266,6 +20639,8 @@ function splitKeys(input, key) {
20266
20639
  if (arrow) return { input: "", key: ARROWS[arrow] };
20267
20640
  if (text === "\r" || text === "\n") return { input: "", key: "return" };
20268
20641
  if (text === ESCAPE2) return { input: "", key: "escape" };
20642
+ if (text === DELETE || text === "\b") return { input: "", key: "backspace" };
20643
+ if (text === " ") return { input: "", key: "tab" };
20269
20644
  return { input: text };
20270
20645
  });
20271
20646
  }
@@ -20294,8 +20669,17 @@ var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
20294
20669
  var NO_TIMEOUTS = {};
20295
20670
  var MARKER_WIDTH = 2;
20296
20671
  var PANEL_WIDTH = 30;
20672
+ var DETAIL_WIDTH = 60;
20673
+ var DEFAULT_ROWS = 24;
20674
+ var FORM_WIDTH = 48;
20675
+ var CURSOR = "_";
20297
20676
  var PANEL_FRAME_WIDTH = 4;
20298
20677
  var seconds = (ms) => `${ms / 1e3}s`;
20678
+ function ending(text, max) {
20679
+ const characters = Array.from(text);
20680
+ if (characters.length <= max) return text;
20681
+ return max <= 1 ? "\u2026" : `\u2026${characters.slice(characters.length - max + 1).join("")}`;
20682
+ }
20299
20683
  function Dashboard({
20300
20684
  terminal,
20301
20685
  settings: initial,
@@ -20304,17 +20688,29 @@ function Dashboard({
20304
20688
  check: check2,
20305
20689
  onSettingsChange,
20306
20690
  onSkipChange,
20307
- onTimeoutChange
20691
+ onTimeoutChange,
20692
+ onOpen,
20693
+ onCopy,
20694
+ serviceFields,
20695
+ onServiceWrite
20308
20696
  }) {
20309
20697
  const { exit } = useApp();
20310
- const { columns } = use_window_size_default();
20698
+ const { columns, rows: windowRows } = use_window_size_default();
20311
20699
  const [latest, setLatest] = (0, import_react36.useState)(null);
20700
+ const [history, setHistory] = (0, import_react36.useState)({});
20312
20701
  const [selected, setSelected] = (0, import_react36.useState)(0);
20313
20702
  const [settings, setSettings] = (0, import_react36.useState)(initial);
20314
20703
  const [skipped, setSkipped] = (0, import_react36.useState)(() => new Set(initiallySkipped));
20315
20704
  const [timeouts, setTimeouts] = (0, import_react36.useState)(initialTimeouts);
20316
20705
  const [panel, setPanel] = (0, import_react36.useState)(null);
20706
+ const [dialog, setDialog] = (0, import_react36.useState)(null);
20707
+ const [filter, setFilter] = (0, import_react36.useState)("");
20708
+ const [filtering, setFiltering] = (0, import_react36.useState)(false);
20709
+ const [palette, setPalette] = (0, import_react36.useState)(null);
20710
+ const [writes, setWrites] = (0, import_react36.useState)(0);
20711
+ const [help, setHelp] = (0, import_react36.useState)(false);
20317
20712
  const [unsaved, setUnsaved] = (0, import_react36.useState)();
20713
+ const [notice, setNotice] = (0, import_react36.useState)();
20318
20714
  const skipPause = (0, import_react36.useRef)(() => {
20319
20715
  });
20320
20716
  const track = (saved) => {
@@ -20329,6 +20725,7 @@ function Dashboard({
20329
20725
  const next = await check2(signal, timeoutMs, skipped, timeouts);
20330
20726
  if (signal.aborted) return;
20331
20727
  setLatest(next);
20728
+ setHistory((before) => recordLatencies(before, next));
20332
20729
  await new Promise((resolve) => {
20333
20730
  const timer = setTimeout(resolve, intervalMs);
20334
20731
  skipPause.current = () => {
@@ -20341,9 +20738,59 @@ function Dashboard({
20341
20738
  };
20342
20739
  void loop();
20343
20740
  return () => controller.abort();
20344
- }, [check2, intervalMs, timeoutMs, skipped, timeouts]);
20345
- const last = (latest?.length ?? 1) - 1;
20741
+ }, [check2, intervalMs, timeoutMs, skipped, timeouts, writes]);
20742
+ const wanted = filter.toLowerCase();
20743
+ const shown = (latest ?? []).filter(({ name }) => name.toLowerCase().includes(wanted));
20744
+ const last = Math.max(0, shown.length - 1);
20346
20745
  const current = Math.min(selected, last);
20746
+ const write = async (change2, done) => {
20747
+ const outcome = await onServiceWrite?.(change2);
20748
+ if (!outcome) return;
20749
+ setUnsaved(outcome.failure);
20750
+ if (!outcome.saved) return;
20751
+ setNotice(outcome.warning ? `${done}
20752
+ ${outcome.warning}` : done);
20753
+ if (change2.kind === "remove") {
20754
+ setLatest((results2) => results2?.filter(({ name }) => name !== change2.name) ?? null);
20755
+ setTimeouts(({ [change2.name]: _, ...others }) => others);
20756
+ }
20757
+ setWrites((count) => count + 1);
20758
+ };
20759
+ const typeInForm = (form, keystroke) => {
20760
+ const { key } = keystroke;
20761
+ const rows2 = formRows(form.editing === void 0);
20762
+ const field = rows2[form.row]?.key;
20763
+ const typed = typedCharacter(keystroke);
20764
+ const edit = (value) => field && setDialog({ ...form, values: { ...form.values, [field]: value }, problem: void 0 });
20765
+ if (key === "escape") setDialog(null);
20766
+ else if (key === "up") setDialog({ ...form, row: Math.max(0, form.row - 1) });
20767
+ else if (key === "down") setDialog({ ...form, row: Math.min(rows2.length - 1, form.row + 1) });
20768
+ else if (key === "tab") setDialog({ ...form, row: (form.row + 1) % rows2.length });
20769
+ else if (key === "backspace" && field) edit(form.values[field].slice(0, -1));
20770
+ else if (typed && field) edit(form.values[field] + typed);
20771
+ else if (key === "return") {
20772
+ const taken2 = (latest ?? []).map(({ name }) => name);
20773
+ const problem = formProblem(rows2, form.values, taken2);
20774
+ if (problem) return setDialog({ ...form, row: problem.row, problem: problem.message });
20775
+ setDialog(null);
20776
+ const fields = formFields(form.values);
20777
+ if (form.editing === void 0)
20778
+ void write({ kind: "add", name: form.values.name, fields }, `Added ${form.values.name}`);
20779
+ else void write({ kind: "edit", name: form.editing, fields }, `Saved ${form.editing}`);
20780
+ }
20781
+ };
20782
+ const typeFilter = (keystroke) => {
20783
+ const { key } = keystroke;
20784
+ const typed = typedCharacter(keystroke);
20785
+ if (key === "escape") {
20786
+ setFilter("");
20787
+ setFiltering(false);
20788
+ } else if (key === "return") setFiltering(false);
20789
+ else if (key === "backspace") setFilter(filter.slice(0, -1));
20790
+ else if (key === "up") setSelected(Math.max(0, current - 1));
20791
+ else if (key === "down") setSelected(Math.min(last, current + 1));
20792
+ else if (typed) setFilter(filter + typed);
20793
+ };
20347
20794
  const change = (rowIndex, direction) => {
20348
20795
  const row = SETTING_ROWS[rowIndex];
20349
20796
  if (!row) return;
@@ -20366,18 +20813,78 @@ function Dashboard({
20366
20813
  setTimeouts({ ...timeouts, [name]: value });
20367
20814
  track(onTimeoutChange?.(name, value));
20368
20815
  };
20369
- const press = ({ input, key }) => {
20816
+ const run = (command) => {
20817
+ const name = shown[current]?.name;
20818
+ switch (command) {
20819
+ case "check":
20820
+ return skipPause.current();
20821
+ case "options":
20822
+ return setPanel({ kind: "options", row: 0 });
20823
+ case "detail":
20824
+ return name ? setPanel({ kind: "service", name, row: 0 }) : void 0;
20825
+ case "open":
20826
+ return name ? void onOpen?.(name).then(setNotice) : void 0;
20827
+ case "copy":
20828
+ return name ? void onCopy?.(name, timeouts[name]).then(setNotice) : void 0;
20829
+ case "filter":
20830
+ return setFiltering(true);
20831
+ case "add":
20832
+ return onServiceWrite ? setDialog({ kind: "form", row: 0, values: EMPTY_FORM }) : void 0;
20833
+ case "edit": {
20834
+ const fields = onServiceWrite && name ? serviceFields?.(name) : void 0;
20835
+ if (fields) setDialog({ kind: "form", editing: name, row: 0, values: formValues(fields) });
20836
+ return;
20837
+ }
20838
+ case "remove":
20839
+ return onServiceWrite && name ? setDialog({ kind: "confirm", name }) : void 0;
20840
+ case "help":
20841
+ return setHelp(true);
20842
+ case "quit":
20843
+ return exit();
20844
+ default:
20845
+ return command;
20846
+ }
20847
+ };
20848
+ const typePalette = ({ query, row }, keystroke) => {
20849
+ const { key } = keystroke;
20850
+ const typed = typedCharacter(keystroke);
20851
+ const entries = paletteEntries(terminal.unicode, query);
20852
+ if (key === "escape") setPalette(null);
20853
+ else if (key === "return") {
20854
+ const entry = entries[row];
20855
+ if (!entry) return;
20856
+ setPalette(null);
20857
+ run(entry.command);
20858
+ } else if (key === "up") setPalette({ query, row: Math.max(0, row - 1) });
20859
+ else if (key === "down")
20860
+ setPalette({ query, row: Math.max(0, Math.min(entries.length - 1, row + 1)) });
20861
+ else if (key === "backspace") setPalette({ query: query.slice(0, -1), row: 0 });
20862
+ else if (typed) setPalette({ query: query + typed, row: 0 });
20863
+ };
20864
+ const press = (keystroke) => {
20865
+ const { input, key } = keystroke;
20370
20866
  const up2 = key === "up" || input === "k";
20371
20867
  const down = key === "down" || input === "j";
20372
20868
  const left = key === "left" || input === "h";
20373
20869
  const right = key === "right" || input === "l";
20374
20870
  const enter = key === "return";
20375
- const selectedName = latest?.[current]?.name;
20376
- if (input === "q") exit();
20871
+ setNotice(void 0);
20872
+ if (dialog?.kind === "form") typeInForm(dialog, keystroke);
20873
+ else if (dialog) {
20874
+ setDialog(null);
20875
+ if (input === "y")
20876
+ void write({ kind: "remove", name: dialog.name }, `Removed ${dialog.name}`);
20877
+ } else if (palette) typePalette(palette, keystroke);
20878
+ else if (filtering) typeFilter(keystroke);
20879
+ else if (input === "q") exit();
20880
+ else if (help) {
20881
+ if (input === "?" || key === "escape") setHelp(false);
20882
+ } else if (input === "?") setHelp(true);
20377
20883
  else if (panel === null) {
20378
- if (input === "r") skipPause.current();
20379
- else if (input === "o") setPanel({ kind: "options", row: 0 });
20380
- else if (enter && selectedName) setPanel({ kind: "service", name: selectedName, row: 0 });
20884
+ const command = keyCommand(input, enter);
20885
+ if (command) run(command);
20886
+ else if (input === ":") setPalette({ query: "", row: 0 });
20887
+ else if (key === "escape") setFilter("");
20381
20888
  else if (up2) setSelected(Math.max(0, current - 1));
20382
20889
  else if (down) setSelected(Math.min(last, current + 1));
20383
20890
  } else if (key === "escape" || (panel.kind === "options" ? input === "o" : enter))
@@ -20419,27 +20926,61 @@ function Dashboard({
20419
20926
  const dot = unicode ? "\xB7" : "-";
20420
20927
  const marker = unicode ? "\u203A " : "> ";
20421
20928
  const cut = (text) => truncate(text, Math.max(1, columns));
20929
+ if (help) {
20930
+ const [left = [], right = []] = helpColumns(unicode, columns);
20931
+ const leftWidth = Math.max(...left.map(({ text }) => text.length));
20932
+ const paint = (line) => line?.heading ? colors.bold(cut(line.text)) : cut(line?.text ?? "");
20933
+ const lines = Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
20934
+ const near = left[index];
20935
+ const far = right[index];
20936
+ const gap = " ".repeat(leftWidth - (near?.text.length ?? 0) + HELP_GAP);
20937
+ return {
20938
+ id: near?.id ?? far?.id,
20939
+ text: far ? `${paint(near)}${gap}${paint(far)}` : paint(near)
20940
+ };
20941
+ });
20942
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "column", children: [
20943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(`probe ${dot} keys`)) }),
20944
+ lines.map(({ id, text }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: text }, id)),
20945
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(helpBar("help", unicode, columns))) })
20946
+ ] });
20947
+ }
20422
20948
  if (!latest) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut("probe: checking\u2026") });
20423
- const results = latest.map((result) => {
20949
+ const asShown = (result) => {
20424
20950
  if (result.skipped || !skipped.has(result.name)) return result;
20425
20951
  const { name, type, target } = result;
20426
20952
  return { name, type, target, ok: true, skipped: true, latencyMs: 0 };
20427
- });
20428
- const checked = results.filter((result) => !result.skipped);
20429
- const up = checked.filter((result) => result.ok).length;
20430
- const aside = results.length - checked.length;
20953
+ };
20954
+ const results = shown.map(asShown);
20955
+ const all = latest.filter((result) => !result.skipped && !skipped.has(result.name));
20956
+ const up = all.filter((result) => result.ok).length;
20957
+ const aside = latest.length - all.length;
20431
20958
  const summary = [
20432
20959
  "probe",
20433
- `${up}/${checked.length} up`,
20960
+ `${up}/${all.length} up`,
20434
20961
  ...aside > 0 ? [`${aside} skipped`] : [],
20435
- `every ${seconds(intervalMs)}`
20962
+ `every ${seconds(intervalMs)}`,
20963
+ ...filter && !filtering ? [`filter: ${filter}`] : []
20436
20964
  ].join(` ${dot} `);
20437
- const rows = formatCheckResults(results, {
20438
- ...terminal,
20439
- columns: Math.max(1, columns - MARKER_WIDTH)
20440
- }).split("\n");
20965
+ const rows = results.length === 0 ? [] : (
20966
+ // One line per service: the hint of the selected one goes under the list.
20967
+ formatCheckResults(
20968
+ results.map(({ hint: _, ...result }) => result),
20969
+ { ...terminal, columns: Math.max(1, columns - MARKER_WIDTH) }
20970
+ ).split("\n")
20971
+ );
20972
+ const empty = filter ? `no service matches "${filter}"` : onServiceWrite ? "no service: press a to add one" : "no service";
20441
20973
  const focused = results[current];
20442
- const panelWidth = Math.min(PANEL_WIDTH, columns);
20974
+ const panelWidth = Math.min(panel?.kind === "service" ? DETAIL_WIDTH : PANEL_WIDTH, columns);
20975
+ const panelRoom = Math.max(1, panelWidth - PANEL_FRAME_WIDTH);
20976
+ const detailed = panel?.kind === "service" ? latest.find(({ name }) => name === panel.name) : void 0;
20977
+ const taken = 1 + Math.max(1, rows.length) + 1 + 2 + 1 + 1 + SERVICE_ROWS.length + 1 + 1;
20978
+ const detail = detailed ? detailLines(asShown(detailed), history[detailed.name] ?? [], {
20979
+ unicode,
20980
+ width: panelRoom,
20981
+ room: (windowRows || DEFAULT_ROWS) - taken - (unsaved === void 0 ? 0 : 1)
20982
+ }) : [];
20983
+ const toned = ({ text, tone }) => tone === "good" ? colors.green(text) : tone === "bad" ? colors.red(text) : tone === "dim" ? colors.dim(text) : text;
20443
20984
  const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? [
20444
20985
  ["Skip", skipped.has(panel.name) ? "yes" : "no"],
20445
20986
  [
@@ -20448,16 +20989,29 @@ function Dashboard({
20448
20989
  ]
20449
20990
  ] : [];
20450
20991
  const labelWidth = Math.max(0, ...panelRows.map(([label]) => label.length));
20451
- const hint = panel === null ? `${unicode ? "\u2191\u2193 jk" : "jk"} move ${dot} r refresh ${dot} ${unicode ? "\u21B5" : "enter"} service ${dot} o options ${dot} q quit` : `${unicode ? "\u2191\u2193 jk" : "jk"} move ${dot} ${unicode ? "\u2190\u2192 hl" : "hl"} change ${dot} esc close`;
20992
+ const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : palette ? "palette" : filtering ? "filter" : panel ? "panel" : "list";
20993
+ const hint = helpBar(screen, unicode, columns);
20994
+ const commands = palette ? paletteEntries(unicode, palette.query) : [];
20995
+ const commandWidth = Math.max(0, ...commands.map(({ label }) => label.length));
20996
+ const paletteRoom = Math.max(
20997
+ 1,
20998
+ (windowRows || DEFAULT_ROWS) - (1 + Math.max(1, rows.length) + (focused ? 1 : 0) + (focused?.hint ? 1 : 0) + 1 + 1 + 1) - (unsaved === void 0 ? 0 : 1)
20999
+ );
21000
+ const firstCommand = Math.max(0, (palette?.row ?? 0) - paletteRoom + 1);
21001
+ const formWidth = Math.min(FORM_WIDTH, columns);
21002
+ const formLabels = dialog?.kind === "form" ? formRows(dialog.editing === void 0) : [];
21003
+ const formLabelWidth = Math.max(0, ...formLabels.map(({ label }) => label.length));
20452
21004
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "column", children: [
20453
21005
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(summary)) }),
20454
21006
  rows.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
20455
21007
  index === current ? marker : " ",
20456
21008
  row
20457
21009
  ] }, results[index]?.name)),
21010
+ rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(empty)) }),
20458
21011
  focused && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(
20459
21012
  cut(focused.error ? `${focused.target} ${dot} ${focused.error}` : focused.target)
20460
21013
  ) }),
21014
+ focused?.hint && panel?.kind !== "service" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(focused.hint)) }),
20461
21015
  panel !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
20462
21016
  Box_default,
20463
21017
  {
@@ -20465,20 +21019,55 @@ function Dashboard({
20465
21019
  width: panelWidth,
20466
21020
  borderStyle: unicode ? "single" : "classic",
20467
21021
  paddingX: 1,
21022
+ children: [
21023
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(truncate(panel.kind === "options" ? "Options" : panel.name, panelRoom)) }),
21024
+ detail.map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: toned(line) }, line.id)),
21025
+ detail.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim((unicode ? "\u2500" : "-").repeat(panelRoom)) }),
21026
+ panelRows.map(([label, value], index) => {
21027
+ const line = `${index === panel.row ? marker : " "}${label.padEnd(labelWidth)} ${unicode ? `\u2039 ${value} \u203A` : `< ${value} >`}`;
21028
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, panelRoom) }, label);
21029
+ })
21030
+ ]
21031
+ }
21032
+ ),
21033
+ filtering && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`Filter: ${filter}${CURSOR}`) }),
21034
+ palette && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`: ${palette.query}${CURSOR}`) }),
21035
+ palette && commands.slice(firstCommand, firstCommand + paletteRoom).map(({ command, label, keys }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(
21036
+ `${commands[palette.row]?.command === command ? marker : " "}${label.padEnd(commandWidth)} ${keys}`
21037
+ ) }, command)),
21038
+ palette && commands.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(`no command matches "${palette.query}"`)) }),
21039
+ dialog?.kind === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(`Remove ${dialog.name} from the config?`)) }),
21040
+ dialog?.kind === "form" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
21041
+ Box_default,
21042
+ {
21043
+ flexDirection: "column",
21044
+ width: formWidth,
21045
+ borderStyle: unicode ? "single" : "classic",
21046
+ paddingX: 1,
20468
21047
  children: [
20469
21048
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(
20470
21049
  truncate(
20471
- panel.kind === "options" ? "Options" : panel.name,
20472
- Math.max(1, panelWidth - PANEL_FRAME_WIDTH)
21050
+ dialog.editing === void 0 ? "Add a service" : `Edit ${dialog.editing}`,
21051
+ Math.max(1, formWidth - PANEL_FRAME_WIDTH)
20473
21052
  )
20474
21053
  ) }),
20475
- panelRows.map(([label, value], index) => {
20476
- const line = `${index === panel.row ? marker : " "}${label.padEnd(labelWidth)} ${unicode ? `\u2039 ${value} \u203A` : `< ${value} >`}`;
20477
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, Math.max(1, panelWidth - PANEL_FRAME_WIDTH)) }, label);
20478
- })
21054
+ formLabels.map(({ key, label, placeholder }, index) => {
21055
+ const active = index === dialog.row;
21056
+ const value = dialog.values[key];
21057
+ const room = Math.max(1, formWidth - PANEL_FRAME_WIDTH);
21058
+ const prefix = `${active ? marker : " "}${label.padEnd(formLabelWidth)} `;
21059
+ const typed = active ? `${prefix}${ending(value, Math.max(1, room - prefix.length - CURSOR.length))}${CURSOR}` : truncate(`${prefix}${value}`, room);
21060
+ const example = value ? "" : truncate(placeholder, Math.max(0, room - typed.length - 1));
21061
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
21062
+ typed,
21063
+ example && colors.dim(`${active ? " " : ""}${example}`)
21064
+ ] }, key);
21065
+ }),
21066
+ dialog.problem !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.yellow(dialog.problem) })
20479
21067
  ]
20480
21068
  }
20481
21069
  ),
21070
+ notice !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: notice }),
20482
21071
  unsaved !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.yellow(cut(`not saved: ${unsaved}`)) }),
20483
21072
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(hint)) })
20484
21073
  ] });
@@ -20486,7 +21075,14 @@ function Dashboard({
20486
21075
 
20487
21076
  // src/dashboard/open.tsx
20488
21077
  var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
20489
- async function openDashboard(config, terminal, state) {
21078
+ var pickFields = ({ host, port, path, expect }) => ({
21079
+ host,
21080
+ port,
21081
+ path,
21082
+ expect
21083
+ });
21084
+ async function openDashboard(initial, terminal, { onServiceWrite, ...state }) {
21085
+ let config = initial;
20490
21086
  const check2 = (signal, timeoutMs, skipped, timeouts2) => {
20491
21087
  const services = Object.fromEntries(
20492
21088
  Object.entries(config.services).map(([name, service]) => [
@@ -20494,7 +21090,7 @@ async function openDashboard(config, terminal, state) {
20494
21090
  { ...service, timeout: timeouts2[name] }
20495
21091
  ])
20496
21092
  );
20497
- return runChecks({ ...config, services }, [], { timeoutMs, signal }, skipped);
21093
+ return runChecks({ ...config, services }, [], { timeoutMs, signal, detail: true }, skipped);
20498
21094
  };
20499
21095
  const timeouts = Object.fromEntries(
20500
21096
  Object.entries(config.services).flatMap(
@@ -20502,7 +21098,25 @@ async function openDashboard(config, terminal, state) {
20502
21098
  )
20503
21099
  );
20504
21100
  const app = render_default(
20505
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dashboard, { terminal, check: check2, timeouts, ...state }),
21101
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
21102
+ Dashboard,
21103
+ {
21104
+ terminal,
21105
+ check: check2,
21106
+ timeouts,
21107
+ ...serviceActions(() => config),
21108
+ ...state,
21109
+ serviceFields: (name) => {
21110
+ const service = config.services[name];
21111
+ return service && pickFields(service);
21112
+ },
21113
+ onServiceWrite: onServiceWrite && (async (write) => {
21114
+ const { config: written2, ...outcome } = await onServiceWrite(write);
21115
+ if (written2) config = written2;
21116
+ return { ...outcome, saved: written2 !== void 0 };
21117
+ })
21118
+ }
21119
+ ),
20506
21120
  { alternateScreen: true }
20507
21121
  );
20508
21122
  await app.waitUntilExit();