@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/README.md +45 -10
- package/dist/chunk-M3ASEG5F.js +48 -0
- package/dist/chunk-ZCAUPLQS.js +305 -0
- package/dist/cli.js +365 -190
- 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.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(
|
|
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
|
|
422
|
+
const block = await blockState(loaded.paths, loaded.config);
|
|
412
423
|
const saved = await saveConfig(loaded, { ...loaded.raw, services });
|
|
413
|
-
if (
|
|
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/
|
|
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
|
-
}
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
503
|
+
return preferences;
|
|
530
504
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) });
|
|
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,
|
|
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
|
|
612
|
-
import { join as
|
|
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 =
|
|
621
|
-
const current = existsSync(file) ? await
|
|
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
|
|
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
|
|
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
|
|
637
|
+
await writeFile5(paths.config, INITIAL_CONFIG);
|
|
641
638
|
await ignoreLocalFiles(paths.root);
|
|
642
|
-
|
|
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,
|
|
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
|
|
697
|
+
import { join as join6 } from "path";
|
|
703
698
|
function cacheDir(env = process.env) {
|
|
704
|
-
return
|
|
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(
|
|
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(
|
|
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.
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
|
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));
|