@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/README.md +32 -2
- package/dist/chunk-M3ASEG5F.js +48 -0
- package/dist/chunk-ZCAUPLQS.js +305 -0
- package/dist/cli.js +355 -182
- package/dist/devtools-QAEP2P5I.js +90 -0
- package/dist/open-TQ6NPJ7V.js +20479 -0
- package/package.json +7 -3
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 {
|
|
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.
|
|
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(
|
|
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/
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
502
|
+
return preferences;
|
|
530
503
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
return
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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,
|
|
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
|
|
612
|
-
import { join as
|
|
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 =
|
|
621
|
-
const current = existsSync(file) ? await
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
698
|
+
import { join as join6 } from "path";
|
|
703
699
|
function cacheDir(env = process.env) {
|
|
704
|
-
return
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
|
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));
|