@ninhache/probe 0.10.0 → 0.11.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/README.md CHANGED
@@ -121,7 +121,7 @@ of macOS.
121
121
  | `e` | Edit the selected service |
122
122
  | `d` | Remove the selected service, after a question |
123
123
  | `:` | Find a command by what it does; `Enter` runs |
124
- | `o` | Options: interval and timeout |
124
+ | `o` | Options: interval, timeout and notifications |
125
125
  | `?` | Every key; `?` or `Esc` closes |
126
126
  | `q` | Quit |
127
127
 
@@ -143,11 +143,15 @@ is replaced by `(hidden)`. None of it is written anywhere, and `probe check --js
143
143
  The panels save what you change in `.probe/local.yml`, for your machine only:
144
144
 
145
145
  ```yaml
146
- dashboard: { interval: 5, timeout: 3 }
146
+ dashboard: { interval: 5, timeout: 3, notify: true }
147
147
  services:
148
148
  db: { skip: true }
149
149
  ```
150
150
 
151
+ With `notify` on, a desktop notification says when a service goes down or comes back: one per round of checks
152
+ that saw a change, none when the dashboard starts. It names the services and nothing else, and needs
153
+ `notify-send` on Linux (`osascript`, already there, on macOS); without it the dashboard says so once and goes on.
154
+
151
155
  A skipped service is left out of `probe check`, `probe wait` and the dashboard, and shown as `skipped`. Name it
152
156
  (`probe check db`) and it is checked anyway.
153
157
 
@@ -36,6 +36,7 @@ var EXIT_ERROR = 2;
36
36
  var DETECT_PORTS = [3e3, 3001, 4200, 4321, 5173, 8e3, 8080];
37
37
  var DETECT_PORTS_ENV = "PROBE_DETECT_PORTS";
38
38
  var DETECT_TIMEOUT_MS = 300;
39
+ var NOTIFY_WAIT_MS = 5e3;
39
40
 
40
41
  // src/errors.ts
41
42
  var ProbeError = class extends Error {
@@ -524,6 +525,7 @@ export {
524
525
  DETECT_PORTS,
525
526
  DETECT_PORTS_ENV,
526
527
  DETECT_TIMEOUT_MS,
528
+ NOTIFY_WAIT_MS,
527
529
  CURRENT_SCHEMA_VERSION,
528
530
  SERVICE_NAME_PATTERN,
529
531
  serviceSchemaV1,
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  serviceSchemaV1,
51
51
  stripStyles,
52
52
  targetUrl
53
- } from "./chunk-VSQVZK7F.js";
53
+ } from "./chunk-VHDD7JN7.js";
54
54
  import "./chunk-M3ASEG5F.js";
55
55
 
56
56
  // src/cli.ts
@@ -624,7 +624,7 @@ async function writeLock(paths, lock) {
624
624
  }
625
625
 
626
626
  // src/version.ts
627
- var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
627
+ var CLI_VERSION = true ? "0.11.0" : "0.0.0-dev";
628
628
 
629
629
  // src/makefile/generator.ts
630
630
  import { createHash } from "crypto";
@@ -885,6 +885,7 @@ import { dirname as dirname2, join as join5 } from "path";
885
885
  import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
886
886
  var SECTION = "dashboard";
887
887
  var KEYS = ["interval", "timeout"];
888
+ var NOTIFY = "notify";
888
889
  var SERVICES = "services";
889
890
  var SKIP = "skip";
890
891
  function isDelay(value) {
@@ -907,18 +908,21 @@ async function readDashboardPreferences(paths) {
907
908
  const value = doc.getIn([SECTION, key]);
908
909
  if (isDelay(value)) preferences[key] = value;
909
910
  }
911
+ if (doc.getIn([SECTION, NOTIFY]) === true) preferences.notify = true;
910
912
  return preferences;
911
913
  }
912
914
  function initialDashboardSettings(askedIntervalMs, saved) {
913
915
  return {
914
916
  intervalMs: askedIntervalMs ?? (saved.interval ?? DASHBOARD_INTERVAL_SECONDS) * 1e3,
915
- timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
917
+ timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3,
918
+ ...saved.notify ? { notify: true } : {}
916
919
  };
917
920
  }
918
921
  function changedPreferences(previous, next) {
919
922
  const changed = {};
920
923
  if (next.intervalMs !== previous.intervalMs) changed.interval = next.intervalMs / 1e3;
921
924
  if (next.timeoutMs !== previous.timeoutMs) changed.timeout = next.timeoutMs / 1e3;
925
+ if (Boolean(next.notify) !== Boolean(previous.notify)) changed.notify = Boolean(next.notify);
922
926
  return changed;
923
927
  }
924
928
  async function editLocal(paths, edit) {
@@ -951,6 +955,11 @@ function writeDashboardPreferences(paths, preferences) {
951
955
  for (const key of KEYS) {
952
956
  if (preferences[key] !== void 0) doc.setIn([SECTION, key], preferences[key]);
953
957
  }
958
+ if (preferences.notify) doc.setIn([SECTION, NOTIFY], true);
959
+ else if (preferences.notify === false && isMap2(section)) {
960
+ section.delete(NOTIFY);
961
+ if (section.items.length === 0) doc.delete(SECTION);
962
+ }
954
963
  return true;
955
964
  });
956
965
  }
@@ -1796,7 +1805,7 @@ async function openedDashboard(rawArgs) {
1796
1805
  if (!asked) return false;
1797
1806
  throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
1798
1807
  }
1799
- const { openDashboard } = await import("./open-W2TWG636.js");
1808
+ const { openDashboard } = await import("./open-QWB24XAM.js");
1800
1809
  let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
1801
1810
  const saves = createSaveQueue();
1802
1811
  const local = `${PROBE_DIR}/${LOCAL_FILE}`;
@@ -4,6 +4,7 @@ import {
4
4
  DASHBOARD_HISTORY_LENGTH,
5
5
  DASHBOARD_INTERVAL_CHOICES_SECONDS,
6
6
  DASHBOARD_TIMEOUT_CHOICES_SECONDS,
7
+ NOTIFY_WAIT_MS,
7
8
  createColors,
8
9
  curlCommand,
9
10
  expectProblem,
@@ -18,7 +19,7 @@ import {
18
19
  runChecks,
19
20
  targetUrl,
20
21
  truncate
21
- } from "./chunk-VSQVZK7F.js";
22
+ } from "./chunk-VHDD7JN7.js";
22
23
  import {
23
24
  __commonJS,
24
25
  __export,
@@ -20279,12 +20280,50 @@ async function copyText(text, platform2 = process.platform, env3 = process.env,
20279
20280
  return false;
20280
20281
  }
20281
20282
 
20283
+ // src/notify.ts
20284
+ import { spawn as nodeSpawn2 } from "child_process";
20285
+ function notifierName(platform2) {
20286
+ return platform2 === "darwin" ? "osascript" : "notify-send";
20287
+ }
20288
+ function notifierCommand(platform2, title, text) {
20289
+ if (platform2 !== "darwin") return ["notify-send", "--app-name=probe", "--", title, text];
20290
+ return [
20291
+ "osascript",
20292
+ "-e",
20293
+ "on run argv",
20294
+ "-e",
20295
+ "display notification (item 2 of argv) with title (item 1 of argv)",
20296
+ "-e",
20297
+ "end run",
20298
+ title,
20299
+ text
20300
+ ];
20301
+ }
20302
+ function sendNotification(title, text, platform2 = process.platform, spawn = nodeSpawn2) {
20303
+ const [command = "", ...args] = notifierCommand(platform2, title, text);
20304
+ return new Promise((resolve) => {
20305
+ const child = spawn(command, args, { stdio: "ignore" });
20306
+ const late = setTimeout(() => {
20307
+ child.kill();
20308
+ resolve(false);
20309
+ }, NOTIFY_WAIT_MS);
20310
+ const settle = (shown) => {
20311
+ clearTimeout(late);
20312
+ resolve(shown);
20313
+ };
20314
+ child.once("error", () => settle(false));
20315
+ child.once("exit", (code) => settle(code === 0));
20316
+ });
20317
+ }
20318
+
20282
20319
  // src/dashboard/actions.ts
20320
+ var NOTIFICATION_TITLE = "probe";
20283
20321
  var system = () => ({
20284
20322
  platform: process.platform,
20285
20323
  env: process.env,
20286
20324
  openUrl,
20287
- copyText
20325
+ copyText,
20326
+ notify: sendNotification
20288
20327
  });
20289
20328
  function serviceActions(config, deps = system()) {
20290
20329
  const service = (name) => config().services[name];
@@ -20306,6 +20345,10 @@ ${url}`;
20306
20345
  const tools = clipboardTools(deps.platform, deps.env).map(([tool]) => tool);
20307
20346
  return `No ${tools.join(" or ")} to copy with. The command:
20308
20347
  ${command}`;
20348
+ },
20349
+ async onNotify(message) {
20350
+ if (await deps.notify(NOTIFICATION_TITLE, message)) return void 0;
20351
+ return `${notifierName(deps.platform)} could not show a desktop notification`;
20309
20352
  }
20310
20353
  };
20311
20354
  }
@@ -20313,6 +20356,24 @@ ${command}`;
20313
20356
  // src/dashboard/Dashboard.tsx
20314
20357
  var import_react36 = __toESM(require_react(), 1);
20315
20358
 
20359
+ // src/dashboard/changes.ts
20360
+ function stateChanges(known, results) {
20361
+ const checked = results.filter(({ skipped }) => !skipped);
20362
+ return {
20363
+ known: new Map(checked.map(({ name, ok }) => [name, ok])),
20364
+ changes: checked.filter(({ name, ok }) => known.has(name) && known.get(name) !== ok).map(({ name, ok }) => ({ name, up: ok }))
20365
+ };
20366
+ }
20367
+ function changeMessage(changes) {
20368
+ const [only] = changes;
20369
+ if (only && changes.length === 1) return `${only.name} is ${only.up ? "back up" : "down"}`;
20370
+ const names = (up) => changes.filter((change) => change.up === up).map(({ name }) => name).join(", ");
20371
+ return [
20372
+ ...names(false) ? [`down: ${names(false)}`] : [],
20373
+ ...names(true) ? [`back up: ${names(true)}`] : []
20374
+ ].join("; ");
20375
+ }
20376
+
20316
20377
  // src/dashboard/detail.ts
20317
20378
  var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
20318
20379
  var HEADER_LINES = 6;
@@ -20665,6 +20726,7 @@ function stepChoice(choices, current, direction) {
20665
20726
  // src/dashboard/Dashboard.tsx
20666
20727
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
20667
20728
  var SERVICE_ROWS = ["Skip", "Timeout"];
20729
+ var NOTIFY_ROW = SETTING_ROWS.length;
20668
20730
  var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
20669
20731
  var NO_TIMEOUTS = {};
20670
20732
  var MARKER_WIDTH = 2;
@@ -20691,6 +20753,7 @@ function Dashboard({
20691
20753
  onTimeoutChange,
20692
20754
  onOpen,
20693
20755
  onCopy,
20756
+ onNotify,
20694
20757
  serviceFields,
20695
20758
  onServiceWrite
20696
20759
  }) {
@@ -20717,6 +20780,20 @@ function Dashboard({
20717
20780
  void saved?.then(setUnsaved);
20718
20781
  };
20719
20782
  const { intervalMs, timeoutMs } = settings;
20783
+ const known = (0, import_react36.useRef)(/* @__PURE__ */ new Map());
20784
+ const notifying = (0, import_react36.useRef)(false);
20785
+ notifying.current = settings.notify === true;
20786
+ const warned = (0, import_react36.useRef)(false);
20787
+ const tell = (next) => {
20788
+ const { known: now, changes } = stateChanges(known.current, next);
20789
+ known.current = now;
20790
+ if (!notifying.current || changes.length === 0) return;
20791
+ void onNotify?.(changeMessage(changes)).then((warning) => {
20792
+ if (warning === void 0 || warned.current) return;
20793
+ warned.current = true;
20794
+ setNotice(warning);
20795
+ });
20796
+ };
20720
20797
  (0, import_react36.useEffect)(() => {
20721
20798
  const controller = new AbortController();
20722
20799
  const { signal } = controller;
@@ -20725,6 +20802,7 @@ function Dashboard({
20725
20802
  const next = await check2(signal, timeoutMs, skipped, timeouts);
20726
20803
  if (signal.aborted) return;
20727
20804
  setLatest(next);
20805
+ tell(next);
20728
20806
  setHistory((before) => recordLatencies(before, next));
20729
20807
  await new Promise((resolve) => {
20730
20808
  const timer = setTimeout(resolve, intervalMs);
@@ -20793,10 +20871,9 @@ ${outcome.warning}` : done);
20793
20871
  };
20794
20872
  const change = (rowIndex, direction) => {
20795
20873
  const row = SETTING_ROWS[rowIndex];
20796
- if (!row) return;
20797
- const value = stepChoice(row.choicesMs, settings[row.key], direction);
20798
- if (value === settings[row.key]) return;
20799
- const next = { ...settings, [row.key]: value };
20874
+ if (!row && rowIndex !== NOTIFY_ROW) return;
20875
+ const next = row ? { ...settings, [row.key]: stepChoice(row.choicesMs, settings[row.key], direction) } : { ...settings, notify: !settings.notify };
20876
+ if (row && next[row.key] === settings[row.key]) return;
20800
20877
  setSettings(next);
20801
20878
  track(onSettingsChange?.(next));
20802
20879
  };
@@ -20890,7 +20967,7 @@ ${outcome.warning}` : done);
20890
20967
  } else if (key === "escape" || (panel.kind === "options" ? input === "o" : enter))
20891
20968
  setPanel(null);
20892
20969
  else if (up2 || down) {
20893
- const lastRow = (panel.kind === "options" ? SETTING_ROWS : SERVICE_ROWS).length - 1;
20970
+ const lastRow = panel.kind === "options" ? NOTIFY_ROW : SERVICE_ROWS.length - 1;
20894
20971
  setPanel({ ...panel, row: Math.max(0, Math.min(lastRow, panel.row + (down ? 1 : -1))) });
20895
20972
  } else if (left || right) {
20896
20973
  if (panel.kind === "options") change(panel.row, right ? 1 : -1);
@@ -20981,7 +21058,10 @@ ${outcome.warning}` : done);
20981
21058
  room: (windowRows || DEFAULT_ROWS) - taken - (unsaved === void 0 ? 0 : 1)
20982
21059
  }) : [];
20983
21060
  const toned = ({ text, tone }) => tone === "good" ? colors.green(text) : tone === "bad" ? colors.red(text) : tone === "dim" ? colors.dim(text) : text;
20984
- const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? [
21061
+ const panelRows = panel?.kind === "options" ? [
21062
+ ...SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]),
21063
+ ["Notify", settings.notify ? "on" : "off"]
21064
+ ] : panel ? [
20985
21065
  ["Skip", skipped.has(panel.name) ? "yes" : "no"],
20986
21066
  [
20987
21067
  "Timeout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninhache/probe",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Health checks for your local dev services, from the CLI or a generated Makefile",
5
5
  "license": "MIT",
6
6
  "author": "Ninhache",