@ninhache/probe 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.
package/README.md CHANGED
@@ -39,7 +39,7 @@ Want `make check` too? See [Makefile](#makefile): it is optional.
39
39
  | Command | What it does |
40
40
  | --------------------------------------------------------- | ---------------------------------------------------------------------- |
41
41
  | `probe init` | Create `.probe/` and the `.gitignore` entries |
42
- | `probe add <name> -p <port> [--host] [--path] [--expect]` | Add a service |
42
+ | `probe add <name> -p <port> [--host] [--path] [--expect]` | Add a service (`--timeout <seconds>` gives it its own timeout) |
43
43
  | `probe rm <name>` | Remove a service |
44
44
  | `probe ls` | List services |
45
45
  | `probe check [name...] [--json] [--timeout <seconds>]` | Check all services, or the named ones |
@@ -53,7 +53,8 @@ Exit codes: `0` all up, `1` a service is down, `2` usage or config error.
53
53
  `probe wait` is for scripts: `probe wait && pnpm test`. It checks at once, then again after 0.5 s, 1 s, 2 s, 4 s
54
54
  and every 5 s, for 60 s in total. `--timeout` sets the total, `--interval` the first pause, `--max-interval` the
55
55
  longest one. Unlike `check --timeout`, which is the time given to each service, `wait --timeout` covers the whole
56
- wait; each check inside it still gets 3 s. Progress goes to stderr, the final report to stdout; exit code `1` names the services still down.
56
+ wait; each check inside it still gets 3 s, or the service's own `timeout`. Progress goes to stderr, the final
57
+ report to stdout; exit code `1` names the services still down.
57
58
 
58
59
  `probe` works from any sub-directory: it walks up to the nearest `.probe/`.
59
60
 
@@ -62,13 +63,13 @@ wait; each check inside it still gets 3 s. Progress goes to stderr, the final re
62
63
  `probe` alone, on a terminal, opens a live list of your services and checks them every 2 s
63
64
  (`probe --interval 5` for another pace). Off a terminal, in CI or in a pipe, nothing changes: use `probe check`.
64
65
 
65
- | Key | What it does |
66
- | --------------- | ------------------------------------------------ |
67
- | `↑` `↓` `j` `k` | Move |
68
- | `r` | Check now |
69
- | `Enter` | Open the panel of the selected service (skip it) |
70
- | `o` | Options: interval and timeout |
71
- | `q` | Quit |
66
+ | Key | What it does |
67
+ | --------------- | -------------------------------------- |
68
+ | `↑` `↓` `j` `k` | Move |
69
+ | `r` | Check now |
70
+ | `Enter` | Open the panel of the selected service |
71
+ | `o` | Options: interval and timeout |
72
+ | `q` | Quit |
72
73
 
73
74
  What you change there is saved in `.probe/local.yml`, for your machine only:
74
75
 
@@ -81,6 +82,9 @@ services:
81
82
  A skipped service is left out of `probe check`, `probe wait` and the dashboard, and shown as `skipped`. Name it
82
83
  (`probe check db`) and it is checked anyway.
83
84
 
85
+ The panel of a service also sets its timeout. That one is shared: it is written to `.probe/config.yml`, so
86
+ commit it.
87
+
84
88
  ## Config
85
89
 
86
90
  `.probe/config.yml`:
@@ -91,14 +95,19 @@ services:
91
95
  api: { port: 3000, path: /health }
92
96
  front: { port: 5173 }
93
97
  admin: { host: 127.0.0.1, port: 8080, expect: [200, 204] }
98
+ db: { port: 5432, timeout: 10 }
94
99
  ```
95
100
 
96
- | Key | Default | Meaning |
97
- | -------- | ----------- | -------------------------------------- |
98
- | `port` | required | TCP port |
99
- | `host` | `localhost` | Host name or IP |
100
- | `path` | `/` | Path requested with `GET` |
101
- | `expect` | any 2xx | Expected status, or a list of statuses |
101
+ | Key | Default | Meaning |
102
+ | --------- | ----------- | --------------------------------------- |
103
+ | `port` | required | TCP port |
104
+ | `host` | `localhost` | Host name or IP |
105
+ | `path` | `/` | Path requested with `GET` |
106
+ | `expect` | any 2xx | Expected status, or a list of statuses |
107
+ | `timeout` | 3 | Seconds given to this service to answer |
108
+
109
+ Which timeout applies: `--timeout` when you pass it, else the service's `timeout`, else 3 s (in the dashboard,
110
+ the timeout set in its options). A config with a `timeout` needs `probe` 0.4.0 or later: older versions refuse it.
102
111
 
103
112
  Redirects are followed and the final status is judged, unless `expect` contains a 3xx: then the first response
104
113
  is judged as is.
@@ -164,7 +164,12 @@ function getChecker(type) {
164
164
  }
165
165
 
166
166
  // src/checkers/run.ts
167
- async function runChecks(config, names, context, skipped = /* @__PURE__ */ new Set()) {
167
+ function resolveTimeoutMs(service, options) {
168
+ const own = options.forceTimeout || service.timeout === void 0 ? void 0 : service.timeout;
169
+ const wanted = own === void 0 ? options.timeoutMs : Math.ceil(own * 1e3);
170
+ return Math.max(1, Math.min(wanted, options.maxTimeoutMs ?? wanted));
171
+ }
172
+ async function runChecks(config, names, options, skipped = /* @__PURE__ */ new Set()) {
168
173
  const known = Object.keys(config.services);
169
174
  const unknown = names.filter((name) => !known.includes(name));
170
175
  if (unknown.length > 0) {
@@ -189,7 +194,10 @@ async function runChecks(config, names, context, skipped = /* @__PURE__ */ new S
189
194
  };
190
195
  return result;
191
196
  }
192
- return getChecker(type).check(name, service, context);
197
+ return getChecker(type).check(name, service, {
198
+ timeoutMs: resolveTimeoutMs(service, options),
199
+ signal: options.signal
200
+ });
193
201
  })
194
202
  );
195
203
  }
@@ -251,7 +259,7 @@ function formatServices(config, terminal, skipped = /* @__PURE__ */ new Set()) {
251
259
  const rows = Object.entries(config.services).map(([name, service]) => ({
252
260
  name,
253
261
  url: targetUrl(service),
254
- notes: (service.expect === void 0 ? "" : `${GAP}expect ${service.expect}`) + (skipped.has(name) ? `${GAP}${SKIPPED}` : "")
262
+ notes: (service.expect === void 0 ? "" : `${GAP}expect ${service.expect}`) + (service.timeout === void 0 ? "" : `${GAP}timeout ${service.timeout}s`) + (skipped.has(name) ? `${GAP}${SKIPPED}` : "")
255
263
  }));
256
264
  const nameWidth = width(rows.map((row) => row.name));
257
265
  const fullWidth = width(rows.map((row) => " ".repeat(nameWidth) + GAP + row.url + row.notes));
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  isInteractive,
35
35
  runChecks,
36
36
  stripStyles
37
- } from "./chunk-ZCAUPLQS.js";
37
+ } from "./chunk-HHTOVIAQ.js";
38
38
  import "./chunk-M3ASEG5F.js";
39
39
 
40
40
  // src/cli.ts
@@ -98,7 +98,9 @@ var serviceSchemaV1 = z.strictObject({
98
98
  host: z.string().min(1).default("localhost"),
99
99
  port: z.number().int().min(1).max(65535),
100
100
  path: z.string().startsWith("/").default("/"),
101
- expect: z.union([statusCode, z.array(statusCode).min(1)]).optional()
101
+ expect: z.union([statusCode, z.array(statusCode).min(1)]).optional(),
102
+ /** Seconds given to this service to answer, instead of the default. */
103
+ timeout: z.number().positive().max(MAX_TIMEOUT_SECONDS).optional()
102
104
  });
103
105
  var configSchemaV1 = z.strictObject({
104
106
  schemaVersion: z.literal(1),
@@ -278,6 +280,15 @@ async function saveConfig(loaded, raw, options = {}) {
278
280
  await writeFile(loaded.paths.config, loaded.doc.toString());
279
281
  return { ...loaded, config, raw };
280
282
  }
283
+ async function writeServiceTimeout(paths, name, seconds2) {
284
+ const loaded = await loadConfig(paths);
285
+ const services = loaded.raw.services ?? {};
286
+ if (!Object.hasOwn(services, name)) return false;
287
+ const { timeout: _previous, ...service } = services[name];
288
+ if (seconds2 !== void 0) service.timeout = seconds2;
289
+ await saveConfig(loaded, { ...loaded.raw, services: { ...services, [name]: service } });
290
+ return true;
291
+ }
281
292
 
282
293
  // src/makefile/sync.ts
283
294
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
@@ -305,7 +316,7 @@ async function writeLock(paths, lock) {
305
316
  }
306
317
 
307
318
  // src/version.ts
308
- var CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
319
+ var CLI_VERSION = true ? "0.4.0" : "0.0.0-dev";
309
320
 
310
321
  // src/makefile/generator.ts
311
322
  import { createHash } from "crypto";
@@ -449,7 +460,11 @@ var add_default = defineCommand({
449
460
  port: { type: "string", alias: "p", description: "Port", required: true },
450
461
  host: { type: "string", description: "Host (default: localhost)" },
451
462
  path: { type: "string", description: "Path to request (default: /)" },
452
- expect: { type: "string", description: "Expected status, e.g. 200 or 200,204 (default: 2xx)" }
463
+ expect: { type: "string", description: "Expected status, e.g. 200 or 200,204 (default: 2xx)" },
464
+ timeout: {
465
+ type: "string",
466
+ description: `Seconds to wait for this service (default: ${DEFAULT_TIMEOUT_SECONDS})`
467
+ }
453
468
  },
454
469
  async run({ args }) {
455
470
  const loaded = await openProject();
@@ -464,6 +479,10 @@ var add_default = defineCommand({
464
479
  if (args.host) service.host = args.host;
465
480
  if (args.path) service.path = args.path;
466
481
  if (args.expect) service.expect = parseExpect(String(args.expect));
482
+ if (args.timeout !== void 0) {
483
+ secondsToMs("--timeout", String(args.timeout));
484
+ service.timeout = Number(args.timeout);
485
+ }
467
486
  await saveServices(loaded, { ...services, [args.name]: service });
468
487
  console.log(`Added ${args.name}. Try: probe check ${args.name}`);
469
488
  }
@@ -583,15 +602,16 @@ var check_default = defineCommand2({
583
602
  json: { type: "boolean", description: "Print results as JSON" },
584
603
  timeout: {
585
604
  type: "string",
586
- description: "Seconds to wait for each service",
587
- default: String(DEFAULT_TIMEOUT_SECONDS)
605
+ // No citty default: a value given here must be told apart from none, it beats the services' own.
606
+ description: `Seconds to wait for each service (default: its own timeout, else ${DEFAULT_TIMEOUT_SECONDS})`
588
607
  }
589
608
  },
590
609
  async run({ args }) {
591
- const timeoutMs = secondsToMs("--timeout", args.timeout);
610
+ const forceTimeout = args.timeout !== void 0;
611
+ const timeoutMs = secondsToMs("--timeout", args.timeout ?? String(DEFAULT_TIMEOUT_SECONDS));
592
612
  const { config, paths } = await openProject();
593
613
  const skipped = await readSkippedServices(paths);
594
- const results = await runChecks(config, args._, { timeoutMs }, skipped);
614
+ const results = await runChecks(config, args._, { timeoutMs, forceTimeout }, skipped);
595
615
  if (args.json) {
596
616
  console.log(JSON.stringify(results, null, 2));
597
617
  } else if (results.length === 0) {
@@ -829,15 +849,16 @@ async function pollUntilUp(check, options) {
829
849
  let pause = Math.min(options.intervalMs, maxIntervalMs);
830
850
  let lastPass = false;
831
851
  while (true) {
832
- const results = await check(Math.max(1, Math.ceil(Math.min(attemptTimeoutMs, remaining()))));
852
+ const left = Math.max(1, Math.ceil(remaining()));
853
+ const results = await check(Math.min(attemptTimeoutMs, left), left);
833
854
  const elapsedMs = Math.round(now() - started);
834
855
  onPass?.(results, elapsedMs);
835
856
  if (results.every((result) => result.ok)) return { results, elapsedMs, timedOut: false };
836
- const left = remaining();
837
- if (lastPass || left <= 0) return { results, elapsedMs, timedOut: true };
838
- if (pause >= left) {
857
+ const rest = remaining();
858
+ if (lastPass || rest <= 0) return { results, elapsedMs, timedOut: true };
859
+ if (pause >= rest) {
839
860
  lastPass = true;
840
- await sleep(left - Math.min(WAIT_LAST_PASS_MS, left / 2));
861
+ await sleep(rest - Math.min(WAIT_LAST_PASS_MS, rest / 2));
841
862
  } else {
842
863
  await sleep(pause);
843
864
  pause = Math.min(pause * WAIT_BACKOFF_FACTOR, maxIntervalMs);
@@ -901,7 +922,13 @@ var wait_default = defineCommand9({
901
922
  const skipped = await readSkippedServices(paths);
902
923
  const report = progressReporter();
903
924
  const { results, timedOut } = await pollUntilUp(
904
- (timeoutMs) => runChecks(config, args._, { timeoutMs }, skipped),
925
+ // A service with its own timeout keeps it here, within what is left of the total.
926
+ (_passMs, remainingMs) => runChecks(
927
+ config,
928
+ args._,
929
+ { timeoutMs: DEFAULT_TIMEOUT_SECONDS * 1e3, maxTimeoutMs: remainingMs },
930
+ skipped
931
+ ),
905
932
  {
906
933
  deadlineMs,
907
934
  attemptTimeoutMs: DEFAULT_TIMEOUT_SECONDS * 1e3,
@@ -987,7 +1014,7 @@ async function openedDashboard(rawArgs) {
987
1014
  if (!asked) return false;
988
1015
  throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
989
1016
  }
990
- const { openDashboard } = await import("./open-TQ6NPJ7V.js");
1017
+ const { openDashboard } = await import("./open-QTP4Z5QX.js");
991
1018
  let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
992
1019
  let saving = Promise.resolve();
993
1020
  const save = (write) => {
@@ -1002,7 +1029,8 @@ async function openedDashboard(rawArgs) {
1002
1029
  settings = next;
1003
1030
  save(() => writeDashboardPreferences(paths, changed));
1004
1031
  },
1005
- onSkipChange: (name, skip) => save(() => writeServiceSkip(paths, name, skip))
1032
+ onSkipChange: (name, skip) => save(() => writeServiceSkip(paths, name, skip)),
1033
+ onTimeoutChange: (name, seconds2) => save(() => writeServiceTimeout(paths, name, seconds2))
1006
1034
  });
1007
1035
  await saving;
1008
1036
  return true;
@@ -7,7 +7,7 @@ import {
7
7
  formatCheckResults,
8
8
  runChecks,
9
9
  truncate
10
- } from "./chunk-ZCAUPLQS.js";
10
+ } from "./chunk-HHTOVIAQ.js";
11
11
  import {
12
12
  __commonJS,
13
13
  __export,
@@ -20252,6 +20252,12 @@ var SETTING_ROWS = [
20252
20252
  { key: "intervalMs", label: "Interval", choicesMs: DASHBOARD_INTERVAL_CHOICES_SECONDS.map(toMs) },
20253
20253
  { key: "timeoutMs", label: "Timeout", choicesMs: DASHBOARD_TIMEOUT_CHOICES_SECONDS.map(toMs) }
20254
20254
  ];
20255
+ function stepServiceTimeout(current, direction) {
20256
+ const choices = DASHBOARD_TIMEOUT_CHOICES_SECONDS;
20257
+ if (current === void 0) return direction === 1 ? choices[0] : void 0;
20258
+ if (direction === 1) return choices.find((choice) => choice > current) ?? current;
20259
+ return choices.findLast((choice) => choice < current);
20260
+ }
20255
20261
  function stepChoice(choices, current, direction) {
20256
20262
  const next = direction === 1 ? choices.find((choice) => choice > current) : choices.findLast((choice) => choice < current);
20257
20263
  return next ?? current;
@@ -20259,7 +20265,9 @@ function stepChoice(choices, current, direction) {
20259
20265
 
20260
20266
  // src/dashboard/Dashboard.tsx
20261
20267
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
20262
- var SERVICE_ROWS = ["Skip"];
20268
+ var SERVICE_ROWS = ["Skip", "Timeout"];
20269
+ var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
20270
+ var NO_TIMEOUTS = {};
20263
20271
  var MARKER_WIDTH = 2;
20264
20272
  var PANEL_WIDTH = 30;
20265
20273
  var PANEL_FRAME_WIDTH = 4;
@@ -20268,9 +20276,11 @@ function Dashboard({
20268
20276
  terminal,
20269
20277
  settings: initial,
20270
20278
  skipped: initiallySkipped,
20279
+ timeouts: initialTimeouts = NO_TIMEOUTS,
20271
20280
  check: check2,
20272
20281
  onSettingsChange,
20273
- onSkipChange
20282
+ onSkipChange,
20283
+ onTimeoutChange
20274
20284
  }) {
20275
20285
  const { exit } = useApp();
20276
20286
  const { columns } = use_window_size_default();
@@ -20278,6 +20288,7 @@ function Dashboard({
20278
20288
  const [selected, setSelected] = (0, import_react36.useState)(0);
20279
20289
  const [settings, setSettings] = (0, import_react36.useState)(initial);
20280
20290
  const [skipped, setSkipped] = (0, import_react36.useState)(() => new Set(initiallySkipped));
20291
+ const [timeouts, setTimeouts] = (0, import_react36.useState)(initialTimeouts);
20281
20292
  const [panel, setPanel] = (0, import_react36.useState)(null);
20282
20293
  const skipPause = (0, import_react36.useRef)(() => {
20283
20294
  });
@@ -20287,7 +20298,7 @@ function Dashboard({
20287
20298
  const { signal } = controller;
20288
20299
  const loop = async () => {
20289
20300
  while (!signal.aborted) {
20290
- const next = await check2(signal, timeoutMs, skipped);
20301
+ const next = await check2(signal, timeoutMs, skipped, timeouts);
20291
20302
  if (signal.aborted) return;
20292
20303
  setLatest(next);
20293
20304
  await new Promise((resolve) => {
@@ -20302,7 +20313,7 @@ function Dashboard({
20302
20313
  };
20303
20314
  void loop();
20304
20315
  return () => controller.abort();
20305
- }, [check2, intervalMs, timeoutMs, skipped]);
20316
+ }, [check2, intervalMs, timeoutMs, skipped, timeouts]);
20306
20317
  const last = (latest?.length ?? 1) - 1;
20307
20318
  const current = Math.min(selected, last);
20308
20319
  const change = (rowIndex, direction) => {
@@ -20321,6 +20332,12 @@ function Dashboard({
20321
20332
  setSkipped(next);
20322
20333
  onSkipChange?.(name, skip);
20323
20334
  };
20335
+ const changeTimeout = (name, direction) => {
20336
+ const value = stepServiceTimeout(timeouts[name], direction);
20337
+ if (value === timeouts[name]) return;
20338
+ setTimeouts({ ...timeouts, [name]: value });
20339
+ onTimeoutChange?.(name, value);
20340
+ };
20324
20341
  use_input_default((input, key) => {
20325
20342
  const up2 = key.upArrow || input === "k";
20326
20343
  const down = key.downArrow || input === "j";
@@ -20342,6 +20359,7 @@ function Dashboard({
20342
20359
  setPanel({ ...panel, row: Math.max(0, Math.min(lastRow, panel.row + (down ? 1 : -1))) });
20343
20360
  } else if (left || right) {
20344
20361
  if (panel.kind === "options") change(panel.row, right ? 1 : -1);
20362
+ else if (panel.row === TIMEOUT_ROW) changeTimeout(panel.name, right ? 1 : -1);
20345
20363
  else toggleSkip(panel.name);
20346
20364
  }
20347
20365
  });
@@ -20371,7 +20389,13 @@ function Dashboard({
20371
20389
  }).split("\n");
20372
20390
  const focused = results[current];
20373
20391
  const panelWidth = Math.min(PANEL_WIDTH, columns);
20374
- const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? SERVICE_ROWS.map((label) => [label, skipped.has(panel.name) ? "yes" : "no"]) : [];
20392
+ const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? [
20393
+ ["Skip", skipped.has(panel.name) ? "yes" : "no"],
20394
+ [
20395
+ "Timeout",
20396
+ timeouts[panel.name] === void 0 ? "default" : `${timeouts[panel.name]}s`
20397
+ ]
20398
+ ] : [];
20375
20399
  const labelWidth = Math.max(0, ...panelRows.map(([label]) => label.length));
20376
20400
  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`;
20377
20401
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "column", children: [
@@ -20411,10 +20435,24 @@ function Dashboard({
20411
20435
  // src/dashboard/open.tsx
20412
20436
  var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
20413
20437
  async function openDashboard(config, terminal, state) {
20414
- const check2 = (signal, timeoutMs, skipped) => runChecks(config, [], { timeoutMs, signal }, skipped);
20415
- const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dashboard, { terminal, check: check2, ...state }), {
20416
- alternateScreen: true
20417
- });
20438
+ const check2 = (signal, timeoutMs, skipped, timeouts2) => {
20439
+ const services = Object.fromEntries(
20440
+ Object.entries(config.services).map(([name, service]) => [
20441
+ name,
20442
+ { ...service, timeout: timeouts2[name] }
20443
+ ])
20444
+ );
20445
+ return runChecks({ ...config, services }, [], { timeoutMs, signal }, skipped);
20446
+ };
20447
+ const timeouts = Object.fromEntries(
20448
+ Object.entries(config.services).flatMap(
20449
+ ([name, { timeout }]) => timeout === void 0 ? [] : [[name, timeout]]
20450
+ )
20451
+ );
20452
+ const app = render_default(
20453
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dashboard, { terminal, check: check2, timeouts, ...state }),
20454
+ { alternateScreen: true }
20455
+ );
20418
20456
  await app.waitUntilExit();
20419
20457
  }
20420
20458
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninhache/probe",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",