@ninhache/probe 0.1.1 → 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.1" : "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,34 +395,39 @@ 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) {
422
+ const edited = await blockState(loaded.paths, loaded.config) === "modified";
411
423
  const saved = await saveConfig(loaded, { ...loaded.raw, services });
412
- await syncMakefile(saved.paths, saved.config);
424
+ if (!edited) {
425
+ await syncMakefile(saved.paths, saved.config);
426
+ return;
427
+ }
428
+ console.error(
429
+ "probe: the Makefile block was edited by hand, left as is. Run `probe sync` to regenerate it."
430
+ );
413
431
  }
414
432
 
415
433
  // src/commands/add.ts
@@ -453,114 +471,111 @@ var add_default = defineCommand({
453
471
  // src/commands/check.ts
454
472
  import { defineCommand as defineCommand2 } from "citty";
455
473
 
456
- // src/checkers/http.ts
457
- var REASONS = {
458
- ECONNREFUSED: "connection refused",
459
- ECONNRESET: "connection reset",
460
- ENOTFOUND: "host not found",
461
- EAI_AGAIN: "host not found",
462
- EHOSTUNREACH: "host unreachable"
463
- };
464
- function targetUrl(service) {
465
- const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
466
- const host = bareIpv6 ? `[${service.host}]` : service.host;
467
- return `http://${host}:${service.port}${service.path}`;
468
- }
469
- function isExpected(expect, status) {
470
- if (expect === void 0) return status >= 200 && status < 300;
471
- return Array.isArray(expect) ? expect.includes(status) : expect === status;
472
- }
473
- function describeExpected(expect) {
474
- if (expect === void 0) return "2xx";
475
- return Array.isArray(expect) ? expect.join(" or ") : String(expect);
476
- }
477
- function failureReason(error, timeoutMs) {
478
- if (error instanceof Error && error.name === "TimeoutError") {
479
- return `timeout after ${timeoutMs / 1e3}s`;
480
- }
481
- const cause = error?.cause;
482
- const code = cause?.code ?? cause?.errors?.[0]?.code;
483
- if (code) return REASONS[code] ?? code;
484
- return error instanceof Error ? error.message : String(error);
485
- }
486
- var httpChecker = {
487
- type: "http",
488
- async check(name, service, { timeoutMs }) {
489
- const target = targetUrl(service);
490
- const base = { name, type: "http", target };
491
- const started = performance.now();
492
- const elapsed = () => Math.round(performance.now() - started);
493
- try {
494
- const response = await fetch(target, { signal: AbortSignal.timeout(timeoutMs) });
495
- const latencyMs = elapsed();
496
- await response.body?.cancel();
497
- const { status } = response;
498
- if (isExpected(service.expect, status)) return { ...base, ok: true, status, latencyMs };
499
- const error = `expected ${describeExpected(service.expect)}, got ${status}`;
500
- return { ...base, ok: false, status, latencyMs, error };
501
- } catch (error) {
502
- return { ...base, ok: false, latencyMs: elapsed(), error: failureReason(error, timeoutMs) };
503
- }
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;
504
490
  }
505
- };
506
-
507
- // src/checkers/registry.ts
508
- var checkers = new Map([httpChecker].map((checker) => [checker.type, checker]));
509
- function getChecker(type) {
510
- const checker = checkers.get(type);
511
- if (!checker) {
512
- const known = [...checkers.keys()].join(", ");
513
- 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;
514
501
  }
515
- return checker;
502
+ return preferences;
516
503
  }
517
-
518
- // src/checkers/run.ts
519
- async function runChecks(config, names, context) {
520
- const known = Object.keys(config.services);
521
- const unknown = names.filter((name) => !known.includes(name));
522
- if (unknown.length > 0) {
523
- throw new ProbeError(
524
- "SERVICE_UNKNOWN",
525
- `unknown service: ${unknown.join(", ")}. Known: ${known.join(", ") || "(none)"}.`
526
- );
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;
527
536
  }
528
- const selected = names.length > 0 ? [...new Set(names)] : known;
529
- return Promise.all(
530
- selected.map((name) => {
531
- const service = config.services[name];
532
- const type = service.type ?? DEFAULT_CHECKER_TYPE;
533
- return getChecker(type).check(name, service, context);
534
- })
535
- );
537
+ return true;
536
538
  }
537
-
538
- // src/ui/report.ts
539
- function width(values) {
540
- return Math.max(0, ...values.map((value) => value.length));
541
- }
542
- function formatCheckResults(results, colors) {
543
- const nameWidth = width(results.map((result) => result.name));
544
- const targetWidth = width(results.map((result) => result.target));
545
- return results.map((result) => {
546
- const mark = result.ok ? colors.green("\u2713") : colors.red("\u2717");
547
- const name = colors.bold(result.name.padEnd(nameWidth));
548
- const target = colors.dim(result.target.padEnd(targetWidth));
549
- const outcome = result.ok ? `${colors.green(String(result.status))} ${colors.dim(`${result.latencyMs}ms`)}` : colors.red(result.error ?? "down");
550
- return `${mark} ${name} ${target} ${outcome}`;
551
- }).join("\n");
552
- }
553
- function formatServices(config, colors) {
554
- const entries = Object.entries(config.services);
555
- const nameWidth = width(entries.map(([name]) => name));
556
- return entries.map(([name, service]) => {
557
- const expect = service.expect === void 0 ? "" : colors.dim(` expect ${service.expect}`);
558
- return `${colors.bold(name.padEnd(nameWidth))} ${targetUrl(service)}${expect}`;
559
- }).join("\n");
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
+ });
548
+ }
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
+ });
560
576
  }
561
577
 
562
578
  // src/commands/check.ts
563
- var MAX_TIMEOUT_SECONDS = 2147483;
564
579
  var check_default = defineCommand2({
565
580
  meta: { name: "check", description: "Check that services respond (all of them by default)" },
566
581
  args: {
@@ -572,21 +587,16 @@ var check_default = defineCommand2({
572
587
  }
573
588
  },
574
589
  async run({ args }) {
575
- const seconds = Number(args.timeout);
576
- if (!Number.isFinite(seconds) || seconds <= 0 || seconds > MAX_TIMEOUT_SECONDS) {
577
- throw new ProbeError(
578
- "USAGE",
579
- `--timeout expects seconds between 0 and ${MAX_TIMEOUT_SECONDS}, got "${args.timeout}".`
580
- );
581
- }
582
- const { config } = await openProject();
583
- 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);
584
594
  if (args.json) {
585
595
  console.log(JSON.stringify(results, null, 2));
586
596
  } else if (results.length === 0) {
587
597
  console.log("No services yet. Add one: probe add api -p 3000");
588
598
  } else {
589
- console.log(formatCheckResults(results, stdoutColors()));
599
+ console.log(formatCheckResults(results, stdoutTerminal()));
590
600
  }
591
601
  if (results.some((result) => !result.ok)) process.exitCode = EXIT_DOWN;
592
602
  }
@@ -594,8 +604,8 @@ var check_default = defineCommand2({
594
604
 
595
605
  // src/commands/init.ts
596
606
  import { existsSync } from "fs";
597
- import { mkdir, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
598
- 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";
599
609
  import { defineCommand as defineCommand3 } from "citty";
600
610
  var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
601
611
  var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
@@ -603,13 +613,13 @@ schemaVersion: ${CURRENT_SCHEMA_VERSION}
603
613
  services: {}
604
614
  `;
605
615
  async function ignoreLocalFiles(root) {
606
- const file = join4(root, ".gitignore");
607
- const current = existsSync(file) ? await readFile4(file, "utf8") : "";
616
+ const file = join5(root, ".gitignore");
617
+ const current = existsSync(file) ? await readFile5(file, "utf8") : "";
608
618
  const lines = current.split("\n").map((line) => line.trim());
609
619
  const missing = IGNORED.filter((entry) => !lines.includes(entry));
610
620
  if (missing.length === 0) return;
611
621
  const separator = current === "" || current.endsWith("\n") ? "" : "\n";
612
- await writeFile4(file, `${current}${separator}${missing.join("\n")}
622
+ await writeFile5(file, `${current}${separator}${missing.join("\n")}
613
623
  `);
614
624
  }
615
625
  var init_default = defineCommand3({
@@ -623,7 +633,7 @@ var init_default = defineCommand3({
623
633
  throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
624
634
  }
625
635
  await mkdir(paths.dir);
626
- await writeFile4(paths.config, INITIAL_CONFIG);
636
+ await writeFile5(paths.config, INITIAL_CONFIG);
627
637
  await ignoreLocalFiles(paths.root);
628
638
  const { config } = await loadConfig(paths);
629
639
  await syncMakefile(paths, config);
@@ -636,12 +646,12 @@ import { defineCommand as defineCommand4 } from "citty";
636
646
  var ls_default = defineCommand4({
637
647
  meta: { name: "ls", description: "List services" },
638
648
  async run() {
639
- const { config } = await openProject();
649
+ const { config, paths } = await openProject();
640
650
  if (Object.keys(config.services).length === 0) {
641
651
  console.log("No services yet. Add one: probe add api -p 3000");
642
652
  return;
643
653
  }
644
- console.log(formatServices(config, stdoutColors()));
654
+ console.log(formatServices(config, stdoutTerminal(), await readSkippedServices(paths)));
645
655
  }
646
656
  });
647
657
 
@@ -685,16 +695,16 @@ import { defineCommand as defineCommand7 } from "citty";
685
695
  import { spawn } from "child_process";
686
696
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
687
697
  import { homedir } from "os";
688
- import { join as join5 } from "path";
698
+ import { join as join6 } from "path";
689
699
  function cacheDir(env = process.env) {
690
- return join5(env.XDG_CACHE_HOME || join5(homedir(), ".cache"), "probe");
700
+ return join6(env.XDG_CACHE_HOME || join6(homedir(), ".cache"), "probe");
691
701
  }
692
702
  function updateCheckDisabled(env = process.env) {
693
703
  return env.PROBE_NO_UPDATE_CHECK === "1" || Boolean(env.CI);
694
704
  }
695
705
  function readCache(dir) {
696
706
  try {
697
- const cache = JSON.parse(readFileSync(join5(dir, UPDATE_CACHE_FILE), "utf8"));
707
+ const cache = JSON.parse(readFileSync(join6(dir, UPDATE_CACHE_FILE), "utf8"));
698
708
  return typeof cache.checkedAt === "number" ? cache : null;
699
709
  } catch {
700
710
  return null;
@@ -702,7 +712,7 @@ function readCache(dir) {
702
712
  }
703
713
  function writeCache(dir, cache) {
704
714
  mkdirSync(dir, { recursive: true });
705
- writeFileSync(join5(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
715
+ writeFileSync(join6(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
706
716
  }
707
717
  function isNewer(latest, current) {
708
718
  const parse2 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
@@ -803,42 +813,219 @@ var sync_default = defineCommand8({
803
813
  }
804
814
  });
805
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
+
806
928
  // src/cli.ts
807
- var main = defineCommand9({
929
+ var main = defineCommand10({
808
930
  meta: {
809
931
  name: "probe",
810
932
  version: CLI_VERSION,
811
933
  description: "Health checks for your local dev services"
812
934
  },
813
- 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 }
814
944
  });
945
+ async function printUsage(command, parent) {
946
+ const usage = await renderUsage(command, parent);
947
+ console.log(`${stdoutTerminal().color ? usage : stripStyles(usage)}
948
+ `);
949
+ }
815
950
  function notifyUpdate(rawArgs) {
816
951
  const cliPath = process.argv[1];
817
952
  if (updateCheckDisabled() || !cliPath) return;
818
953
  scheduleRefresh(cacheDir(), cliPath);
819
- 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;
820
956
  const notice = updateNotice(CLI_VERSION, cacheDir());
821
957
  if (notice) console.error(`
822
958
  ${notice}`);
823
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
+ }
824
1008
  async function run(rawArgs) {
825
1009
  if (rawArgs[0] === REFRESH_COMMAND) return refreshCache(cacheDir());
826
1010
  const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
827
1011
  const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
828
- if (wantsHelp || wantsVersion) return runMain(main, { rawArgs });
1012
+ if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
1013
+ let dashboard = false;
829
1014
  try {
830
- await runCommand(main, { rawArgs });
1015
+ dashboard = await openedDashboard(rawArgs);
1016
+ if (!dashboard) await runCommand(main, { rawArgs });
831
1017
  } catch (error) {
832
1018
  process.exitCode = EXIT_ERROR;
833
1019
  if (error instanceof ProbeError) {
834
1020
  console.error(`probe: ${error.message}`);
835
1021
  } else if (error instanceof Error && error.name === "CLIError") {
836
- if (rawArgs.length === 0) await showUsage(main);
1022
+ if (rawArgs.length === 0) await printUsage(main);
837
1023
  console.error(`probe: ${error.message.replace(/\.$/, "")}. See: probe --help`);
838
1024
  } else {
839
1025
  console.error(`probe: ${error instanceof Error ? error.message : String(error)}`);
840
1026
  }
841
1027
  }
842
1028
  notifyUpdate(rawArgs);
1029
+ if (dashboard) process.exit();
843
1030
  }
844
1031
  await run(process.argv.slice(2));