@ninhache/probe 0.4.1 → 0.6.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 +30 -1
- package/dist/{chunk-HHTOVIAQ.js → chunk-YTTYVEYF.js} +84 -24
- package/dist/cli.js +412 -57
- package/dist/{open-7LCTV2ZW.js → open-T3OQH2O4.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Declare your project's local services once. Check they are up with one command,
|
|
|
11
11
|
$ probe check
|
|
12
12
|
✓ api http://localhost:3000/health 200 12ms
|
|
13
13
|
✗ front http://localhost:5173/ connection refused
|
|
14
|
+
nothing listens on :5173: is the service started?
|
|
14
15
|
$ echo $?
|
|
15
16
|
1
|
|
16
17
|
```
|
|
@@ -44,20 +45,44 @@ Want `make check` too? See [Makefile](#makefile): it is optional.
|
|
|
44
45
|
| `probe ls` | List services |
|
|
45
46
|
| `probe check [name...] [--json] [--timeout <seconds>]` | Check all services, or the named ones |
|
|
46
47
|
| `probe wait [name...] [--json] [--timeout <seconds>]` | Check again until every service is up, or give up after the timeout |
|
|
48
|
+
| `probe open <name>` | Open the URL of a service in the browser |
|
|
49
|
+
| `probe curl <name>` | Print the curl command that requests a service |
|
|
47
50
|
| `probe status` | CLI version, pending migration, Makefile block state, available update |
|
|
48
51
|
| `probe sync` | Add or regenerate the Makefile block (optional) |
|
|
49
52
|
| `probe migrate` | Migrate `.probe/config.yml` to the current schema |
|
|
53
|
+
| `probe completion <zsh\|bash\|fish>` | Print the shell completion script |
|
|
50
54
|
|
|
51
55
|
Exit codes: `0` all up, `1` a service is down, `2` usage or config error.
|
|
52
56
|
|
|
57
|
+
A service that is down comes with a hint when the cause is a known one: connection refused, timeout, host not
|
|
58
|
+
found, unexpected status, certificate rejected after a redirect to https. With `--json` it is the `hint` field,
|
|
59
|
+
next to `error`.
|
|
60
|
+
|
|
53
61
|
`probe wait` is for scripts: `probe wait && pnpm test`. It checks at once, then again after 0.5 s, 1 s, 2 s, 4 s
|
|
54
62
|
and every 5 s, for 60 s in total. `--timeout` sets the total, `--interval` the first pause, `--max-interval` the
|
|
55
63
|
longest one. Unlike `check --timeout`, which is the time given to each service, `wait --timeout` covers the whole
|
|
56
64
|
wait; each check inside it still gets 3 s, or the service's own `timeout`. Progress goes to stderr, the final
|
|
57
65
|
report to stdout; exit code `1` names the services still down.
|
|
58
66
|
|
|
67
|
+
`probe open` uses `open` on macOS and `xdg-open` elsewhere. Without it, the URL is printed and the exit code is
|
|
68
|
+
`1`. `probe curl api` prints a command to copy, or to run: `probe curl api | sh`.
|
|
69
|
+
|
|
59
70
|
`probe` works from any sub-directory: it walks up to the nearest `.probe/`.
|
|
60
71
|
|
|
72
|
+
## Completion
|
|
73
|
+
|
|
74
|
+
Tab completes the commands, their options and the names of your services (`probe check a<Tab>`).
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
eval "$(probe completion zsh)" # in ~/.zshrc, after compinit
|
|
78
|
+
eval "$(probe completion bash)" # in ~/.bashrc (macOS: ~/.bash_profile)
|
|
79
|
+
probe completion fish | source # in ~/.config/fish/config.fish
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
In zsh you can also save it once instead: `probe completion zsh > ~/.zfunc/_probe`, with `~/.zfunc` in your
|
|
83
|
+
`fpath`; in fish, `probe completion fish > ~/.config/fish/completions/probe.fish`. It works with the bash 3.2
|
|
84
|
+
of macOS.
|
|
85
|
+
|
|
61
86
|
## Dashboard
|
|
62
87
|
|
|
63
88
|
`probe` alone, on a terminal, opens a live list of your services and checks them every 2 s
|
|
@@ -137,12 +162,16 @@ outside the block is yours and is never touched. Without a block, no command cre
|
|
|
137
162
|
Once the block is there, `probe add` and `probe rm` keep it up to date. Commit the `Makefile` and
|
|
138
163
|
`.probe/lock.yml`.
|
|
139
164
|
|
|
140
|
-
Targets: `help`, `check`, `status` and one `check-<name>` per
|
|
165
|
+
Targets: `help`, `check`, `status`, `probe` (opens the [dashboard](#dashboard)) and one `check-<name>` per
|
|
166
|
+
service. The block uses `probe` when it is in
|
|
141
167
|
your `PATH`, else `npx -y @ninhache/probe@latest`. It works with GNU Make 3.81 (the macOS default).
|
|
142
168
|
|
|
143
169
|
Edited the block by hand? `probe status` says so and `probe add` / `probe rm` leave it alone. `probe sync`
|
|
144
170
|
restores it.
|
|
145
171
|
|
|
172
|
+
A newer `probe` can add targets to the block: `make probe` came with 0.6.0. A block written before that is
|
|
173
|
+
reported as `outdated` by `probe status`. Run `probe sync` once to get the new targets.
|
|
174
|
+
|
|
146
175
|
## Migrations
|
|
147
176
|
|
|
148
177
|
`config.yml` carries a `schemaVersion`. When a newer `probe` needs a newer schema, the first command you run:
|
|
@@ -27,6 +27,7 @@ var REGISTRY_TIMEOUT_MS = 5e3;
|
|
|
27
27
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
28
28
|
var UPDATE_CACHE_FILE = "latest.json";
|
|
29
29
|
var REFRESH_COMMAND = "__refresh-update-cache";
|
|
30
|
+
var COMPLETE_COMMAND = "__complete";
|
|
30
31
|
var EXIT_DOWN = 1;
|
|
31
32
|
var EXIT_ERROR = 2;
|
|
32
33
|
|
|
@@ -93,19 +94,83 @@ function stripStyles(text) {
|
|
|
93
94
|
return text.replace(SGR_PATTERN, "");
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
// src/checkers/
|
|
97
|
-
var
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
// src/checkers/diagnosis.ts
|
|
98
|
+
var CONFIG = `${PROBE_DIR}/${CONFIG_FILE}`;
|
|
99
|
+
var CERTIFICATE_CODES = /* @__PURE__ */ new Set([
|
|
100
|
+
"CERT_HAS_EXPIRED",
|
|
101
|
+
"CERT_NOT_YET_VALID",
|
|
102
|
+
"CERT_UNTRUSTED",
|
|
103
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
104
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
105
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
106
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
107
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
108
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
109
|
+
]);
|
|
110
|
+
var hostNotFound = (service) => ({
|
|
111
|
+
error: "host not found",
|
|
112
|
+
hint: `"${service.host}" does not resolve: check \`host\` in ${CONFIG}`
|
|
113
|
+
});
|
|
114
|
+
var NETWORK = {
|
|
115
|
+
ECONNREFUSED: (service) => ({
|
|
116
|
+
error: "connection refused",
|
|
117
|
+
hint: `nothing listens on :${service.port}: is the service started?`
|
|
118
|
+
}),
|
|
119
|
+
ECONNRESET: (service) => ({
|
|
120
|
+
error: "connection reset",
|
|
121
|
+
hint: `:${service.port} closed the connection without answering: does it speak plain http?`
|
|
122
|
+
}),
|
|
123
|
+
ENOTFOUND: hostNotFound,
|
|
124
|
+
EAI_AGAIN: hostNotFound,
|
|
125
|
+
EHOSTUNREACH: (service) => ({
|
|
126
|
+
error: "host unreachable",
|
|
127
|
+
hint: `no route to ${service.host}: check the network or the VPN`
|
|
128
|
+
})
|
|
103
129
|
};
|
|
130
|
+
function diagnoseFailure(error, service, timeoutMs) {
|
|
131
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
132
|
+
return {
|
|
133
|
+
error: `timeout after ${timeoutMs / 1e3}s`,
|
|
134
|
+
hint: `still starting, or stuck? Give it more time: \`timeout\` in ${CONFIG}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const cause = error?.cause;
|
|
138
|
+
const code = cause?.code ?? cause?.errors?.[0]?.code;
|
|
139
|
+
if (!code) return { error: error instanceof Error ? error.message : String(error) };
|
|
140
|
+
if (CERTIFICATE_CODES.has(code)) {
|
|
141
|
+
return {
|
|
142
|
+
error: `certificate rejected (${code})`,
|
|
143
|
+
// Services are checked over http: a certificate can only come from a redirect to https.
|
|
144
|
+
hint: "a redirect led to https with a certificate that is not trusted: renew it, or add its CA with NODE_EXTRA_CA_CERTS"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return NETWORK[code]?.(service) ?? { error: code };
|
|
148
|
+
}
|
|
149
|
+
function describeExpected(expect) {
|
|
150
|
+
if (expect === void 0) return "2xx";
|
|
151
|
+
return Array.isArray(expect) ? expect.join(" or ") : String(expect);
|
|
152
|
+
}
|
|
153
|
+
function diagnoseStatus(service, status) {
|
|
154
|
+
const error = `expected ${describeExpected(service.expect)}, got ${status}`;
|
|
155
|
+
if (status >= 500) return { error, hint: "the service answers with an error: look at its logs" };
|
|
156
|
+
if (status === 404) {
|
|
157
|
+
return { error, hint: `nothing at ${service.path}: check \`path\` in ${CONFIG}` };
|
|
158
|
+
}
|
|
159
|
+
return { error, hint: `the service answers: check \`path\` and \`expect\` in ${CONFIG}` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/checkers/http.ts
|
|
104
163
|
function targetUrl(service) {
|
|
105
164
|
const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
|
|
106
165
|
const host = bareIpv6 ? `[${service.host}]` : service.host;
|
|
107
166
|
return `http://${host}:${service.port}${service.path}`;
|
|
108
167
|
}
|
|
168
|
+
var shellQuoted = (text) => `'${text.replaceAll("'", "'\\''")}'`;
|
|
169
|
+
function curlCommand(service) {
|
|
170
|
+
const follow = expectsRedirect(service.expect) ? "" : " -L";
|
|
171
|
+
const seconds = service.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
172
|
+
return `curl -i${follow} --max-time ${seconds} ${shellQuoted(targetUrl(service))}`;
|
|
173
|
+
}
|
|
109
174
|
function isExpected(expect, status) {
|
|
110
175
|
if (expect === void 0) return status >= 200 && status < 300;
|
|
111
176
|
return Array.isArray(expect) ? expect.includes(status) : expect === status;
|
|
@@ -113,19 +178,6 @@ function isExpected(expect, status) {
|
|
|
113
178
|
function expectsRedirect(expect) {
|
|
114
179
|
return [expect ?? []].flat().some((status) => status >= 300 && status < 400);
|
|
115
180
|
}
|
|
116
|
-
function describeExpected(expect) {
|
|
117
|
-
if (expect === void 0) return "2xx";
|
|
118
|
-
return Array.isArray(expect) ? expect.join(" or ") : String(expect);
|
|
119
|
-
}
|
|
120
|
-
function failureReason(error, timeoutMs) {
|
|
121
|
-
if (error instanceof Error && error.name === "TimeoutError") {
|
|
122
|
-
return `timeout after ${timeoutMs / 1e3}s`;
|
|
123
|
-
}
|
|
124
|
-
const cause = error?.cause;
|
|
125
|
-
const code = cause?.code ?? cause?.errors?.[0]?.code;
|
|
126
|
-
if (code) return REASONS[code] ?? code;
|
|
127
|
-
return error instanceof Error ? error.message : String(error);
|
|
128
|
-
}
|
|
129
181
|
var httpChecker = {
|
|
130
182
|
type: "http",
|
|
131
183
|
async check(name, service, { timeoutMs, signal }) {
|
|
@@ -144,10 +196,10 @@ var httpChecker = {
|
|
|
144
196
|
await response.body?.cancel();
|
|
145
197
|
const { status } = response;
|
|
146
198
|
if (isExpected(service.expect, status)) return { ...base, ok: true, status, latencyMs };
|
|
147
|
-
|
|
148
|
-
return { ...base, ok: false, status, latencyMs, error };
|
|
199
|
+
return { ...base, ok: false, status, latencyMs, ...diagnoseStatus(service, status) };
|
|
149
200
|
} catch (error) {
|
|
150
|
-
|
|
201
|
+
const diagnosis = diagnoseFailure(error, service, timeoutMs);
|
|
202
|
+
return { ...base, ok: false, latencyMs: elapsed(), ...diagnosis };
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
};
|
|
@@ -251,7 +303,12 @@ function formatCheckResults(results, terminal) {
|
|
|
251
303
|
} else {
|
|
252
304
|
outcome = `${colors.green(String(result.status))}${GAP}${colors.dim(`${result.latencyMs}ms`)}`;
|
|
253
305
|
}
|
|
254
|
-
|
|
306
|
+
const line = `${mark} ${name}${GAP}${target}${outcome}`;
|
|
307
|
+
if (!result.hint) return line;
|
|
308
|
+
const room = columns === void 0 ? void 0 : Math.max(1, columns - GAP.length);
|
|
309
|
+
const hint = room === void 0 ? result.hint : truncate(result.hint, room);
|
|
310
|
+
return `${line}
|
|
311
|
+
${GAP}${colors.dim(hint)}`;
|
|
255
312
|
}).join("\n");
|
|
256
313
|
}
|
|
257
314
|
function formatServices(config, terminal, skipped = /* @__PURE__ */ new Set()) {
|
|
@@ -299,6 +356,7 @@ export {
|
|
|
299
356
|
UPDATE_CHECK_TTL_MS,
|
|
300
357
|
UPDATE_CACHE_FILE,
|
|
301
358
|
REFRESH_COMMAND,
|
|
359
|
+
COMPLETE_COMMAND,
|
|
302
360
|
EXIT_DOWN,
|
|
303
361
|
EXIT_ERROR,
|
|
304
362
|
ProbeError,
|
|
@@ -307,6 +365,8 @@ export {
|
|
|
307
365
|
isInteractive,
|
|
308
366
|
truncate,
|
|
309
367
|
stripStyles,
|
|
368
|
+
targetUrl,
|
|
369
|
+
curlCommand,
|
|
310
370
|
runChecks,
|
|
311
371
|
formatCheckResults,
|
|
312
372
|
formatServices
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire as __createRequire } from "node:module";
|
|
|
3
3
|
const require = __createRequire(import.meta.url);
|
|
4
4
|
import {
|
|
5
5
|
BACKUP_GLOB,
|
|
6
|
+
COMPLETE_COMMAND,
|
|
6
7
|
CONFIG_FILE,
|
|
7
8
|
DASHBOARD_INTERVAL_SECONDS,
|
|
8
9
|
DEFAULT_MAKEFILE,
|
|
@@ -28,18 +29,20 @@ import {
|
|
|
28
29
|
WAIT_BACKOFF_FACTOR,
|
|
29
30
|
WAIT_LAST_PASS_MS,
|
|
30
31
|
createColors,
|
|
32
|
+
curlCommand,
|
|
31
33
|
detectTerminal,
|
|
32
34
|
formatCheckResults,
|
|
33
35
|
formatServices,
|
|
34
36
|
isInteractive,
|
|
35
37
|
runChecks,
|
|
36
|
-
stripStyles
|
|
37
|
-
|
|
38
|
+
stripStyles,
|
|
39
|
+
targetUrl
|
|
40
|
+
} from "./chunk-YTTYVEYF.js";
|
|
38
41
|
import "./chunk-M3ASEG5F.js";
|
|
39
42
|
|
|
40
43
|
// src/cli.ts
|
|
41
44
|
import {
|
|
42
|
-
defineCommand as
|
|
45
|
+
defineCommand as defineCommand13,
|
|
43
46
|
renderUsage,
|
|
44
47
|
runCommand,
|
|
45
48
|
runMain
|
|
@@ -316,7 +319,7 @@ async function writeLock(paths, lock) {
|
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
// src/version.ts
|
|
319
|
-
var CLI_VERSION = true ? "0.
|
|
322
|
+
var CLI_VERSION = true ? "0.6.0" : "0.0.0-dev";
|
|
320
323
|
|
|
321
324
|
// src/makefile/generator.ts
|
|
322
325
|
import { createHash } from "crypto";
|
|
@@ -331,13 +334,15 @@ function renderBlock(serviceNames) {
|
|
|
331
334
|
return [
|
|
332
335
|
BLOCK_START,
|
|
333
336
|
`PROBE ?= $(shell command -v probe >/dev/null 2>&1 && echo probe || echo "${fallback}")`,
|
|
334
|
-
`.PHONY: ${["help", "check", "status", ...targets].join(" ")}`,
|
|
337
|
+
`.PHONY: ${["help", "check", "status", "probe", ...targets].join(" ")}`,
|
|
335
338
|
"help: ## List available targets",
|
|
336
339
|
` ${HELP_RECIPE}`,
|
|
337
340
|
"check: ## Check that every service responds",
|
|
338
341
|
" @$(PROBE) check",
|
|
339
342
|
"status: ## Show probe status",
|
|
340
343
|
" @$(PROBE) status",
|
|
344
|
+
"probe: ## Open the dashboard",
|
|
345
|
+
" @$(PROBE)",
|
|
341
346
|
...serviceNames.flatMap((name) => [
|
|
342
347
|
`check-${name}: ## Check ${name}`,
|
|
343
348
|
` @$(PROBE) check ${name}`
|
|
@@ -426,6 +431,11 @@ function secondsToMs(option, value) {
|
|
|
426
431
|
}
|
|
427
432
|
return Math.ceil(seconds2 * 1e3);
|
|
428
433
|
}
|
|
434
|
+
function namedService(config, name) {
|
|
435
|
+
const service = Object.hasOwn(config.services, name) ? config.services[name] : void 0;
|
|
436
|
+
if (!service) throw new ProbeError("SERVICE_UNKNOWN", `unknown service "${name}". See: probe ls`);
|
|
437
|
+
return service;
|
|
438
|
+
}
|
|
429
439
|
function rawServices(loaded) {
|
|
430
440
|
return loaded.raw.services ?? {};
|
|
431
441
|
}
|
|
@@ -466,25 +476,25 @@ var add_default = defineCommand({
|
|
|
466
476
|
description: `Seconds to wait for this service (default: ${DEFAULT_TIMEOUT_SECONDS})`
|
|
467
477
|
}
|
|
468
478
|
},
|
|
469
|
-
async run({ args }) {
|
|
479
|
+
async run({ args: args2 }) {
|
|
470
480
|
const loaded = await openProject();
|
|
471
481
|
const services = rawServices(loaded);
|
|
472
|
-
if (Object.hasOwn(services,
|
|
482
|
+
if (Object.hasOwn(services, args2.name)) {
|
|
473
483
|
throw new ProbeError(
|
|
474
484
|
"SERVICE_EXISTS",
|
|
475
|
-
`service "${
|
|
485
|
+
`service "${args2.name}" already exists. Remove it first: probe rm ${args2.name}`
|
|
476
486
|
);
|
|
477
487
|
}
|
|
478
|
-
const service = { port: parseInteger(String(
|
|
479
|
-
if (
|
|
480
|
-
if (
|
|
481
|
-
if (
|
|
482
|
-
if (
|
|
483
|
-
secondsToMs("--timeout", String(
|
|
484
|
-
service.timeout = Number(
|
|
488
|
+
const service = { port: parseInteger(String(args2.port), "--port") };
|
|
489
|
+
if (args2.host) service.host = args2.host;
|
|
490
|
+
if (args2.path) service.path = args2.path;
|
|
491
|
+
if (args2.expect) service.expect = parseExpect(String(args2.expect));
|
|
492
|
+
if (args2.timeout !== void 0) {
|
|
493
|
+
secondsToMs("--timeout", String(args2.timeout));
|
|
494
|
+
service.timeout = Number(args2.timeout);
|
|
485
495
|
}
|
|
486
|
-
await saveServices(loaded, { ...services, [
|
|
487
|
-
console.log(`Added ${
|
|
496
|
+
await saveServices(loaded, { ...services, [args2.name]: service });
|
|
497
|
+
console.log(`Added ${args2.name}. Try: probe check ${args2.name}`);
|
|
488
498
|
}
|
|
489
499
|
});
|
|
490
500
|
|
|
@@ -606,13 +616,13 @@ var check_default = defineCommand2({
|
|
|
606
616
|
description: `Seconds to wait for each service (default: its own timeout, else ${DEFAULT_TIMEOUT_SECONDS})`
|
|
607
617
|
}
|
|
608
618
|
},
|
|
609
|
-
async run({ args }) {
|
|
610
|
-
const forceTimeout =
|
|
611
|
-
const timeoutMs = secondsToMs("--timeout",
|
|
619
|
+
async run({ args: args2 }) {
|
|
620
|
+
const forceTimeout = args2.timeout !== void 0;
|
|
621
|
+
const timeoutMs = secondsToMs("--timeout", args2.timeout ?? String(DEFAULT_TIMEOUT_SECONDS));
|
|
612
622
|
const { config, paths } = await openProject();
|
|
613
623
|
const skipped = await readSkippedServices(paths);
|
|
614
|
-
const results = await runChecks(config,
|
|
615
|
-
if (
|
|
624
|
+
const results = await runChecks(config, args2._, { timeoutMs, forceTimeout }, skipped);
|
|
625
|
+
if (args2.json) {
|
|
616
626
|
console.log(JSON.stringify(results, null, 2));
|
|
617
627
|
} else if (results.length === 0) {
|
|
618
628
|
console.log("No services yet. Add one: probe add api -p 3000");
|
|
@@ -623,11 +633,190 @@ var check_default = defineCommand2({
|
|
|
623
633
|
}
|
|
624
634
|
});
|
|
625
635
|
|
|
636
|
+
// src/commands/completion.ts
|
|
637
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
638
|
+
|
|
639
|
+
// src/completion/spec.ts
|
|
640
|
+
var SHELLS = ["zsh", "bash", "fish"];
|
|
641
|
+
var COMPLETION_COMMAND = "completion";
|
|
642
|
+
var SERVICE_COMMANDS = ["rm", "check", "wait", "open", "curl"];
|
|
643
|
+
var ROOT_OPTIONS = ["--interval", "--help", "--version"];
|
|
644
|
+
var SERVICES_QUERY = `probe ${COMPLETE_COMMAND} services 2>/dev/null`;
|
|
645
|
+
var HELP_OPTION = "--help";
|
|
646
|
+
function completionSpec(commands2) {
|
|
647
|
+
return Object.entries(commands2).map(([name, command]) => {
|
|
648
|
+
const meta2 = command.meta ?? {};
|
|
649
|
+
const args2 = command.args ?? {};
|
|
650
|
+
const options = Object.entries(args2).filter(([, arg]) => arg.type !== "positional").map(([option]) => `--${option}`);
|
|
651
|
+
return {
|
|
652
|
+
name,
|
|
653
|
+
description: meta2.description ?? "",
|
|
654
|
+
options: [...options, HELP_OPTION],
|
|
655
|
+
words: name === COMPLETION_COMMAND ? [...SHELLS] : [],
|
|
656
|
+
services: SERVICE_COMMANDS.includes(name)
|
|
657
|
+
};
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/completion/bash.ts
|
|
662
|
+
function bashScript(commands2) {
|
|
663
|
+
const cases = (pick) => commands2.filter((command) => pick(command).length > 0).map((command) => ` ${command.name}) words="${pick(command).join(" ")}" ;;`);
|
|
664
|
+
const withServices = commands2.filter((command) => command.services).map(({ name }) => name);
|
|
665
|
+
return [
|
|
666
|
+
'# bash completion for probe. Load it with: eval "$(probe completion bash)"',
|
|
667
|
+
"_probe() {",
|
|
668
|
+
` local cur="\${COMP_WORDS[COMP_CWORD]}" cmd="" words="" i`,
|
|
669
|
+
" for ((i = 1; i < COMP_CWORD; i++)); do",
|
|
670
|
+
` case "\${COMP_WORDS[i]}" in`,
|
|
671
|
+
" -*) ;;",
|
|
672
|
+
` *) cmd="\${COMP_WORDS[i]}"; break ;;`,
|
|
673
|
+
" esac",
|
|
674
|
+
" done",
|
|
675
|
+
' if [ -z "$cmd" ]; then',
|
|
676
|
+
' case "$cur" in',
|
|
677
|
+
` -*) words="${ROOT_OPTIONS.join(" ")}" ;;`,
|
|
678
|
+
` *) words="${commands2.map(({ name }) => name).join(" ")}" ;;`,
|
|
679
|
+
" esac",
|
|
680
|
+
` elif [ "\${cur#-}" != "$cur" ]; then`,
|
|
681
|
+
' case "$cmd" in',
|
|
682
|
+
...cases((command) => command.options),
|
|
683
|
+
" esac",
|
|
684
|
+
" else",
|
|
685
|
+
' case "$cmd" in',
|
|
686
|
+
` ${withServices.join("|")}) words="$(${SERVICES_QUERY})" ;;`,
|
|
687
|
+
...cases((command) => command.words),
|
|
688
|
+
" esac",
|
|
689
|
+
" fi",
|
|
690
|
+
' COMPREPLY=($(compgen -W "$words" -- "$cur"))',
|
|
691
|
+
"}",
|
|
692
|
+
"complete -F _probe probe",
|
|
693
|
+
""
|
|
694
|
+
].join("\n");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/completion/fish.ts
|
|
698
|
+
var quoted = (text) => `'${text.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
699
|
+
var longOption = (option) => `-l ${option.replace(/^--/, "")}`;
|
|
700
|
+
function fishScript(commands2) {
|
|
701
|
+
const inCommand = (names) => `-n '__fish_seen_subcommand_from ${names.join(" ")}'`;
|
|
702
|
+
const withServices = commands2.filter((command) => command.services).map(({ name }) => name);
|
|
703
|
+
return [
|
|
704
|
+
"# fish completion for probe. Load it with: probe completion fish | source",
|
|
705
|
+
"function __probe_services",
|
|
706
|
+
` ${SERVICES_QUERY}`,
|
|
707
|
+
"end",
|
|
708
|
+
// No file names: probe takes none.
|
|
709
|
+
"complete -c probe -f",
|
|
710
|
+
...ROOT_OPTIONS.map(
|
|
711
|
+
(option) => `complete -c probe -n __fish_use_subcommand ${longOption(option)}`
|
|
712
|
+
),
|
|
713
|
+
...commands2.map(
|
|
714
|
+
({ name, description }) => `complete -c probe -n __fish_use_subcommand -a ${name} -d ${quoted(description)}`
|
|
715
|
+
),
|
|
716
|
+
`complete -c probe ${inCommand(withServices)} -a '(__probe_services)'`,
|
|
717
|
+
...commands2.flatMap((command) => [
|
|
718
|
+
...command.options.map(
|
|
719
|
+
(option) => `complete -c probe ${inCommand([command.name])} ${longOption(option)}`
|
|
720
|
+
),
|
|
721
|
+
...command.words.length > 0 ? [`complete -c probe ${inCommand([command.name])} -a ${quoted(command.words.join(" "))}`] : []
|
|
722
|
+
]),
|
|
723
|
+
""
|
|
724
|
+
].join("\n");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/completion/zsh.ts
|
|
728
|
+
function described({ name, description }) {
|
|
729
|
+
const text = `${name}:${description.replaceAll(":", "\\:")}`;
|
|
730
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
731
|
+
}
|
|
732
|
+
function zshScript(commands2) {
|
|
733
|
+
const cases = (pick) => commands2.filter((command) => pick(command).length > 0).map((command) => ` ${command.name}) compadd -- ${pick(command).join(" ")} ;;`);
|
|
734
|
+
const withServices = commands2.filter((command) => command.services).map(({ name }) => name);
|
|
735
|
+
return [
|
|
736
|
+
"#compdef probe",
|
|
737
|
+
'# zsh completion for probe. Load it after compinit with: eval "$(probe completion zsh)"',
|
|
738
|
+
"_probe() {",
|
|
739
|
+
" local -a commands services",
|
|
740
|
+
" commands=(",
|
|
741
|
+
...commands2.map((command) => ` ${described(command)}`),
|
|
742
|
+
" )",
|
|
743
|
+
" if (( CURRENT == 2 )); then",
|
|
744
|
+
" if [[ $words[CURRENT] == -* ]]; then",
|
|
745
|
+
` compadd -- ${ROOT_OPTIONS.join(" ")}`,
|
|
746
|
+
" else",
|
|
747
|
+
" _describe 'command' commands",
|
|
748
|
+
" fi",
|
|
749
|
+
" return",
|
|
750
|
+
" fi",
|
|
751
|
+
" if [[ $words[CURRENT] == -* ]]; then",
|
|
752
|
+
" case $words[2] in",
|
|
753
|
+
...cases((command) => command.options).map((line) => ` ${line}`),
|
|
754
|
+
" esac",
|
|
755
|
+
" return",
|
|
756
|
+
" fi",
|
|
757
|
+
" case $words[2] in",
|
|
758
|
+
` ${withServices.join("|")})`,
|
|
759
|
+
` services=(\${(f)"$(${SERVICES_QUERY})"})`,
|
|
760
|
+
" compadd -a services ;;",
|
|
761
|
+
...cases((command) => command.words),
|
|
762
|
+
" esac",
|
|
763
|
+
"}",
|
|
764
|
+
"if (( $+functions[compdef] )); then compdef _probe probe; fi",
|
|
765
|
+
"# Saved as _probe in $fpath, this file is the function itself: run it.",
|
|
766
|
+
'if [[ $funcstack[1] == _probe ]]; then _probe "$@"; fi',
|
|
767
|
+
""
|
|
768
|
+
].join("\n");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/commands/completion.ts
|
|
772
|
+
var SCRIPTS = {
|
|
773
|
+
zsh: zshScript,
|
|
774
|
+
bash: bashScript,
|
|
775
|
+
fish: fishScript
|
|
776
|
+
};
|
|
777
|
+
var meta = {
|
|
778
|
+
name: COMPLETION_COMMAND,
|
|
779
|
+
description: `Print the shell completion script (${SHELLS.join(", ")})`
|
|
780
|
+
};
|
|
781
|
+
var args = {
|
|
782
|
+
shell: { type: "positional", description: SHELLS.join(", "), required: true }
|
|
783
|
+
};
|
|
784
|
+
function completion(others) {
|
|
785
|
+
const commands2 = { ...others, [COMPLETION_COMMAND]: { meta, args } };
|
|
786
|
+
return defineCommand3({
|
|
787
|
+
meta,
|
|
788
|
+
args,
|
|
789
|
+
run({ args: { shell } }) {
|
|
790
|
+
const script = Object.hasOwn(SCRIPTS, shell) ? SCRIPTS[shell] : void 0;
|
|
791
|
+
if (!script) {
|
|
792
|
+
throw new ProbeError(
|
|
793
|
+
"USAGE",
|
|
794
|
+
`no completion for the shell "${shell}". Supported: ${SHELLS.join(", ")}.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
process.stdout.write(script(completionSpec(commands2)));
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/commands/curl.ts
|
|
803
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
804
|
+
var curl_default = defineCommand4({
|
|
805
|
+
meta: { name: "curl", description: "Print the curl command that requests a service" },
|
|
806
|
+
args: {
|
|
807
|
+
name: { type: "positional", description: "Service name", required: true }
|
|
808
|
+
},
|
|
809
|
+
async run({ args: args2 }) {
|
|
810
|
+
const { config } = await openProject();
|
|
811
|
+
console.log(curlCommand(namedService(config, args2.name)));
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
626
815
|
// src/commands/init.ts
|
|
627
816
|
import { existsSync } from "fs";
|
|
628
817
|
import { mkdir, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
629
818
|
import { join as join5 } from "path";
|
|
630
|
-
import { defineCommand as
|
|
819
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
631
820
|
var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
|
|
632
821
|
var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
|
|
633
822
|
schemaVersion: ${CURRENT_SCHEMA_VERSION}
|
|
@@ -643,7 +832,7 @@ async function ignoreLocalFiles(root) {
|
|
|
643
832
|
await writeFile5(file, `${current}${separator}${missing.join("\n")}
|
|
644
833
|
`);
|
|
645
834
|
}
|
|
646
|
-
var init_default =
|
|
835
|
+
var init_default = defineCommand5({
|
|
647
836
|
meta: {
|
|
648
837
|
name: "init",
|
|
649
838
|
description: "Create .probe/ and the .gitignore entries"
|
|
@@ -661,8 +850,8 @@ var init_default = defineCommand3({
|
|
|
661
850
|
});
|
|
662
851
|
|
|
663
852
|
// src/commands/ls.ts
|
|
664
|
-
import { defineCommand as
|
|
665
|
-
var ls_default =
|
|
853
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
854
|
+
var ls_default = defineCommand6({
|
|
666
855
|
meta: { name: "ls", description: "List services" },
|
|
667
856
|
async run() {
|
|
668
857
|
const { config, paths } = await openProject();
|
|
@@ -675,8 +864,8 @@ var ls_default = defineCommand4({
|
|
|
675
864
|
});
|
|
676
865
|
|
|
677
866
|
// src/commands/migrate.ts
|
|
678
|
-
import { defineCommand as
|
|
679
|
-
var migrate_default =
|
|
867
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
868
|
+
var migrate_default = defineCommand7({
|
|
680
869
|
meta: { name: "migrate", description: "Migrate the config to the current schema version" },
|
|
681
870
|
async run() {
|
|
682
871
|
const paths = requireProbeDir(process.cwd());
|
|
@@ -688,27 +877,65 @@ var migrate_default = defineCommand5({
|
|
|
688
877
|
}
|
|
689
878
|
});
|
|
690
879
|
|
|
880
|
+
// src/commands/open.ts
|
|
881
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
882
|
+
|
|
883
|
+
// src/open-url.ts
|
|
884
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
885
|
+
function openerCommand(platform) {
|
|
886
|
+
return platform === "darwin" ? "open" : "xdg-open";
|
|
887
|
+
}
|
|
888
|
+
function openUrl(url, platform = process.platform, spawn2 = nodeSpawn) {
|
|
889
|
+
return new Promise((resolve2) => {
|
|
890
|
+
const child = spawn2(openerCommand(platform), [url], { detached: true, stdio: "ignore" });
|
|
891
|
+
child.once("error", () => resolve2(false));
|
|
892
|
+
child.once("spawn", () => {
|
|
893
|
+
child.unref();
|
|
894
|
+
resolve2(true);
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/commands/open.ts
|
|
900
|
+
var open_default = defineCommand8({
|
|
901
|
+
meta: { name: "open", description: "Open the URL of a service in the browser" },
|
|
902
|
+
args: {
|
|
903
|
+
name: { type: "positional", description: "Service name", required: true }
|
|
904
|
+
},
|
|
905
|
+
async run({ args: args2 }) {
|
|
906
|
+
const { config } = await openProject();
|
|
907
|
+
const url = targetUrl(namedService(config, args2.name));
|
|
908
|
+
if (await openUrl(url)) {
|
|
909
|
+
console.log(`Opened ${url}`);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
console.log(url);
|
|
913
|
+
console.error(`probe: ${openerCommand(process.platform)} not found, open the URL yourself.`);
|
|
914
|
+
process.exitCode = EXIT_DOWN;
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
691
918
|
// src/commands/rm.ts
|
|
692
|
-
import { defineCommand as
|
|
693
|
-
var rm_default =
|
|
919
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
920
|
+
var rm_default = defineCommand9({
|
|
694
921
|
meta: { name: "rm", description: "Remove a service" },
|
|
695
922
|
args: {
|
|
696
923
|
name: { type: "positional", description: "Service name", required: true }
|
|
697
924
|
},
|
|
698
|
-
async run({ args }) {
|
|
925
|
+
async run({ args: args2 }) {
|
|
699
926
|
const loaded = await openProject();
|
|
700
927
|
const services = rawServices(loaded);
|
|
701
|
-
if (!Object.hasOwn(services,
|
|
702
|
-
throw new ProbeError("SERVICE_UNKNOWN", `unknown service "${
|
|
928
|
+
if (!Object.hasOwn(services, args2.name)) {
|
|
929
|
+
throw new ProbeError("SERVICE_UNKNOWN", `unknown service "${args2.name}". See: probe ls`);
|
|
703
930
|
}
|
|
704
|
-
const { [
|
|
931
|
+
const { [args2.name]: _removed, ...rest } = services;
|
|
705
932
|
await saveServices(loaded, rest);
|
|
706
|
-
console.log(`Removed ${
|
|
933
|
+
console.log(`Removed ${args2.name}.`);
|
|
707
934
|
}
|
|
708
935
|
});
|
|
709
936
|
|
|
710
937
|
// src/commands/status.ts
|
|
711
|
-
import { defineCommand as
|
|
938
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
712
939
|
|
|
713
940
|
// src/update/notifier.ts
|
|
714
941
|
import { spawn } from "child_process";
|
|
@@ -734,8 +961,8 @@ function writeCache(dir, cache) {
|
|
|
734
961
|
writeFileSync(join6(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
|
|
735
962
|
}
|
|
736
963
|
function isNewer(latest, current) {
|
|
737
|
-
const
|
|
738
|
-
const [a, b] = [
|
|
964
|
+
const parse3 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
|
|
965
|
+
const [a, b] = [parse3(latest), parse3(current)];
|
|
739
966
|
for (let index = 0; index < 3; index++) {
|
|
740
967
|
const difference = (a[index] || 0) - (b[index] || 0);
|
|
741
968
|
if (difference !== 0) return difference > 0;
|
|
@@ -797,7 +1024,7 @@ function describeUpdate(colors) {
|
|
|
797
1024
|
if (!readCache(cacheDir())?.latest) return colors.dim(`not checked yet (${CLI_VERSION})`);
|
|
798
1025
|
return colors.green(`up to date (${CLI_VERSION})`);
|
|
799
1026
|
}
|
|
800
|
-
var status_default =
|
|
1027
|
+
var status_default = defineCommand10({
|
|
801
1028
|
meta: { name: "status", description: "Show pending migration, Makefile state and updates" },
|
|
802
1029
|
async run() {
|
|
803
1030
|
const colors = stdoutColors();
|
|
@@ -819,8 +1046,8 @@ var status_default = defineCommand7({
|
|
|
819
1046
|
|
|
820
1047
|
// src/commands/sync.ts
|
|
821
1048
|
import { relative as relative2 } from "path";
|
|
822
|
-
import { defineCommand as
|
|
823
|
-
var sync_default =
|
|
1049
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
1050
|
+
var sync_default = defineCommand11({
|
|
824
1051
|
meta: {
|
|
825
1052
|
name: "sync",
|
|
826
1053
|
description: "Add or regenerate the probe block of the Makefile (optional)"
|
|
@@ -836,7 +1063,7 @@ var sync_default = defineCommand8({
|
|
|
836
1063
|
});
|
|
837
1064
|
|
|
838
1065
|
// src/commands/wait.ts
|
|
839
|
-
import { defineCommand as
|
|
1066
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
840
1067
|
|
|
841
1068
|
// src/checkers/poll.ts
|
|
842
1069
|
import { setTimeout as delay } from "timers/promises";
|
|
@@ -885,7 +1112,7 @@ function progressReporter() {
|
|
|
885
1112
|
return lines;
|
|
886
1113
|
};
|
|
887
1114
|
}
|
|
888
|
-
var wait_default =
|
|
1115
|
+
var wait_default = defineCommand12({
|
|
889
1116
|
meta: {
|
|
890
1117
|
name: "wait",
|
|
891
1118
|
description: "Check again and again until services are up (all of them by default)"
|
|
@@ -908,14 +1135,14 @@ var wait_default = defineCommand9({
|
|
|
908
1135
|
default: String(DEFAULT_WAIT_MAX_INTERVAL_SECONDS)
|
|
909
1136
|
}
|
|
910
1137
|
},
|
|
911
|
-
async run({ args }) {
|
|
912
|
-
const deadlineMs = secondsToMs("--timeout",
|
|
913
|
-
const intervalMs = secondsToMs("--interval",
|
|
914
|
-
const maxIntervalMs = secondsToMs("--max-interval",
|
|
1138
|
+
async run({ args: args2 }) {
|
|
1139
|
+
const deadlineMs = secondsToMs("--timeout", args2.timeout);
|
|
1140
|
+
const intervalMs = secondsToMs("--interval", args2.interval);
|
|
1141
|
+
const maxIntervalMs = secondsToMs("--max-interval", args2["max-interval"]);
|
|
915
1142
|
if (intervalMs > maxIntervalMs) {
|
|
916
1143
|
throw new ProbeError(
|
|
917
1144
|
"USAGE",
|
|
918
|
-
`--interval (${
|
|
1145
|
+
`--interval (${args2.interval}s) cannot be longer than --max-interval (${args2["max-interval"]}s).`
|
|
919
1146
|
);
|
|
920
1147
|
}
|
|
921
1148
|
const { config, paths } = await openProject();
|
|
@@ -925,7 +1152,7 @@ var wait_default = defineCommand9({
|
|
|
925
1152
|
// A service with its own timeout keeps it here, within what is left of the total.
|
|
926
1153
|
(_passMs, remainingMs) => runChecks(
|
|
927
1154
|
config,
|
|
928
|
-
|
|
1155
|
+
args2._,
|
|
929
1156
|
{ timeoutMs: DEFAULT_TIMEOUT_SECONDS * 1e3, maxTimeoutMs: remainingMs },
|
|
930
1157
|
skipped
|
|
931
1158
|
),
|
|
@@ -939,7 +1166,7 @@ var wait_default = defineCommand9({
|
|
|
939
1166
|
}
|
|
940
1167
|
}
|
|
941
1168
|
);
|
|
942
|
-
if (
|
|
1169
|
+
if (args2.json) {
|
|
943
1170
|
console.log(JSON.stringify(results, null, 2));
|
|
944
1171
|
} else if (results.length === 0) {
|
|
945
1172
|
console.log("No services yet. Add one: probe add api -p 3000");
|
|
@@ -954,6 +1181,22 @@ var wait_default = defineCommand9({
|
|
|
954
1181
|
}
|
|
955
1182
|
});
|
|
956
1183
|
|
|
1184
|
+
// src/completion/services.ts
|
|
1185
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1186
|
+
import { parse as parse2 } from "yaml";
|
|
1187
|
+
async function completableServices(cwd) {
|
|
1188
|
+
try {
|
|
1189
|
+
const paths = findProbeDir(cwd);
|
|
1190
|
+
if (!paths) return [];
|
|
1191
|
+
const raw = parse2(await readFile6(paths.config, "utf8"));
|
|
1192
|
+
const services = raw?.services;
|
|
1193
|
+
if (typeof services !== "object" || services === null) return [];
|
|
1194
|
+
return Object.keys(services).filter((name) => SERVICE_NAME_PATTERN.test(name));
|
|
1195
|
+
} catch {
|
|
1196
|
+
return [];
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
957
1200
|
// src/dashboard/saves.ts
|
|
958
1201
|
function describe(file, error) {
|
|
959
1202
|
const code = error?.code;
|
|
@@ -983,8 +1226,102 @@ function createSaveQueue() {
|
|
|
983
1226
|
};
|
|
984
1227
|
}
|
|
985
1228
|
|
|
1229
|
+
// src/suggest.ts
|
|
1230
|
+
var SHORT_WORD = 4;
|
|
1231
|
+
var TYPOS_SHORT = 1;
|
|
1232
|
+
var TYPOS_LONG = 2;
|
|
1233
|
+
function typoDistance(a, b) {
|
|
1234
|
+
const rows = Array.from(
|
|
1235
|
+
{ length: a.length + 1 },
|
|
1236
|
+
(_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
1237
|
+
);
|
|
1238
|
+
const at = (i, j) => rows[i]?.[j] ?? 0;
|
|
1239
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1240
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1241
|
+
const same = a[i - 1] === b[j - 1];
|
|
1242
|
+
let best = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + (same ? 0 : 1));
|
|
1243
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
1244
|
+
best = Math.min(best, at(i - 2, j - 2) + 1);
|
|
1245
|
+
}
|
|
1246
|
+
rows[i][j] = best;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return at(a.length, b.length);
|
|
1250
|
+
}
|
|
1251
|
+
function closest(word, candidates) {
|
|
1252
|
+
let found;
|
|
1253
|
+
for (const candidate of candidates) {
|
|
1254
|
+
const distance = typoDistance(word.toLowerCase(), candidate);
|
|
1255
|
+
const allowed = candidate.length <= SHORT_WORD ? TYPOS_SHORT : TYPOS_LONG;
|
|
1256
|
+
if (distance > allowed || distance >= candidate.length) continue;
|
|
1257
|
+
if (!found || distance < found.distance) found = { candidate, distance };
|
|
1258
|
+
}
|
|
1259
|
+
return found?.candidate;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/ui/examples.ts
|
|
1263
|
+
var ROOT = "probe";
|
|
1264
|
+
var GAP = " ";
|
|
1265
|
+
var INDENT = " ";
|
|
1266
|
+
var ESC = "\x1B";
|
|
1267
|
+
var EXAMPLES = {
|
|
1268
|
+
[ROOT]: [
|
|
1269
|
+
{ command: "probe init", note: "Start in this project" },
|
|
1270
|
+
{ command: "probe add api -p 3000 --path /health", note: "Declare a service" },
|
|
1271
|
+
{ command: "probe check", note: "Check every service" },
|
|
1272
|
+
{ command: "probe wait && pnpm test", note: "Run the tests once all are up" },
|
|
1273
|
+
{ command: "probe", note: "Open the dashboard (on a terminal)" }
|
|
1274
|
+
],
|
|
1275
|
+
init: [{ command: "probe init", note: "Create .probe/ in this directory" }],
|
|
1276
|
+
add: [
|
|
1277
|
+
{ command: "probe add api -p 3000", note: "http://localhost:3000/" },
|
|
1278
|
+
{ command: "probe add api -p 3000 --path /health", note: "Check another path" },
|
|
1279
|
+
{ command: "probe add admin -p 8080 --expect 200,204", note: "Accept these statuses only" },
|
|
1280
|
+
{ command: "probe add db -p 5432 --timeout 10", note: "Give it 10 s to answer" }
|
|
1281
|
+
],
|
|
1282
|
+
rm: [{ command: "probe rm api", note: "Remove the service named api" }],
|
|
1283
|
+
ls: [{ command: "probe ls", note: "List the services and their URL" }],
|
|
1284
|
+
check: [
|
|
1285
|
+
{ command: "probe check", note: "Check every service" },
|
|
1286
|
+
{ command: "probe check api db", note: "Check these two only" },
|
|
1287
|
+
{ command: "probe check --json", note: "Print the results as JSON" },
|
|
1288
|
+
{ command: "probe check --timeout 10", note: "Give each service 10 s" }
|
|
1289
|
+
],
|
|
1290
|
+
wait: [
|
|
1291
|
+
{ command: "probe wait && pnpm test", note: "Run the tests once all are up" },
|
|
1292
|
+
{ command: "probe wait api --timeout 120", note: "Wait for api, 2 minutes at most" },
|
|
1293
|
+
{ command: "probe wait --interval 1 --max-interval 10", note: "Check less often" }
|
|
1294
|
+
],
|
|
1295
|
+
open: [{ command: "probe open front", note: "Open the URL of front in the browser" }],
|
|
1296
|
+
curl: [
|
|
1297
|
+
{ command: "probe curl api", note: "Print the curl command for api" },
|
|
1298
|
+
{ command: "probe curl api | sh", note: "Run it" }
|
|
1299
|
+
],
|
|
1300
|
+
status: [{ command: "probe status", note: "Version, migration, Makefile block" }],
|
|
1301
|
+
sync: [{ command: "probe sync", note: "Write the probe block of the Makefile" }],
|
|
1302
|
+
migrate: [{ command: "probe migrate", note: "Bring .probe/config.yml up to date" }],
|
|
1303
|
+
completion: [
|
|
1304
|
+
{ command: "probe completion zsh", note: 'In ~/.zshrc: eval "$(probe completion zsh)"' },
|
|
1305
|
+
{ command: "probe completion bash", note: 'In ~/.bashrc: eval "$(probe completion bash)"' },
|
|
1306
|
+
{ command: "probe completion fish | source", note: "In ~/.config/fish/config.fish" }
|
|
1307
|
+
]
|
|
1308
|
+
};
|
|
1309
|
+
function formatExamples(name, color) {
|
|
1310
|
+
const examples = EXAMPLES[name ?? ROOT] ?? [];
|
|
1311
|
+
if (examples.length === 0) return "";
|
|
1312
|
+
const style = (open, close) => (text) => color ? `${ESC}[${open}m${text}${ESC}[${close}m` : text;
|
|
1313
|
+
const title = style("1;4", "22;24");
|
|
1314
|
+
const cyan = style("36", "39");
|
|
1315
|
+
const width = Math.max(...examples.map((example) => example.command.length));
|
|
1316
|
+
const lines = examples.map(
|
|
1317
|
+
({ command, note }) => `${INDENT}${cyan(command.padEnd(width))}${GAP}${note}`
|
|
1318
|
+
);
|
|
1319
|
+
return [title("EXAMPLES"), "", ...lines].join("\n");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
986
1322
|
// src/cli.ts
|
|
987
|
-
var
|
|
1323
|
+
var commands = { init: init_default, add: add_default, rm: rm_default, ls: ls_default, check: check_default, wait: wait_default, open: open_default, curl: curl_default, status: status_default, sync: sync_default, migrate: migrate_default };
|
|
1324
|
+
var main = defineCommand13({
|
|
988
1325
|
meta: {
|
|
989
1326
|
name: "probe",
|
|
990
1327
|
version: CLI_VERSION,
|
|
@@ -998,16 +1335,21 @@ var main = defineCommand10({
|
|
|
998
1335
|
default: String(DASHBOARD_INTERVAL_SECONDS)
|
|
999
1336
|
}
|
|
1000
1337
|
},
|
|
1001
|
-
subCommands: {
|
|
1338
|
+
subCommands: { ...commands, completion: completion(commands) }
|
|
1002
1339
|
});
|
|
1003
1340
|
async function printUsage(command, parent) {
|
|
1004
1341
|
const usage = await renderUsage(command, parent);
|
|
1005
|
-
|
|
1006
|
-
|
|
1342
|
+
const { color } = stdoutTerminal();
|
|
1343
|
+
const meta2 = await (typeof command.meta === "function" ? command.meta() : command.meta);
|
|
1344
|
+
const examples = formatExamples(command === main ? void 0 : meta2?.name, color);
|
|
1345
|
+
console.log(`${color ? usage : stripStyles(usage)}
|
|
1346
|
+
${examples ? `
|
|
1347
|
+
${examples}
|
|
1348
|
+
` : ""}`);
|
|
1007
1349
|
}
|
|
1008
1350
|
function notifyUpdate(rawArgs) {
|
|
1009
1351
|
const cliPath = process.argv[1];
|
|
1010
|
-
if (updateCheckDisabled() || !cliPath) return;
|
|
1352
|
+
if (updateCheckDisabled() || !cliPath || rawArgs[0] === COMPLETION_COMMAND) return;
|
|
1011
1353
|
scheduleRefresh(cacheDir(), cliPath);
|
|
1012
1354
|
if (!detectTerminal(process.stderr, process.env).isTTY || rawArgs.includes("--json") || rawArgs[0] === "status")
|
|
1013
1355
|
return;
|
|
@@ -1043,7 +1385,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1043
1385
|
if (!asked) return false;
|
|
1044
1386
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1045
1387
|
}
|
|
1046
|
-
const { openDashboard } = await import("./open-
|
|
1388
|
+
const { openDashboard } = await import("./open-T3OQH2O4.js");
|
|
1047
1389
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1048
1390
|
const saves = createSaveQueue();
|
|
1049
1391
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|
|
@@ -1067,13 +1409,25 @@ async function openedDashboard(rawArgs) {
|
|
|
1067
1409
|
for (const failure of saves.failures()) console.error(`probe: not saved: ${failure}`);
|
|
1068
1410
|
return true;
|
|
1069
1411
|
}
|
|
1412
|
+
var UNKNOWN_COMMAND = "E_UNKNOWN_COMMAND";
|
|
1413
|
+
function didYouMean(error, rawArgs) {
|
|
1414
|
+
if (error.code !== UNKNOWN_COMMAND) return "";
|
|
1415
|
+
const typed = rawArgs.find((arg) => !arg.startsWith("-"));
|
|
1416
|
+
const command = typed && closest(typed, Object.keys(main.subCommands ?? {}));
|
|
1417
|
+
return command ? ` Did you mean: probe ${command}?` : "";
|
|
1418
|
+
}
|
|
1070
1419
|
function flushed(stream) {
|
|
1071
1420
|
return new Promise((done) => {
|
|
1072
1421
|
stream.write("", () => done());
|
|
1073
1422
|
});
|
|
1074
1423
|
}
|
|
1424
|
+
async function printCompletions(kind) {
|
|
1425
|
+
if (kind !== "services") return;
|
|
1426
|
+
for (const name of await completableServices(process.cwd())) console.log(name);
|
|
1427
|
+
}
|
|
1075
1428
|
async function run(rawArgs) {
|
|
1076
1429
|
if (rawArgs[0] === REFRESH_COMMAND) return refreshCache(cacheDir());
|
|
1430
|
+
if (rawArgs[0] === COMPLETE_COMMAND) return printCompletions(rawArgs[1]);
|
|
1077
1431
|
const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
1078
1432
|
const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
|
|
1079
1433
|
if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
|
|
@@ -1085,7 +1439,8 @@ async function run(rawArgs) {
|
|
|
1085
1439
|
console.error(`probe: ${error.message}`);
|
|
1086
1440
|
} else if (error instanceof Error && error.name === "CLIError") {
|
|
1087
1441
|
if (rawArgs.length === 0) await printUsage(main);
|
|
1088
|
-
|
|
1442
|
+
const message = stripStyles(error.message).replace(/\.$/, "");
|
|
1443
|
+
console.error(`probe: ${message}.${didYouMean(error, rawArgs)} See: probe --help`);
|
|
1089
1444
|
} else {
|
|
1090
1445
|
console.error(`probe: ${error instanceof Error ? error.message : String(error)}`);
|
|
1091
1446
|
}
|