@ninhache/probe 0.1.2 → 0.2.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.2.0" : "0.0.0-dev";
296
309
 
297
310
  // src/makefile/generator.ts
298
311
  import { createHash } from "crypto";
@@ -382,27 +395,25 @@ 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 ?? {};
@@ -460,121 +471,111 @@ var add_default = defineCommand({
460
471
  // src/commands/check.ts
461
472
  import { defineCommand as defineCommand2 } from "citty";
462
473
 
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
- }
474
+ // src/config/local.ts
475
+ import { chmod, readFile as readFile4, realpath, rename, rm, stat, writeFile as writeFile4 } from "fs/promises";
476
+ import { dirname as dirname2, join as join4 } from "path";
477
+ import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
478
+ var SECTION = "dashboard";
479
+ var KEYS = ["interval", "timeout"];
480
+ var SERVICES = "services";
481
+ var SKIP = "skip";
482
+ function isDelay(value) {
483
+ return typeof value === "number" && value > 0 && value <= MAX_TIMEOUT_SECONDS;
484
+ }
485
+ async function readLocal(paths) {
486
+ try {
487
+ return await readFile4(paths.local, "utf8");
488
+ } catch {
489
+ return null;
518
490
  }
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}.`);
491
+ }
492
+ async function readDashboardPreferences(paths) {
493
+ const text = await readLocal(paths);
494
+ if (text === null) return {};
495
+ const doc = parseDocument2(text);
496
+ if (doc.errors.length > 0) return {};
497
+ const preferences = {};
498
+ for (const key of KEYS) {
499
+ const value = doc.getIn([SECTION, key]);
500
+ if (isDelay(value)) preferences[key] = value;
528
501
  }
529
- return checker;
502
+ return preferences;
530
503
  }
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
- );
504
+ function initialDashboardSettings(askedIntervalMs, saved) {
505
+ return {
506
+ intervalMs: askedIntervalMs ?? (saved.interval ?? DASHBOARD_INTERVAL_SECONDS) * 1e3,
507
+ timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
508
+ };
509
+ }
510
+ function changedPreferences(previous, next) {
511
+ const changed = {};
512
+ if (next.intervalMs !== previous.intervalMs) changed.interval = next.intervalMs / 1e3;
513
+ if (next.timeoutMs !== previous.timeoutMs) changed.timeout = next.timeoutMs / 1e3;
514
+ return changed;
515
+ }
516
+ async function editLocal(paths, edit) {
517
+ const doc = parseDocument2(await readLocal(paths) ?? "");
518
+ if (doc.errors.length > 0) return false;
519
+ if (doc.contents !== null && !isMap2(doc.contents)) return false;
520
+ const outcome = edit(doc);
521
+ if (outcome !== true) return outcome === null;
522
+ const { contents } = doc;
523
+ const empty = isMap2(contents) && contents.items.length === 0 && !contents.comment;
524
+ const text = empty && !doc.comment && !doc.commentBefore ? "" : doc.toString();
525
+ const target = await realpath(paths.local).catch(() => paths.local);
526
+ const existing = await stat(target).catch(() => null);
527
+ const mode = existing ? existing.mode & 511 : LOCAL_FILE_MODE;
528
+ const draft = join4(dirname2(target), LOCAL_DRAFT_FILE);
529
+ try {
530
+ await writeFile4(draft, text, { mode });
531
+ await chmod(draft, mode);
532
+ await rename(draft, target);
533
+ } catch (error) {
534
+ await rm(draft, { force: true });
535
+ throw error;
541
536
  }
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
- );
537
+ return true;
538
+ }
539
+ function writeDashboardPreferences(paths, preferences) {
540
+ return editLocal(paths, (doc) => {
541
+ const section = doc.get(SECTION, true);
542
+ if (section !== void 0 && !isMap2(section)) return false;
543
+ for (const key of KEYS) {
544
+ if (preferences[key] !== void 0) doc.setIn([SECTION, key], preferences[key]);
545
+ }
546
+ return true;
547
+ });
550
548
  }
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");
549
+ async function readSkippedServices(paths) {
550
+ const text = await readLocal(paths);
551
+ if (text === null) return /* @__PURE__ */ new Set();
552
+ const doc = parseDocument2(text);
553
+ if (doc.errors.length > 0) return /* @__PURE__ */ new Set();
554
+ const services = doc.get(SERVICES, true);
555
+ if (!isMap2(services)) return /* @__PURE__ */ new Set();
556
+ 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));
557
+ return new Set(names);
558
+ }
559
+ function writeServiceSkip(paths, name, skip) {
560
+ return editLocal(paths, (doc) => {
561
+ const services = doc.get(SERVICES, true);
562
+ if (services !== void 0 && !isMap2(services)) return false;
563
+ const entry = services?.get(name, true);
564
+ if (entry !== void 0 && !isMap2(entry)) return false;
565
+ if (skip) {
566
+ if (entry?.get(SKIP) === true) return null;
567
+ doc.setIn([SERVICES, name, SKIP], true);
568
+ return true;
569
+ }
570
+ if (!services || !entry?.has(SKIP)) return null;
571
+ entry.delete(SKIP);
572
+ if (entry.items.length === 0) services.delete(name);
573
+ if (services.items.length === 0) doc.delete(SERVICES);
574
+ return true;
575
+ });
574
576
  }
575
577
 
576
578
  // src/commands/check.ts
577
- var MAX_TIMEOUT_SECONDS = 2147483;
578
579
  var check_default = defineCommand2({
579
580
  meta: { name: "check", description: "Check that services respond (all of them by default)" },
580
581
  args: {
@@ -586,21 +587,16 @@ var check_default = defineCommand2({
586
587
  }
587
588
  },
588
589
  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) });
590
+ const timeoutMs = secondsToMs("--timeout", args.timeout);
591
+ const { config, paths } = await openProject();
592
+ const skipped = await readSkippedServices(paths);
593
+ const results = await runChecks(config, args._, { timeoutMs }, skipped);
598
594
  if (args.json) {
599
595
  console.log(JSON.stringify(results, null, 2));
600
596
  } else if (results.length === 0) {
601
597
  console.log("No services yet. Add one: probe add api -p 3000");
602
598
  } else {
603
- console.log(formatCheckResults(results, stdoutColors()));
599
+ console.log(formatCheckResults(results, stdoutTerminal()));
604
600
  }
605
601
  if (results.some((result) => !result.ok)) process.exitCode = EXIT_DOWN;
606
602
  }
@@ -608,8 +604,8 @@ var check_default = defineCommand2({
608
604
 
609
605
  // src/commands/init.ts
610
606
  import { existsSync } from "fs";
611
- import { mkdir, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
612
- import { join as join4 } from "path";
607
+ import { mkdir, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
608
+ import { join as join5 } from "path";
613
609
  import { defineCommand as defineCommand3 } from "citty";
614
610
  var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
615
611
  var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
@@ -617,13 +613,13 @@ schemaVersion: ${CURRENT_SCHEMA_VERSION}
617
613
  services: {}
618
614
  `;
619
615
  async function ignoreLocalFiles(root) {
620
- const file = join4(root, ".gitignore");
621
- const current = existsSync(file) ? await readFile4(file, "utf8") : "";
616
+ const file = join5(root, ".gitignore");
617
+ const current = existsSync(file) ? await readFile5(file, "utf8") : "";
622
618
  const lines = current.split("\n").map((line) => line.trim());
623
619
  const missing = IGNORED.filter((entry) => !lines.includes(entry));
624
620
  if (missing.length === 0) return;
625
621
  const separator = current === "" || current.endsWith("\n") ? "" : "\n";
626
- await writeFile4(file, `${current}${separator}${missing.join("\n")}
622
+ await writeFile5(file, `${current}${separator}${missing.join("\n")}
627
623
  `);
628
624
  }
629
625
  var init_default = defineCommand3({
@@ -637,7 +633,7 @@ var init_default = defineCommand3({
637
633
  throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
638
634
  }
639
635
  await mkdir(paths.dir);
640
- await writeFile4(paths.config, INITIAL_CONFIG);
636
+ await writeFile5(paths.config, INITIAL_CONFIG);
641
637
  await ignoreLocalFiles(paths.root);
642
638
  const { config } = await loadConfig(paths);
643
639
  await syncMakefile(paths, config);
@@ -650,12 +646,12 @@ import { defineCommand as defineCommand4 } from "citty";
650
646
  var ls_default = defineCommand4({
651
647
  meta: { name: "ls", description: "List services" },
652
648
  async run() {
653
- const { config } = await openProject();
649
+ const { config, paths } = await openProject();
654
650
  if (Object.keys(config.services).length === 0) {
655
651
  console.log("No services yet. Add one: probe add api -p 3000");
656
652
  return;
657
653
  }
658
- console.log(formatServices(config, stdoutColors()));
654
+ console.log(formatServices(config, stdoutTerminal(), await readSkippedServices(paths)));
659
655
  }
660
656
  });
661
657
 
@@ -699,16 +695,16 @@ import { defineCommand as defineCommand7 } from "citty";
699
695
  import { spawn } from "child_process";
700
696
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
701
697
  import { homedir } from "os";
702
- import { join as join5 } from "path";
698
+ import { join as join6 } from "path";
703
699
  function cacheDir(env = process.env) {
704
- return join5(env.XDG_CACHE_HOME || join5(homedir(), ".cache"), "probe");
700
+ return join6(env.XDG_CACHE_HOME || join6(homedir(), ".cache"), "probe");
705
701
  }
706
702
  function updateCheckDisabled(env = process.env) {
707
703
  return env.PROBE_NO_UPDATE_CHECK === "1" || Boolean(env.CI);
708
704
  }
709
705
  function readCache(dir) {
710
706
  try {
711
- const cache = JSON.parse(readFileSync(join5(dir, UPDATE_CACHE_FILE), "utf8"));
707
+ const cache = JSON.parse(readFileSync(join6(dir, UPDATE_CACHE_FILE), "utf8"));
712
708
  return typeof cache.checkedAt === "number" ? cache : null;
713
709
  } catch {
714
710
  return null;
@@ -716,7 +712,7 @@ function readCache(dir) {
716
712
  }
717
713
  function writeCache(dir, cache) {
718
714
  mkdirSync(dir, { recursive: true });
719
- writeFileSync(join5(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
715
+ writeFileSync(join6(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
720
716
  }
721
717
  function isNewer(latest, current) {
722
718
  const parse2 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
@@ -817,42 +813,219 @@ var sync_default = defineCommand8({
817
813
  }
818
814
  });
819
815
 
816
+ // src/commands/wait.ts
817
+ import { defineCommand as defineCommand9 } from "citty";
818
+
819
+ // src/checkers/poll.ts
820
+ import { setTimeout as delay } from "timers/promises";
821
+ async function pollUntilUp(check, options) {
822
+ const { deadlineMs, attemptTimeoutMs, maxIntervalMs, onPass } = options;
823
+ const now = options.now ?? (() => performance.now());
824
+ const sleep = options.sleep ?? delay;
825
+ const started = now();
826
+ const remaining = () => deadlineMs - (now() - started);
827
+ let pause = Math.min(options.intervalMs, maxIntervalMs);
828
+ let lastPass = false;
829
+ while (true) {
830
+ const results = await check(Math.max(1, Math.ceil(Math.min(attemptTimeoutMs, remaining()))));
831
+ const elapsedMs = Math.round(now() - started);
832
+ onPass?.(results, elapsedMs);
833
+ if (results.every((result) => result.ok)) return { results, elapsedMs, timedOut: false };
834
+ const left = remaining();
835
+ if (lastPass || left <= 0) return { results, elapsedMs, timedOut: true };
836
+ if (pause >= left) {
837
+ lastPass = true;
838
+ await sleep(left - Math.min(WAIT_LAST_PASS_MS, left / 2));
839
+ } else {
840
+ await sleep(pause);
841
+ pause = Math.min(pause * WAIT_BACKOFF_FACTOR, maxIntervalMs);
842
+ }
843
+ }
844
+ }
845
+
846
+ // src/commands/wait.ts
847
+ var seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
848
+ function progressReporter() {
849
+ let previous = null;
850
+ return (results, elapsedMs) => {
851
+ const lines = [];
852
+ if (previous === null) {
853
+ const down = results.filter((result) => !result.ok).map((result) => result.name);
854
+ if (down.length > 0) lines.push(`waiting for ${down.join(", ")}`);
855
+ } else {
856
+ for (const { name, ok } of results) {
857
+ if (previous.get(name) === ok) continue;
858
+ lines.push(ok ? `${name} up after ${seconds(elapsedMs)}` : `${name} down again`);
859
+ }
860
+ }
861
+ previous = new Map(results.map((result) => [result.name, result.ok]));
862
+ return lines;
863
+ };
864
+ }
865
+ var wait_default = defineCommand9({
866
+ meta: {
867
+ name: "wait",
868
+ description: "Check again and again until services are up (all of them by default)"
869
+ },
870
+ args: {
871
+ json: { type: "boolean", description: "Print the final results as JSON" },
872
+ timeout: {
873
+ type: "string",
874
+ description: "Seconds to wait in total, all checks included, before giving up",
875
+ default: String(DEFAULT_WAIT_TIMEOUT_SECONDS)
876
+ },
877
+ interval: {
878
+ type: "string",
879
+ description: "Seconds before the second check; the pause then doubles after each check",
880
+ default: String(DEFAULT_WAIT_INTERVAL_SECONDS)
881
+ },
882
+ "max-interval": {
883
+ type: "string",
884
+ description: "Longest pause between two checks, in seconds",
885
+ default: String(DEFAULT_WAIT_MAX_INTERVAL_SECONDS)
886
+ }
887
+ },
888
+ async run({ args }) {
889
+ const deadlineMs = secondsToMs("--timeout", args.timeout);
890
+ const intervalMs = secondsToMs("--interval", args.interval);
891
+ const maxIntervalMs = secondsToMs("--max-interval", args["max-interval"]);
892
+ if (intervalMs > maxIntervalMs) {
893
+ throw new ProbeError(
894
+ "USAGE",
895
+ `--interval (${args.interval}s) cannot be longer than --max-interval (${args["max-interval"]}s).`
896
+ );
897
+ }
898
+ const { config, paths } = await openProject();
899
+ const skipped = await readSkippedServices(paths);
900
+ const report = progressReporter();
901
+ const { results, timedOut } = await pollUntilUp(
902
+ (timeoutMs) => runChecks(config, args._, { timeoutMs }, skipped),
903
+ {
904
+ deadlineMs,
905
+ attemptTimeoutMs: DEFAULT_TIMEOUT_SECONDS * 1e3,
906
+ intervalMs,
907
+ maxIntervalMs,
908
+ onPass: (pass, elapsedMs) => {
909
+ for (const line of report(pass, elapsedMs)) console.error(line);
910
+ }
911
+ }
912
+ );
913
+ if (args.json) {
914
+ console.log(JSON.stringify(results, null, 2));
915
+ } else if (results.length === 0) {
916
+ console.log("No services yet. Add one: probe add api -p 3000");
917
+ } else {
918
+ console.log(formatCheckResults(results, stdoutTerminal()));
919
+ }
920
+ if (timedOut) {
921
+ const down = results.filter((result) => !result.ok).map((result) => result.name);
922
+ console.error(`probe: still down after ${deadlineMs / 1e3}s: ${down.join(", ")}`);
923
+ process.exitCode = EXIT_DOWN;
924
+ }
925
+ }
926
+ });
927
+
820
928
  // src/cli.ts
821
- var main = defineCommand9({
929
+ var main = defineCommand10({
822
930
  meta: {
823
931
  name: "probe",
824
932
  version: CLI_VERSION,
825
933
  description: "Health checks for your local dev services"
826
934
  },
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 }
935
+ args: {
936
+ interval: {
937
+ type: "string",
938
+ valueHint: "seconds",
939
+ description: "Seconds between two refreshes of the dashboard (probe alone, on a terminal)",
940
+ default: String(DASHBOARD_INTERVAL_SECONDS)
941
+ }
942
+ },
943
+ 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
944
  });
945
+ async function printUsage(command, parent) {
946
+ const usage = await renderUsage(command, parent);
947
+ console.log(`${stdoutTerminal().color ? usage : stripStyles(usage)}
948
+ `);
949
+ }
829
950
  function notifyUpdate(rawArgs) {
830
951
  const cliPath = process.argv[1];
831
952
  if (updateCheckDisabled() || !cliPath) return;
832
953
  scheduleRefresh(cacheDir(), cliPath);
833
- if (!process.stderr.isTTY || rawArgs.includes("--json") || rawArgs[0] === "status") return;
954
+ if (!detectTerminal(process.stderr, process.env).isTTY || rawArgs.includes("--json") || rawArgs[0] === "status")
955
+ return;
834
956
  const notice = updateNotice(CLI_VERSION, cacheDir());
835
957
  if (notice) console.error(`
836
958
  ${notice}`);
837
959
  }
960
+ var INTERVAL_OPTION = "--interval";
961
+ function dashboardInterval(rawArgs) {
962
+ const [first, second, ...rest] = rawArgs;
963
+ if (first === void 0) return void 0;
964
+ if (first.startsWith(`${INTERVAL_OPTION}=`) && second === void 0)
965
+ return first.slice(INTERVAL_OPTION.length + 1);
966
+ if (first === INTERVAL_OPTION && rest.length === 0) return second ?? "";
967
+ return null;
968
+ }
969
+ async function openedDashboard(rawArgs) {
970
+ const interval = dashboardInterval(rawArgs);
971
+ if (interval === null) return false;
972
+ const asked = interval !== void 0;
973
+ const askedMs = asked ? secondsToMs(INTERVAL_OPTION, interval) : void 0;
974
+ if (!isInteractive(process.stdin, process.stdout, process.env)) {
975
+ if (!asked) return false;
976
+ throw new ProbeError(
977
+ "USAGE",
978
+ `${INTERVAL_OPTION} sets the refresh of the dashboard, which needs an interactive terminal.`
979
+ );
980
+ }
981
+ const paths = asked ? requireProbeDir(process.cwd()) : findProbeDir(process.cwd());
982
+ if (!paths) return false;
983
+ const { config } = await loadConfig(paths);
984
+ if (Object.keys(config.services).length === 0) {
985
+ if (!asked) return false;
986
+ throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
987
+ }
988
+ const { openDashboard } = await import("./open-TQ6NPJ7V.js");
989
+ let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
990
+ let saving = Promise.resolve();
991
+ const save = (write) => {
992
+ saving = saving.then(write).catch(() => {
993
+ });
994
+ };
995
+ await openDashboard(config, stdoutTerminal(), {
996
+ settings,
997
+ skipped: await readSkippedServices(paths),
998
+ onSettingsChange: (next) => {
999
+ const changed = changedPreferences(settings, next);
1000
+ settings = next;
1001
+ save(() => writeDashboardPreferences(paths, changed));
1002
+ },
1003
+ onSkipChange: (name, skip) => save(() => writeServiceSkip(paths, name, skip))
1004
+ });
1005
+ await saving;
1006
+ return true;
1007
+ }
838
1008
  async function run(rawArgs) {
839
1009
  if (rawArgs[0] === REFRESH_COMMAND) return refreshCache(cacheDir());
840
1010
  const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
841
1011
  const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
842
- if (wantsHelp || wantsVersion) return runMain(main, { rawArgs });
1012
+ if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
1013
+ let dashboard = false;
843
1014
  try {
844
- await runCommand(main, { rawArgs });
1015
+ dashboard = await openedDashboard(rawArgs);
1016
+ if (!dashboard) await runCommand(main, { rawArgs });
845
1017
  } catch (error) {
846
1018
  process.exitCode = EXIT_ERROR;
847
1019
  if (error instanceof ProbeError) {
848
1020
  console.error(`probe: ${error.message}`);
849
1021
  } else if (error instanceof Error && error.name === "CLIError") {
850
- if (rawArgs.length === 0) await showUsage(main);
1022
+ if (rawArgs.length === 0) await printUsage(main);
851
1023
  console.error(`probe: ${error.message.replace(/\.$/, "")}. See: probe --help`);
852
1024
  } else {
853
1025
  console.error(`probe: ${error instanceof Error ? error.message : String(error)}`);
854
1026
  }
855
1027
  }
856
1028
  notifyUpdate(rawArgs);
1029
+ if (dashboard) process.exit();
857
1030
  }
858
1031
  await run(process.argv.slice(2));