@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/README.md +43 -4
- package/dist/chunk-M3ASEG5F.js +48 -0
- package/dist/chunk-ZCAUPLQS.js +305 -0
- package/dist/cli.js +363 -176
- 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,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(
|
|
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
|
-
|
|
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/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
502
|
+
return preferences;
|
|
516
503
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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,
|
|
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
|
|
598
|
-
import { join as
|
|
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 =
|
|
607
|
-
const current = existsSync(file) ? await
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
698
|
+
import { join as join6 } from "path";
|
|
689
699
|
function cacheDir(env = process.env) {
|
|
690
|
-
return
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
|
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));
|