@ninhache/probe 0.1.2 → 0.3.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/dist/cli.js CHANGED
@@ -1,40 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from "node:module";
3
+ const require = __createRequire(import.meta.url);
4
+ import {
5
+ BACKUP_GLOB,
6
+ CONFIG_FILE,
7
+ DASHBOARD_INTERVAL_SECONDS,
8
+ DEFAULT_MAKEFILE,
9
+ DEFAULT_TIMEOUT_SECONDS,
10
+ DEFAULT_WAIT_INTERVAL_SECONDS,
11
+ DEFAULT_WAIT_MAX_INTERVAL_SECONDS,
12
+ DEFAULT_WAIT_TIMEOUT_SECONDS,
13
+ EXIT_DOWN,
14
+ EXIT_ERROR,
15
+ LOCAL_DRAFT_FILE,
16
+ LOCAL_FILE,
17
+ LOCAL_FILE_MODE,
18
+ LOCK_FILE,
19
+ MAX_TIMEOUT_SECONDS,
20
+ PACKAGE_NAME,
21
+ PROBE_DIR,
22
+ ProbeError,
23
+ REFRESH_COMMAND,
24
+ REGISTRY_LATEST_URL,
25
+ REGISTRY_TIMEOUT_MS,
26
+ UPDATE_CACHE_FILE,
27
+ UPDATE_CHECK_TTL_MS,
28
+ WAIT_BACKOFF_FACTOR,
29
+ WAIT_LAST_PASS_MS,
30
+ createColors,
31
+ detectTerminal,
32
+ formatCheckResults,
33
+ formatServices,
34
+ isInteractive,
35
+ runChecks,
36
+ stripStyles
37
+ } from "./chunk-ZCAUPLQS.js";
38
+ import "./chunk-M3ASEG5F.js";
2
39
 
3
40
  // src/cli.ts
4
- import { defineCommand as defineCommand9, runCommand, runMain, showUsage } from "citty";
41
+ import {
42
+ defineCommand as defineCommand10,
43
+ renderUsage,
44
+ runCommand,
45
+ runMain
46
+ } from "citty";
5
47
 
6
48
  // src/commands/add.ts
7
49
  import { defineCommand } from "citty";
8
50
 
9
- // src/constants.ts
10
- var PACKAGE_NAME = "@ninhache/probe";
11
- var PROBE_DIR = ".probe";
12
- var CONFIG_FILE = "config.yml";
13
- var LOCK_FILE = "lock.yml";
14
- var LOCAL_FILE = "local.yml";
15
- var BACKUP_GLOB = "*.bak.yml";
16
- var DEFAULT_MAKEFILE = "Makefile";
17
- var DEFAULT_CHECKER_TYPE = "http";
18
- var DEFAULT_TIMEOUT_SECONDS = 3;
19
- var REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
20
- var REGISTRY_TIMEOUT_MS = 5e3;
21
- var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
22
- var UPDATE_CACHE_FILE = "latest.json";
23
- var REFRESH_COMMAND = "__refresh-update-cache";
24
- var EXIT_DOWN = 1;
25
- var EXIT_ERROR = 2;
26
-
27
- // src/errors.ts
28
- var ProbeError = class extends Error {
29
- constructor(code, message) {
30
- super(message);
31
- this.code = code;
32
- this.name = "ProbeError";
33
- }
34
- code;
35
- exitCode = EXIT_ERROR;
36
- };
37
-
38
51
  // src/config/discovery.ts
39
52
  import { statSync } from "fs";
40
53
  import { dirname, join, resolve } from "path";
@@ -292,7 +305,7 @@ async function writeLock(paths, lock) {
292
305
  }
293
306
 
294
307
  // src/version.ts
295
- var CLI_VERSION = true ? "0.1.2" : "0.0.0-dev";
308
+ var CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
296
309
 
297
310
  // src/makefile/generator.ts
298
311
  import { createHash } from "crypto";
@@ -382,35 +395,34 @@ async function blockState(paths, config) {
382
395
  return lock?.makefile.hash === hashBlock(block) ? "outdated" : "modified";
383
396
  }
384
397
 
385
- // src/ui/colors.ts
386
- function colorEnabled(stream, env) {
387
- return Boolean(stream.isTTY) && !env.NO_COLOR;
388
- }
389
- function createColors(enabled) {
390
- const wrap = (open, close) => (text) => enabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
391
- return {
392
- green: wrap(32, 39),
393
- red: wrap(31, 39),
394
- yellow: wrap(33, 39),
395
- dim: wrap(2, 22),
396
- bold: wrap(1, 22)
397
- };
398
- }
399
-
400
398
  // src/commands/shared.ts
401
399
  function openProject() {
402
400
  return loadConfig(requireProbeDir(process.cwd()));
403
401
  }
402
+ function stdoutTerminal() {
403
+ return detectTerminal(process.stdout, process.env);
404
+ }
404
405
  function stdoutColors() {
405
- return createColors(colorEnabled(process.stdout, process.env));
406
+ return createColors(stdoutTerminal().color);
407
+ }
408
+ function secondsToMs(option, value) {
409
+ const seconds2 = Number(value);
410
+ if (!Number.isFinite(seconds2) || seconds2 <= 0 || seconds2 > MAX_TIMEOUT_SECONDS) {
411
+ throw new ProbeError(
412
+ "USAGE",
413
+ `${option} expects seconds between 0 and ${MAX_TIMEOUT_SECONDS}, got "${value}".`
414
+ );
415
+ }
416
+ return Math.ceil(seconds2 * 1e3);
406
417
  }
407
418
  function rawServices(loaded) {
408
419
  return loaded.raw.services ?? {};
409
420
  }
410
421
  async function saveServices(loaded, services) {
411
- const edited = await blockState(loaded.paths, loaded.config) === "modified";
422
+ const block = await blockState(loaded.paths, loaded.config);
412
423
  const saved = await saveConfig(loaded, { ...loaded.raw, services });
413
- if (!edited) {
424
+ if (block === "missing") return;
425
+ if (block !== "modified") {
414
426
  await syncMakefile(saved.paths, saved.config);
415
427
  return;
416
428
  }
@@ -460,121 +472,111 @@ var add_default = defineCommand({
460
472
  // src/commands/check.ts
461
473
  import { defineCommand as defineCommand2 } from "citty";
462
474
 
463
- // src/checkers/http.ts
464
- var REASONS = {
465
- ECONNREFUSED: "connection refused",
466
- ECONNRESET: "connection reset",
467
- ENOTFOUND: "host not found",
468
- EAI_AGAIN: "host not found",
469
- EHOSTUNREACH: "host unreachable"
470
- };
471
- function targetUrl(service) {
472
- const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
473
- const host = bareIpv6 ? `[${service.host}]` : service.host;
474
- return `http://${host}:${service.port}${service.path}`;
475
- }
476
- function isExpected(expect, status) {
477
- if (expect === void 0) return status >= 200 && status < 300;
478
- return Array.isArray(expect) ? expect.includes(status) : expect === status;
479
- }
480
- function expectsRedirect(expect) {
481
- return [expect ?? []].flat().some((status) => status >= 300 && status < 400);
482
- }
483
- function describeExpected(expect) {
484
- if (expect === void 0) return "2xx";
485
- return Array.isArray(expect) ? expect.join(" or ") : String(expect);
486
- }
487
- function failureReason(error, timeoutMs) {
488
- if (error instanceof Error && error.name === "TimeoutError") {
489
- return `timeout after ${timeoutMs / 1e3}s`;
490
- }
491
- const cause = error?.cause;
492
- const code = cause?.code ?? cause?.errors?.[0]?.code;
493
- if (code) return REASONS[code] ?? code;
494
- return error instanceof Error ? error.message : String(error);
495
- }
496
- var httpChecker = {
497
- type: "http",
498
- async check(name, service, { timeoutMs }) {
499
- const target = targetUrl(service);
500
- const base = { name, type: "http", target };
501
- const started = performance.now();
502
- const elapsed = () => Math.round(performance.now() - started);
503
- try {
504
- const response = await fetch(target, {
505
- signal: AbortSignal.timeout(timeoutMs),
506
- // Following a redirect hides its status: an expected 3xx could never match.
507
- redirect: expectsRedirect(service.expect) ? "manual" : "follow"
508
- });
509
- const latencyMs = elapsed();
510
- await response.body?.cancel();
511
- const { status } = response;
512
- if (isExpected(service.expect, status)) return { ...base, ok: true, status, latencyMs };
513
- const error = `expected ${describeExpected(service.expect)}, got ${status}`;
514
- return { ...base, ok: false, status, latencyMs, error };
515
- } catch (error) {
516
- return { ...base, ok: false, latencyMs: elapsed(), error: failureReason(error, timeoutMs) };
517
- }
475
+ // src/config/local.ts
476
+ import { chmod, readFile as readFile4, realpath, rename, rm, stat, writeFile as writeFile4 } from "fs/promises";
477
+ import { dirname as dirname2, join as join4 } from "path";
478
+ import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
479
+ var SECTION = "dashboard";
480
+ var KEYS = ["interval", "timeout"];
481
+ var SERVICES = "services";
482
+ var SKIP = "skip";
483
+ function isDelay(value) {
484
+ return typeof value === "number" && value > 0 && value <= MAX_TIMEOUT_SECONDS;
485
+ }
486
+ async function readLocal(paths) {
487
+ try {
488
+ return await readFile4(paths.local, "utf8");
489
+ } catch {
490
+ return null;
518
491
  }
519
- };
520
-
521
- // src/checkers/registry.ts
522
- var checkers = new Map([httpChecker].map((checker) => [checker.type, checker]));
523
- function getChecker(type) {
524
- const checker = checkers.get(type);
525
- if (!checker) {
526
- const known = [...checkers.keys()].join(", ");
527
- throw new ProbeError("CHECKER_UNKNOWN", `unknown service type "${type}". Known: ${known}.`);
492
+ }
493
+ async function readDashboardPreferences(paths) {
494
+ const text = await readLocal(paths);
495
+ if (text === null) return {};
496
+ const doc = parseDocument2(text);
497
+ if (doc.errors.length > 0) return {};
498
+ const preferences = {};
499
+ for (const key of KEYS) {
500
+ const value = doc.getIn([SECTION, key]);
501
+ if (isDelay(value)) preferences[key] = value;
528
502
  }
529
- return checker;
503
+ return preferences;
530
504
  }
531
-
532
- // src/checkers/run.ts
533
- async function runChecks(config, names, context) {
534
- const known = Object.keys(config.services);
535
- const unknown = names.filter((name) => !known.includes(name));
536
- if (unknown.length > 0) {
537
- throw new ProbeError(
538
- "SERVICE_UNKNOWN",
539
- `unknown service: ${unknown.join(", ")}. Known: ${known.join(", ") || "(none)"}.`
540
- );
505
+ function initialDashboardSettings(askedIntervalMs, saved) {
506
+ return {
507
+ intervalMs: askedIntervalMs ?? (saved.interval ?? DASHBOARD_INTERVAL_SECONDS) * 1e3,
508
+ timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
509
+ };
510
+ }
511
+ function changedPreferences(previous, next) {
512
+ const changed = {};
513
+ if (next.intervalMs !== previous.intervalMs) changed.interval = next.intervalMs / 1e3;
514
+ if (next.timeoutMs !== previous.timeoutMs) changed.timeout = next.timeoutMs / 1e3;
515
+ return changed;
516
+ }
517
+ async function editLocal(paths, edit) {
518
+ const doc = parseDocument2(await readLocal(paths) ?? "");
519
+ if (doc.errors.length > 0) return false;
520
+ if (doc.contents !== null && !isMap2(doc.contents)) return false;
521
+ const outcome = edit(doc);
522
+ if (outcome !== true) return outcome === null;
523
+ const { contents } = doc;
524
+ const empty = isMap2(contents) && contents.items.length === 0 && !contents.comment;
525
+ const text = empty && !doc.comment && !doc.commentBefore ? "" : doc.toString();
526
+ const target = await realpath(paths.local).catch(() => paths.local);
527
+ const existing = await stat(target).catch(() => null);
528
+ const mode = existing ? existing.mode & 511 : LOCAL_FILE_MODE;
529
+ const draft = join4(dirname2(target), LOCAL_DRAFT_FILE);
530
+ try {
531
+ await writeFile4(draft, text, { mode });
532
+ await chmod(draft, mode);
533
+ await rename(draft, target);
534
+ } catch (error) {
535
+ await rm(draft, { force: true });
536
+ throw error;
541
537
  }
542
- const selected = names.length > 0 ? [...new Set(names)] : known;
543
- return Promise.all(
544
- selected.map((name) => {
545
- const service = config.services[name];
546
- const type = service.type ?? DEFAULT_CHECKER_TYPE;
547
- return getChecker(type).check(name, service, context);
548
- })
549
- );
538
+ return true;
539
+ }
540
+ function writeDashboardPreferences(paths, preferences) {
541
+ return editLocal(paths, (doc) => {
542
+ const section = doc.get(SECTION, true);
543
+ if (section !== void 0 && !isMap2(section)) return false;
544
+ for (const key of KEYS) {
545
+ if (preferences[key] !== void 0) doc.setIn([SECTION, key], preferences[key]);
546
+ }
547
+ return true;
548
+ });
550
549
  }
551
-
552
- // src/ui/report.ts
553
- function width(values) {
554
- return Math.max(0, ...values.map((value) => value.length));
555
- }
556
- function formatCheckResults(results, colors) {
557
- const nameWidth = width(results.map((result) => result.name));
558
- const targetWidth = width(results.map((result) => result.target));
559
- return results.map((result) => {
560
- const mark = result.ok ? colors.green("\u2713") : colors.red("\u2717");
561
- const name = colors.bold(result.name.padEnd(nameWidth));
562
- const target = colors.dim(result.target.padEnd(targetWidth));
563
- const outcome = result.ok ? `${colors.green(String(result.status))} ${colors.dim(`${result.latencyMs}ms`)}` : colors.red(result.error ?? "down");
564
- return `${mark} ${name} ${target} ${outcome}`;
565
- }).join("\n");
566
- }
567
- function formatServices(config, colors) {
568
- const entries = Object.entries(config.services);
569
- const nameWidth = width(entries.map(([name]) => name));
570
- return entries.map(([name, service]) => {
571
- const expect = service.expect === void 0 ? "" : colors.dim(` expect ${service.expect}`);
572
- return `${colors.bold(name.padEnd(nameWidth))} ${targetUrl(service)}${expect}`;
573
- }).join("\n");
550
+ async function readSkippedServices(paths) {
551
+ const text = await readLocal(paths);
552
+ if (text === null) return /* @__PURE__ */ new Set();
553
+ const doc = parseDocument2(text);
554
+ if (doc.errors.length > 0) return /* @__PURE__ */ new Set();
555
+ const services = doc.get(SERVICES, true);
556
+ if (!isMap2(services)) return /* @__PURE__ */ new Set();
557
+ const names = services.items.filter((pair) => isMap2(pair.value) && pair.value.get(SKIP) === true).map((pair) => String(isScalar2(pair.key) ? pair.key.value : pair.key));
558
+ return new Set(names);
559
+ }
560
+ function writeServiceSkip(paths, name, skip) {
561
+ return editLocal(paths, (doc) => {
562
+ const services = doc.get(SERVICES, true);
563
+ if (services !== void 0 && !isMap2(services)) return false;
564
+ const entry = services?.get(name, true);
565
+ if (entry !== void 0 && !isMap2(entry)) return false;
566
+ if (skip) {
567
+ if (entry?.get(SKIP) === true) return null;
568
+ doc.setIn([SERVICES, name, SKIP], true);
569
+ return true;
570
+ }
571
+ if (!services || !entry?.has(SKIP)) return null;
572
+ entry.delete(SKIP);
573
+ if (entry.items.length === 0) services.delete(name);
574
+ if (services.items.length === 0) doc.delete(SERVICES);
575
+ return true;
576
+ });
574
577
  }
575
578
 
576
579
  // src/commands/check.ts
577
- var MAX_TIMEOUT_SECONDS = 2147483;
578
580
  var check_default = defineCommand2({
579
581
  meta: { name: "check", description: "Check that services respond (all of them by default)" },
580
582
  args: {
@@ -586,21 +588,16 @@ var check_default = defineCommand2({
586
588
  }
587
589
  },
588
590
  async run({ args }) {
589
- const seconds = Number(args.timeout);
590
- if (!Number.isFinite(seconds) || seconds <= 0 || seconds > MAX_TIMEOUT_SECONDS) {
591
- throw new ProbeError(
592
- "USAGE",
593
- `--timeout expects seconds between 0 and ${MAX_TIMEOUT_SECONDS}, got "${args.timeout}".`
594
- );
595
- }
596
- const { config } = await openProject();
597
- const results = await runChecks(config, args._, { timeoutMs: Math.ceil(seconds * 1e3) });
591
+ const timeoutMs = secondsToMs("--timeout", args.timeout);
592
+ const { config, paths } = await openProject();
593
+ const skipped = await readSkippedServices(paths);
594
+ const results = await runChecks(config, args._, { timeoutMs }, skipped);
598
595
  if (args.json) {
599
596
  console.log(JSON.stringify(results, null, 2));
600
597
  } else if (results.length === 0) {
601
598
  console.log("No services yet. Add one: probe add api -p 3000");
602
599
  } else {
603
- console.log(formatCheckResults(results, stdoutColors()));
600
+ console.log(formatCheckResults(results, stdoutTerminal()));
604
601
  }
605
602
  if (results.some((result) => !result.ok)) process.exitCode = EXIT_DOWN;
606
603
  }
@@ -608,8 +605,8 @@ var check_default = defineCommand2({
608
605
 
609
606
  // src/commands/init.ts
610
607
  import { existsSync } from "fs";
611
- import { mkdir, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
612
- import { join as join4 } from "path";
608
+ import { mkdir, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
609
+ import { join as join5 } from "path";
613
610
  import { defineCommand as defineCommand3 } from "citty";
614
611
  var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
615
612
  var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
@@ -617,19 +614,19 @@ schemaVersion: ${CURRENT_SCHEMA_VERSION}
617
614
  services: {}
618
615
  `;
619
616
  async function ignoreLocalFiles(root) {
620
- const file = join4(root, ".gitignore");
621
- const current = existsSync(file) ? await readFile4(file, "utf8") : "";
617
+ const file = join5(root, ".gitignore");
618
+ const current = existsSync(file) ? await readFile5(file, "utf8") : "";
622
619
  const lines = current.split("\n").map((line) => line.trim());
623
620
  const missing = IGNORED.filter((entry) => !lines.includes(entry));
624
621
  if (missing.length === 0) return;
625
622
  const separator = current === "" || current.endsWith("\n") ? "" : "\n";
626
- await writeFile4(file, `${current}${separator}${missing.join("\n")}
623
+ await writeFile5(file, `${current}${separator}${missing.join("\n")}
627
624
  `);
628
625
  }
629
626
  var init_default = defineCommand3({
630
627
  meta: {
631
628
  name: "init",
632
- description: "Create .probe/, the .gitignore entries and the Makefile block"
629
+ description: "Create .probe/ and the .gitignore entries"
633
630
  },
634
631
  async run() {
635
632
  const paths = pathsFor(process.cwd());
@@ -637,11 +634,9 @@ var init_default = defineCommand3({
637
634
  throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
638
635
  }
639
636
  await mkdir(paths.dir);
640
- await writeFile4(paths.config, INITIAL_CONFIG);
637
+ await writeFile5(paths.config, INITIAL_CONFIG);
641
638
  await ignoreLocalFiles(paths.root);
642
- const { config } = await loadConfig(paths);
643
- await syncMakefile(paths, config);
644
- console.log(`Created ${PROBE_DIR}/ and the Makefile block. Next: probe add api -p 3000`);
639
+ console.log(`Created ${PROBE_DIR}/. Next: probe add api -p 3000`);
645
640
  }
646
641
  });
647
642
 
@@ -650,12 +645,12 @@ import { defineCommand as defineCommand4 } from "citty";
650
645
  var ls_default = defineCommand4({
651
646
  meta: { name: "ls", description: "List services" },
652
647
  async run() {
653
- const { config } = await openProject();
648
+ const { config, paths } = await openProject();
654
649
  if (Object.keys(config.services).length === 0) {
655
650
  console.log("No services yet. Add one: probe add api -p 3000");
656
651
  return;
657
652
  }
658
- console.log(formatServices(config, stdoutColors()));
653
+ console.log(formatServices(config, stdoutTerminal(), await readSkippedServices(paths)));
659
654
  }
660
655
  });
661
656
 
@@ -699,16 +694,16 @@ import { defineCommand as defineCommand7 } from "citty";
699
694
  import { spawn } from "child_process";
700
695
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
701
696
  import { homedir } from "os";
702
- import { join as join5 } from "path";
697
+ import { join as join6 } from "path";
703
698
  function cacheDir(env = process.env) {
704
- return join5(env.XDG_CACHE_HOME || join5(homedir(), ".cache"), "probe");
699
+ return join6(env.XDG_CACHE_HOME || join6(homedir(), ".cache"), "probe");
705
700
  }
706
701
  function updateCheckDisabled(env = process.env) {
707
702
  return env.PROBE_NO_UPDATE_CHECK === "1" || Boolean(env.CI);
708
703
  }
709
704
  function readCache(dir) {
710
705
  try {
711
- const cache = JSON.parse(readFileSync(join5(dir, UPDATE_CACHE_FILE), "utf8"));
706
+ const cache = JSON.parse(readFileSync(join6(dir, UPDATE_CACHE_FILE), "utf8"));
712
707
  return typeof cache.checkedAt === "number" ? cache : null;
713
708
  } catch {
714
709
  return null;
@@ -716,7 +711,7 @@ function readCache(dir) {
716
711
  }
717
712
  function writeCache(dir, cache) {
718
713
  mkdirSync(dir, { recursive: true });
719
- writeFileSync(join5(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
714
+ writeFileSync(join6(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
720
715
  }
721
716
  function isNewer(latest, current) {
722
717
  const parse2 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
@@ -772,7 +767,7 @@ function describeBlock(state, colors) {
772
767
  case "outdated":
773
768
  return colors.yellow("out of date \u2192 run `probe sync`");
774
769
  case "missing":
775
- return colors.yellow("no probe block \u2192 run `probe sync`");
770
+ return colors.dim("no probe block (optional) \u2192 `probe sync` adds one");
776
771
  }
777
772
  }
778
773
  function describeUpdate(colors) {
@@ -806,7 +801,10 @@ var status_default = defineCommand7({
806
801
  import { relative as relative2 } from "path";
807
802
  import { defineCommand as defineCommand8 } from "citty";
808
803
  var sync_default = defineCommand8({
809
- meta: { name: "sync", description: "Regenerate the probe block of the Makefile" },
804
+ meta: {
805
+ name: "sync",
806
+ description: "Add or regenerate the probe block of the Makefile (optional)"
807
+ },
810
808
  async run() {
811
809
  const { config, paths } = await openProject();
812
810
  const { created, changed } = await syncMakefile(paths, config);
@@ -817,42 +815,219 @@ var sync_default = defineCommand8({
817
815
  }
818
816
  });
819
817
 
818
+ // src/commands/wait.ts
819
+ import { defineCommand as defineCommand9 } from "citty";
820
+
821
+ // src/checkers/poll.ts
822
+ import { setTimeout as delay } from "timers/promises";
823
+ async function pollUntilUp(check, options) {
824
+ const { deadlineMs, attemptTimeoutMs, maxIntervalMs, onPass } = options;
825
+ const now = options.now ?? (() => performance.now());
826
+ const sleep = options.sleep ?? delay;
827
+ const started = now();
828
+ const remaining = () => deadlineMs - (now() - started);
829
+ let pause = Math.min(options.intervalMs, maxIntervalMs);
830
+ let lastPass = false;
831
+ while (true) {
832
+ const results = await check(Math.max(1, Math.ceil(Math.min(attemptTimeoutMs, remaining()))));
833
+ const elapsedMs = Math.round(now() - started);
834
+ onPass?.(results, elapsedMs);
835
+ 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) {
839
+ lastPass = true;
840
+ await sleep(left - Math.min(WAIT_LAST_PASS_MS, left / 2));
841
+ } else {
842
+ await sleep(pause);
843
+ pause = Math.min(pause * WAIT_BACKOFF_FACTOR, maxIntervalMs);
844
+ }
845
+ }
846
+ }
847
+
848
+ // src/commands/wait.ts
849
+ var seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
850
+ function progressReporter() {
851
+ let previous = null;
852
+ return (results, elapsedMs) => {
853
+ const lines = [];
854
+ if (previous === null) {
855
+ const down = results.filter((result) => !result.ok).map((result) => result.name);
856
+ if (down.length > 0) lines.push(`waiting for ${down.join(", ")}`);
857
+ } else {
858
+ for (const { name, ok } of results) {
859
+ if (previous.get(name) === ok) continue;
860
+ lines.push(ok ? `${name} up after ${seconds(elapsedMs)}` : `${name} down again`);
861
+ }
862
+ }
863
+ previous = new Map(results.map((result) => [result.name, result.ok]));
864
+ return lines;
865
+ };
866
+ }
867
+ var wait_default = defineCommand9({
868
+ meta: {
869
+ name: "wait",
870
+ description: "Check again and again until services are up (all of them by default)"
871
+ },
872
+ args: {
873
+ json: { type: "boolean", description: "Print the final results as JSON" },
874
+ timeout: {
875
+ type: "string",
876
+ description: "Seconds to wait in total, all checks included, before giving up",
877
+ default: String(DEFAULT_WAIT_TIMEOUT_SECONDS)
878
+ },
879
+ interval: {
880
+ type: "string",
881
+ description: "Seconds before the second check; the pause then doubles after each check",
882
+ default: String(DEFAULT_WAIT_INTERVAL_SECONDS)
883
+ },
884
+ "max-interval": {
885
+ type: "string",
886
+ description: "Longest pause between two checks, in seconds",
887
+ default: String(DEFAULT_WAIT_MAX_INTERVAL_SECONDS)
888
+ }
889
+ },
890
+ async run({ args }) {
891
+ const deadlineMs = secondsToMs("--timeout", args.timeout);
892
+ const intervalMs = secondsToMs("--interval", args.interval);
893
+ const maxIntervalMs = secondsToMs("--max-interval", args["max-interval"]);
894
+ if (intervalMs > maxIntervalMs) {
895
+ throw new ProbeError(
896
+ "USAGE",
897
+ `--interval (${args.interval}s) cannot be longer than --max-interval (${args["max-interval"]}s).`
898
+ );
899
+ }
900
+ const { config, paths } = await openProject();
901
+ const skipped = await readSkippedServices(paths);
902
+ const report = progressReporter();
903
+ const { results, timedOut } = await pollUntilUp(
904
+ (timeoutMs) => runChecks(config, args._, { timeoutMs }, skipped),
905
+ {
906
+ deadlineMs,
907
+ attemptTimeoutMs: DEFAULT_TIMEOUT_SECONDS * 1e3,
908
+ intervalMs,
909
+ maxIntervalMs,
910
+ onPass: (pass, elapsedMs) => {
911
+ for (const line of report(pass, elapsedMs)) console.error(line);
912
+ }
913
+ }
914
+ );
915
+ if (args.json) {
916
+ console.log(JSON.stringify(results, null, 2));
917
+ } else if (results.length === 0) {
918
+ console.log("No services yet. Add one: probe add api -p 3000");
919
+ } else {
920
+ console.log(formatCheckResults(results, stdoutTerminal()));
921
+ }
922
+ if (timedOut) {
923
+ const down = results.filter((result) => !result.ok).map((result) => result.name);
924
+ console.error(`probe: still down after ${deadlineMs / 1e3}s: ${down.join(", ")}`);
925
+ process.exitCode = EXIT_DOWN;
926
+ }
927
+ }
928
+ });
929
+
820
930
  // src/cli.ts
821
- var main = defineCommand9({
931
+ var main = defineCommand10({
822
932
  meta: {
823
933
  name: "probe",
824
934
  version: CLI_VERSION,
825
935
  description: "Health checks for your local dev services"
826
936
  },
827
- subCommands: { init: init_default, add: add_default, rm: rm_default, ls: ls_default, check: check_default, status: status_default, sync: sync_default, migrate: migrate_default }
937
+ args: {
938
+ interval: {
939
+ type: "string",
940
+ valueHint: "seconds",
941
+ description: "Seconds between two refreshes of the dashboard (probe alone, on a terminal)",
942
+ default: String(DASHBOARD_INTERVAL_SECONDS)
943
+ }
944
+ },
945
+ subCommands: { init: init_default, add: add_default, rm: rm_default, ls: ls_default, check: check_default, wait: wait_default, status: status_default, sync: sync_default, migrate: migrate_default }
828
946
  });
947
+ async function printUsage(command, parent) {
948
+ const usage = await renderUsage(command, parent);
949
+ console.log(`${stdoutTerminal().color ? usage : stripStyles(usage)}
950
+ `);
951
+ }
829
952
  function notifyUpdate(rawArgs) {
830
953
  const cliPath = process.argv[1];
831
954
  if (updateCheckDisabled() || !cliPath) return;
832
955
  scheduleRefresh(cacheDir(), cliPath);
833
- if (!process.stderr.isTTY || rawArgs.includes("--json") || rawArgs[0] === "status") return;
956
+ if (!detectTerminal(process.stderr, process.env).isTTY || rawArgs.includes("--json") || rawArgs[0] === "status")
957
+ return;
834
958
  const notice = updateNotice(CLI_VERSION, cacheDir());
835
959
  if (notice) console.error(`
836
960
  ${notice}`);
837
961
  }
962
+ var INTERVAL_OPTION = "--interval";
963
+ function dashboardInterval(rawArgs) {
964
+ const [first, second, ...rest] = rawArgs;
965
+ if (first === void 0) return void 0;
966
+ if (first.startsWith(`${INTERVAL_OPTION}=`) && second === void 0)
967
+ return first.slice(INTERVAL_OPTION.length + 1);
968
+ if (first === INTERVAL_OPTION && rest.length === 0) return second ?? "";
969
+ return null;
970
+ }
971
+ async function openedDashboard(rawArgs) {
972
+ const interval = dashboardInterval(rawArgs);
973
+ if (interval === null) return false;
974
+ const asked = interval !== void 0;
975
+ const askedMs = asked ? secondsToMs(INTERVAL_OPTION, interval) : void 0;
976
+ if (!isInteractive(process.stdin, process.stdout, process.env)) {
977
+ if (!asked) return false;
978
+ throw new ProbeError(
979
+ "USAGE",
980
+ `${INTERVAL_OPTION} sets the refresh of the dashboard, which needs an interactive terminal.`
981
+ );
982
+ }
983
+ const paths = asked ? requireProbeDir(process.cwd()) : findProbeDir(process.cwd());
984
+ if (!paths) return false;
985
+ const { config } = await loadConfig(paths);
986
+ if (Object.keys(config.services).length === 0) {
987
+ if (!asked) return false;
988
+ throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
989
+ }
990
+ const { openDashboard } = await import("./open-TQ6NPJ7V.js");
991
+ let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
992
+ let saving = Promise.resolve();
993
+ const save = (write) => {
994
+ saving = saving.then(write).catch(() => {
995
+ });
996
+ };
997
+ await openDashboard(config, stdoutTerminal(), {
998
+ settings,
999
+ skipped: await readSkippedServices(paths),
1000
+ onSettingsChange: (next) => {
1001
+ const changed = changedPreferences(settings, next);
1002
+ settings = next;
1003
+ save(() => writeDashboardPreferences(paths, changed));
1004
+ },
1005
+ onSkipChange: (name, skip) => save(() => writeServiceSkip(paths, name, skip))
1006
+ });
1007
+ await saving;
1008
+ return true;
1009
+ }
838
1010
  async function run(rawArgs) {
839
1011
  if (rawArgs[0] === REFRESH_COMMAND) return refreshCache(cacheDir());
840
1012
  const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
841
1013
  const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
842
- if (wantsHelp || wantsVersion) return runMain(main, { rawArgs });
1014
+ if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
1015
+ let dashboard = false;
843
1016
  try {
844
- await runCommand(main, { rawArgs });
1017
+ dashboard = await openedDashboard(rawArgs);
1018
+ if (!dashboard) await runCommand(main, { rawArgs });
845
1019
  } catch (error) {
846
1020
  process.exitCode = EXIT_ERROR;
847
1021
  if (error instanceof ProbeError) {
848
1022
  console.error(`probe: ${error.message}`);
849
1023
  } else if (error instanceof Error && error.name === "CLIError") {
850
- if (rawArgs.length === 0) await showUsage(main);
1024
+ if (rawArgs.length === 0) await printUsage(main);
851
1025
  console.error(`probe: ${error.message.replace(/\.$/, "")}. See: probe --help`);
852
1026
  } else {
853
1027
  console.error(`probe: ${error instanceof Error ? error.message : String(error)}`);
854
1028
  }
855
1029
  }
856
1030
  notifyUpdate(rawArgs);
1031
+ if (dashboard) process.exit();
857
1032
  }
858
1033
  await run(process.argv.slice(2));