@ninhache/probe 0.8.0 → 0.10.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 +34 -11
- package/dist/{chunk-Z4F2X5IS.js → chunk-VSQVZK7F.js} +177 -6
- package/dist/cli.js +74 -76
- package/dist/{open-2H7PHDYL.js → open-W2TWG636.js} +661 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ tick. Nothing is ticked at first, and nothing is written unless you tick it. The
|
|
|
81
81
|
each, redirects not followed).
|
|
82
82
|
`PROBE_DETECT_PORTS=4000,4001` tries other ports, `PROBE_DETECT_PORTS=` none.
|
|
83
83
|
|
|
84
|
-
`probe rm`
|
|
84
|
+
`probe rm` deletes something, so on a terminal it asks first, with "No" selected: `--yes`
|
|
85
85
|
(`-y`) skips the question. Off a terminal, in a script or in CI, it removes at once, as before.
|
|
86
86
|
|
|
87
87
|
`probe open` uses `open` on macOS and `xdg-open` elsewhere. Without it, the URL is printed and the exit code is
|
|
@@ -109,15 +109,38 @@ of macOS.
|
|
|
109
109
|
`probe` alone, on a terminal, opens a live list of your services and checks them every 2 s
|
|
110
110
|
(`probe --interval 5` for another pace). Off a terminal, in CI or in a pipe, nothing changes: use `probe check`.
|
|
111
111
|
|
|
112
|
-
| Key | What it does
|
|
113
|
-
| --------------- |
|
|
114
|
-
| `↑` `↓` `j` `k` | Move
|
|
115
|
-
| `r` | Check now
|
|
116
|
-
| `Enter` | Open the
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
| Key | What it does |
|
|
113
|
+
| --------------- | ---------------------------------------------- |
|
|
114
|
+
| `↑` `↓` `j` `k` | Move |
|
|
115
|
+
| `r` | Check now |
|
|
116
|
+
| `Enter` | Open the detail of the selected service |
|
|
117
|
+
| `u` | Open its URL in the browser |
|
|
118
|
+
| `c` | Copy its curl command |
|
|
119
|
+
| `/` | Filter by name; `Enter` keeps it, `Esc` clears |
|
|
120
|
+
| `a` | Add a service |
|
|
121
|
+
| `e` | Edit the selected service |
|
|
122
|
+
| `d` | Remove the selected service, after a question |
|
|
123
|
+
| `:` | Find a command by what it does; `Enter` runs |
|
|
124
|
+
| `o` | Options: interval and timeout |
|
|
125
|
+
| `?` | Every key; `?` or `Esc` closes |
|
|
126
|
+
| `q` | Quit |
|
|
127
|
+
|
|
128
|
+
The bar at the bottom shows the keys you need first; `?` lists them all.
|
|
129
|
+
|
|
130
|
+
`c` uses `pbcopy` on macOS, `wl-copy` or `xclip` on Linux. Without one of them, the command is shown instead,
|
|
131
|
+
to copy by hand.
|
|
132
|
+
|
|
133
|
+
`a` and `e` open a form: name, port, host, path, expected status. `↑` `↓` or `Tab` change field, `Enter` saves,
|
|
134
|
+
`Esc` cancels. A field left empty takes its default. `d` asks first: only `y` removes. All three write
|
|
135
|
+
`.probe/config.yml` the way `probe add` and `probe rm` do: comments kept, [Makefile](#makefile) block kept up to
|
|
136
|
+
date. While you type, in the filter, the form or the command palette, `q` and `?` are letters.
|
|
137
|
+
|
|
138
|
+
`Enter` shows the last answer of the selected service above its two settings: status, latencies of this
|
|
139
|
+
session, headers and the first lines of the body; for a service that is down, what failed and what to try. The
|
|
140
|
+
value of a header that can carry a secret (`Authorization`, `Set-Cookie`, anything named after a token or a key)
|
|
141
|
+
is replaced by `(hidden)`. None of it is written anywhere, and `probe check --json` prints what it printed before.
|
|
142
|
+
|
|
143
|
+
The panels save what you change in `.probe/local.yml`, for your machine only:
|
|
121
144
|
|
|
122
145
|
```yaml
|
|
123
146
|
dashboard: { interval: 5, timeout: 3 }
|
|
@@ -180,7 +203,7 @@ outside the block is yours and is never touched. Without a block, no command cre
|
|
|
180
203
|
# <<< probe
|
|
181
204
|
```
|
|
182
205
|
|
|
183
|
-
Once the block is there, `probe add
|
|
206
|
+
Once the block is there, `probe add`, `probe rm` and the dashboard keep it up to date. Commit the `Makefile` and
|
|
184
207
|
`.probe/lock.yml`.
|
|
185
208
|
|
|
186
209
|
Targets: `help`, `check`, `status`, `probe` (opens the [dashboard](#dashboard)) and one `check-<name>` per
|
|
@@ -14,6 +14,9 @@ var DEFAULT_MAKEFILE = "Makefile";
|
|
|
14
14
|
var DEFAULT_CHECKER_TYPE = "http";
|
|
15
15
|
var DEFAULT_TIMEOUT_SECONDS = 3;
|
|
16
16
|
var DASHBOARD_INTERVAL_SECONDS = 2;
|
|
17
|
+
var DASHBOARD_HISTORY_LENGTH = 30;
|
|
18
|
+
var RESPONSE_BODY_BYTES = 2048;
|
|
19
|
+
var RESPONSE_BODY_WAIT_MS = 500;
|
|
17
20
|
var DASHBOARD_INTERVAL_CHOICES_SECONDS = [0.5, 1, 2, 5, 10, 30, 60];
|
|
18
21
|
var DASHBOARD_TIMEOUT_CHOICES_SECONDS = [1, 2, 3, 5, 10, 30];
|
|
19
22
|
var MAX_TIMEOUT_SECONDS = 2147483;
|
|
@@ -162,6 +165,79 @@ function diagnoseStatus(service, status) {
|
|
|
162
165
|
return { error, hint: `the service answers: check \`path\` and \`expect\` in ${CONFIG}` };
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// src/checkers/response.ts
|
|
169
|
+
var HIDDEN_VALUE = "(hidden)";
|
|
170
|
+
var SENSITIVE_HEADER = /auth|cookie|token|secret|key|session|password|credential/i;
|
|
171
|
+
var TEXT_TYPE = /^text\/|json|xml|javascript|yaml|x-www-form-urlencoded/i;
|
|
172
|
+
var NUL = 0;
|
|
173
|
+
var FIRST_PRINTABLE = 32;
|
|
174
|
+
var DELETE = 127;
|
|
175
|
+
var LAST_C1_CONTROL = 159;
|
|
176
|
+
var REPLACEMENT = "\uFFFD";
|
|
177
|
+
function isControl(character) {
|
|
178
|
+
const code = character.codePointAt(0) ?? NUL;
|
|
179
|
+
return code < FIRST_PRINTABLE || code >= DELETE && code <= LAST_C1_CONTROL;
|
|
180
|
+
}
|
|
181
|
+
function printable(text, kept = "") {
|
|
182
|
+
return Array.from(text).filter((character) => kept.includes(character) || !isControl(character)).join("");
|
|
183
|
+
}
|
|
184
|
+
async function readStart(body) {
|
|
185
|
+
const reader = body.getReader();
|
|
186
|
+
const chunks = [];
|
|
187
|
+
let size = 0;
|
|
188
|
+
let ended = false;
|
|
189
|
+
let timer;
|
|
190
|
+
const late = new Promise((resolve) => {
|
|
191
|
+
timer = setTimeout(() => resolve("late"), RESPONSE_BODY_WAIT_MS);
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
while (size < RESPONSE_BODY_BYTES) {
|
|
195
|
+
const read = reader.read();
|
|
196
|
+
read.catch(() => {
|
|
197
|
+
});
|
|
198
|
+
const next = await Promise.race([read, late]);
|
|
199
|
+
if (next === "late") break;
|
|
200
|
+
if (next.done) {
|
|
201
|
+
ended = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
chunks.push(next.value);
|
|
205
|
+
size += next.value.length;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
void reader.cancel().catch(() => {
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
bytes: Buffer.concat(chunks).subarray(0, RESPONSE_BODY_BYTES),
|
|
215
|
+
cut: !ended || size > RESPONSE_BODY_BYTES
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function describeResponse(response) {
|
|
219
|
+
const headers = [...response.headers].map(([name, value]) => [
|
|
220
|
+
printable(name),
|
|
221
|
+
SENSITIVE_HEADER.test(name) ? HIDDEN_VALUE : printable(value)
|
|
222
|
+
]);
|
|
223
|
+
const detail = { statusText: printable(response.statusText), headers };
|
|
224
|
+
if (!response.body) return detail;
|
|
225
|
+
const type = printable(response.headers.get("content-type")?.split(";")[0] ?? "").trim();
|
|
226
|
+
if (type && !TEXT_TYPE.test(type)) {
|
|
227
|
+
void response.body.cancel().catch(() => {
|
|
228
|
+
});
|
|
229
|
+
return { ...detail, bodyNote: `${type} body, not shown` };
|
|
230
|
+
}
|
|
231
|
+
const { bytes, cut } = await readStart(response.body);
|
|
232
|
+
if (bytes.length === 0) return detail;
|
|
233
|
+
if (bytes.includes(NUL)) return { ...detail, bodyNote: "binary body, not shown" };
|
|
234
|
+
const decoded = new TextDecoder().decode(bytes).replaceAll(" ", " ");
|
|
235
|
+
const text = printable(cut ? decoded.replace(new RegExp(`${REPLACEMENT}+$`), "") : decoded, "\n");
|
|
236
|
+
const body = text.trimEnd();
|
|
237
|
+
if (!body) return detail;
|
|
238
|
+
return cut ? { ...detail, body, bodyCut: true } : { ...detail, body };
|
|
239
|
+
}
|
|
240
|
+
|
|
165
241
|
// src/checkers/http.ts
|
|
166
242
|
function targetUrl(service) {
|
|
167
243
|
const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
|
|
@@ -183,7 +259,7 @@ function expectsRedirect(expect) {
|
|
|
183
259
|
}
|
|
184
260
|
var httpChecker = {
|
|
185
261
|
type: "http",
|
|
186
|
-
async check(name, service, { timeoutMs, signal }) {
|
|
262
|
+
async check(name, service, { timeoutMs, signal, detail }) {
|
|
187
263
|
const target = targetUrl(service);
|
|
188
264
|
const base = { name, type: "http", target };
|
|
189
265
|
const started = performance.now();
|
|
@@ -196,10 +272,20 @@ var httpChecker = {
|
|
|
196
272
|
redirect: expectsRedirect(service.expect) ? "manual" : "follow"
|
|
197
273
|
});
|
|
198
274
|
const latencyMs = elapsed();
|
|
199
|
-
await response.body?.cancel();
|
|
200
275
|
const { status } = response;
|
|
201
|
-
|
|
202
|
-
|
|
276
|
+
const described = detail ? { response: await describeResponse(response) } : {};
|
|
277
|
+
if (!detail) await response.body?.cancel();
|
|
278
|
+
if (isExpected(service.expect, status)) {
|
|
279
|
+
return { ...base, ok: true, status, latencyMs, ...described };
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
ok: false,
|
|
284
|
+
status,
|
|
285
|
+
latencyMs,
|
|
286
|
+
...diagnoseStatus(service, status),
|
|
287
|
+
...described
|
|
288
|
+
};
|
|
203
289
|
} catch (error) {
|
|
204
290
|
const diagnosis = diagnoseFailure(error, service, timeoutMs);
|
|
205
291
|
return { ...base, ok: false, latencyMs: elapsed(), ...diagnosis };
|
|
@@ -251,12 +337,68 @@ async function runChecks(config, names, options, skipped = /* @__PURE__ */ new S
|
|
|
251
337
|
}
|
|
252
338
|
return getChecker(type).check(name, service, {
|
|
253
339
|
timeoutMs: resolveTimeoutMs(service, options),
|
|
254
|
-
signal: options.signal
|
|
340
|
+
signal: options.signal,
|
|
341
|
+
detail: options.detail
|
|
255
342
|
});
|
|
256
343
|
})
|
|
257
344
|
);
|
|
258
345
|
}
|
|
259
346
|
|
|
347
|
+
// src/config/schema.ts
|
|
348
|
+
import { z } from "zod";
|
|
349
|
+
var CURRENT_SCHEMA_VERSION = 1;
|
|
350
|
+
var SERVICE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
351
|
+
var statusCode = z.number().int().min(100).max(599);
|
|
352
|
+
var serviceSchemaV1 = z.strictObject({
|
|
353
|
+
host: z.string().min(1).default("localhost"),
|
|
354
|
+
port: z.number().int().min(1).max(65535),
|
|
355
|
+
path: z.string().startsWith("/").default("/"),
|
|
356
|
+
expect: z.union([statusCode, z.array(statusCode).min(1)]).optional(),
|
|
357
|
+
/** Seconds given to this service to answer, instead of the default. */
|
|
358
|
+
timeout: z.number().positive().max(MAX_TIMEOUT_SECONDS).optional()
|
|
359
|
+
});
|
|
360
|
+
var configSchemaV1 = z.strictObject({
|
|
361
|
+
schemaVersion: z.literal(1),
|
|
362
|
+
services: z.record(
|
|
363
|
+
z.string().regex(SERVICE_NAME_PATTERN, "letters, digits, _ and - only"),
|
|
364
|
+
serviceSchemaV1
|
|
365
|
+
).nullish().transform((services) => services ?? {})
|
|
366
|
+
});
|
|
367
|
+
var schemas = {
|
|
368
|
+
1: configSchemaV1
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/config/service-input.ts
|
|
372
|
+
var INTEGER = /^\d+$/;
|
|
373
|
+
function schemaProblem(schema, value) {
|
|
374
|
+
const result = schema.safeParse(value);
|
|
375
|
+
return result.success ? void 0 : result.error.issues[0]?.message;
|
|
376
|
+
}
|
|
377
|
+
function parseExpectList(value) {
|
|
378
|
+
const codes = value.split(",").map((code) => Number(code.trim()));
|
|
379
|
+
return codes.length === 1 ? codes[0] : codes;
|
|
380
|
+
}
|
|
381
|
+
function nameProblem(name, taken) {
|
|
382
|
+
if (!SERVICE_NAME_PATTERN.test(name)) return "Letters, digits, _ and - only, e.g. api";
|
|
383
|
+
if ([...taken].includes(name)) return `${name} already exists`;
|
|
384
|
+
return void 0;
|
|
385
|
+
}
|
|
386
|
+
function portProblem(port) {
|
|
387
|
+
if (!INTEGER.test(port)) return "A port number, e.g. 3000";
|
|
388
|
+
return schemaProblem(serviceSchemaV1.shape.port, Number(port)) && "A port between 1 and 65535";
|
|
389
|
+
}
|
|
390
|
+
function hostProblem(host) {
|
|
391
|
+
return /\s|\//.test(host) || host === "" ? "A host name or an IP, e.g. localhost" : void 0;
|
|
392
|
+
}
|
|
393
|
+
function pathProblem(path) {
|
|
394
|
+
return schemaProblem(serviceSchemaV1.shape.path, path) && "A path that starts with /";
|
|
395
|
+
}
|
|
396
|
+
function expectProblem(expect) {
|
|
397
|
+
const codes = expect.split(",").map((code) => code.trim());
|
|
398
|
+
if (!codes.every((code) => INTEGER.test(code))) return "A status, or several: 200 or 200,204";
|
|
399
|
+
return schemaProblem(serviceSchemaV1.shape.expect, parseExpectList(expect)) && "Statuses go from 100 to 599";
|
|
400
|
+
}
|
|
401
|
+
|
|
260
402
|
// src/ui/colors.ts
|
|
261
403
|
function createColors(enabled) {
|
|
262
404
|
const wrap = (open, close) => (text) => enabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
@@ -334,6 +476,22 @@ function formatServices(config, terminal, skipped = /* @__PURE__ */ new Set()) {
|
|
|
334
476
|
}).join("\n");
|
|
335
477
|
}
|
|
336
478
|
|
|
479
|
+
// src/open-url.ts
|
|
480
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
481
|
+
function openerCommand(platform) {
|
|
482
|
+
return platform === "darwin" ? "open" : "xdg-open";
|
|
483
|
+
}
|
|
484
|
+
function openUrl(url, platform = process.platform, spawn = nodeSpawn) {
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
const child = spawn(openerCommand(platform), [url], { detached: true, stdio: "ignore" });
|
|
487
|
+
child.once("error", () => resolve(false));
|
|
488
|
+
child.once("spawn", () => {
|
|
489
|
+
child.unref();
|
|
490
|
+
resolve(true);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
337
495
|
export {
|
|
338
496
|
PACKAGE_NAME,
|
|
339
497
|
PROBE_DIR,
|
|
@@ -346,6 +504,7 @@ export {
|
|
|
346
504
|
DEFAULT_MAKEFILE,
|
|
347
505
|
DEFAULT_TIMEOUT_SECONDS,
|
|
348
506
|
DASHBOARD_INTERVAL_SECONDS,
|
|
507
|
+
DASHBOARD_HISTORY_LENGTH,
|
|
349
508
|
DASHBOARD_INTERVAL_CHOICES_SECONDS,
|
|
350
509
|
DASHBOARD_TIMEOUT_CHOICES_SECONDS,
|
|
351
510
|
MAX_TIMEOUT_SECONDS,
|
|
@@ -365,6 +524,16 @@ export {
|
|
|
365
524
|
DETECT_PORTS,
|
|
366
525
|
DETECT_PORTS_ENV,
|
|
367
526
|
DETECT_TIMEOUT_MS,
|
|
527
|
+
CURRENT_SCHEMA_VERSION,
|
|
528
|
+
SERVICE_NAME_PATTERN,
|
|
529
|
+
serviceSchemaV1,
|
|
530
|
+
schemas,
|
|
531
|
+
parseExpectList,
|
|
532
|
+
nameProblem,
|
|
533
|
+
portProblem,
|
|
534
|
+
hostProblem,
|
|
535
|
+
pathProblem,
|
|
536
|
+
expectProblem,
|
|
368
537
|
ProbeError,
|
|
369
538
|
targetUrl,
|
|
370
539
|
curlCommand,
|
|
@@ -375,5 +544,7 @@ export {
|
|
|
375
544
|
stripStyles,
|
|
376
545
|
runChecks,
|
|
377
546
|
formatCheckResults,
|
|
378
|
-
formatServices
|
|
547
|
+
formatServices,
|
|
548
|
+
openerCommand,
|
|
549
|
+
openUrl
|
|
379
550
|
};
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
BACKUP_GLOB,
|
|
6
6
|
COMPLETE_COMMAND,
|
|
7
7
|
CONFIG_FILE,
|
|
8
|
+
CURRENT_SCHEMA_VERSION,
|
|
8
9
|
DASHBOARD_INTERVAL_SECONDS,
|
|
9
10
|
DEFAULT_MAKEFILE,
|
|
10
11
|
DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
REFRESH_COMMAND,
|
|
28
29
|
REGISTRY_LATEST_URL,
|
|
29
30
|
REGISTRY_TIMEOUT_MS,
|
|
31
|
+
SERVICE_NAME_PATTERN,
|
|
30
32
|
UPDATE_CACHE_FILE,
|
|
31
33
|
UPDATE_CHECK_TTL_MS,
|
|
32
34
|
WAIT_BACKOFF_FACTOR,
|
|
@@ -34,13 +36,21 @@ import {
|
|
|
34
36
|
createColors,
|
|
35
37
|
curlCommand,
|
|
36
38
|
detectTerminal,
|
|
39
|
+
expectProblem,
|
|
37
40
|
formatCheckResults,
|
|
38
41
|
formatServices,
|
|
39
42
|
isInteractive,
|
|
43
|
+
nameProblem,
|
|
44
|
+
openUrl,
|
|
45
|
+
openerCommand,
|
|
46
|
+
pathProblem,
|
|
47
|
+
portProblem,
|
|
40
48
|
runChecks,
|
|
49
|
+
schemas,
|
|
50
|
+
serviceSchemaV1,
|
|
41
51
|
stripStyles,
|
|
42
52
|
targetUrl
|
|
43
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-VSQVZK7F.js";
|
|
44
54
|
import "./chunk-M3ASEG5F.js";
|
|
45
55
|
|
|
46
56
|
// src/cli.ts
|
|
@@ -54,30 +64,6 @@ import {
|
|
|
54
64
|
// src/commands/add.ts
|
|
55
65
|
import { defineCommand } from "citty";
|
|
56
66
|
|
|
57
|
-
// src/config/schema.ts
|
|
58
|
-
import { z } from "zod";
|
|
59
|
-
var CURRENT_SCHEMA_VERSION = 1;
|
|
60
|
-
var SERVICE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
61
|
-
var statusCode = z.number().int().min(100).max(599);
|
|
62
|
-
var serviceSchemaV1 = z.strictObject({
|
|
63
|
-
host: z.string().min(1).default("localhost"),
|
|
64
|
-
port: z.number().int().min(1).max(65535),
|
|
65
|
-
path: z.string().startsWith("/").default("/"),
|
|
66
|
-
expect: z.union([statusCode, z.array(statusCode).min(1)]).optional(),
|
|
67
|
-
/** Seconds given to this service to answer, instead of the default. */
|
|
68
|
-
timeout: z.number().positive().max(MAX_TIMEOUT_SECONDS).optional()
|
|
69
|
-
});
|
|
70
|
-
var configSchemaV1 = z.strictObject({
|
|
71
|
-
schemaVersion: z.literal(1),
|
|
72
|
-
services: z.record(
|
|
73
|
-
z.string().regex(SERVICE_NAME_PATTERN, "letters, digits, _ and - only"),
|
|
74
|
-
serviceSchemaV1
|
|
75
|
-
).nullish().transform((services) => services ?? {})
|
|
76
|
-
});
|
|
77
|
-
var schemas = {
|
|
78
|
-
1: configSchemaV1
|
|
79
|
-
};
|
|
80
|
-
|
|
81
67
|
// src/ui/prompt.ts
|
|
82
68
|
async function confirm(message, streams = {}) {
|
|
83
69
|
const { confirm: ask2, isCancel } = await import("./dist-SITQEC3X.js");
|
|
@@ -619,10 +605,10 @@ import { join as join4 } from "path";
|
|
|
619
605
|
// src/config/lock.ts
|
|
620
606
|
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
621
607
|
import { parse as parse2, stringify } from "yaml";
|
|
622
|
-
import { z
|
|
623
|
-
var lockSchema =
|
|
624
|
-
cliVersion:
|
|
625
|
-
makefile:
|
|
608
|
+
import { z } from "zod";
|
|
609
|
+
var lockSchema = z.object({
|
|
610
|
+
cliVersion: z.string(),
|
|
611
|
+
makefile: z.object({ hash: z.string() })
|
|
626
612
|
});
|
|
627
613
|
var HEADER = "# Generated by probe. Do not edit.\n";
|
|
628
614
|
async function readLock(paths) {
|
|
@@ -638,7 +624,7 @@ async function writeLock(paths, lock) {
|
|
|
638
624
|
}
|
|
639
625
|
|
|
640
626
|
// src/version.ts
|
|
641
|
-
var CLI_VERSION = true ? "0.
|
|
627
|
+
var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
642
628
|
|
|
643
629
|
// src/makefile/generator.ts
|
|
644
630
|
import { createHash } from "crypto";
|
|
@@ -759,16 +745,21 @@ function rawServices(loaded) {
|
|
|
759
745
|
return loaded.raw.services ?? {};
|
|
760
746
|
}
|
|
761
747
|
async function saveServices(loaded, services) {
|
|
748
|
+
const { warning } = await writeServices(loaded, services);
|
|
749
|
+
if (warning) console.error(`probe: ${warning}`);
|
|
750
|
+
}
|
|
751
|
+
async function writeServices(loaded, services) {
|
|
762
752
|
const block = await blockState(loaded.paths, loaded.config);
|
|
763
753
|
const saved = await saveConfig(loaded, { ...loaded.raw, services });
|
|
764
|
-
if (block === "missing") return;
|
|
754
|
+
if (block === "missing") return { saved };
|
|
765
755
|
if (block !== "modified") {
|
|
766
756
|
await syncMakefile(saved.paths, saved.config);
|
|
767
|
-
return;
|
|
757
|
+
return { saved };
|
|
768
758
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
759
|
+
return {
|
|
760
|
+
saved,
|
|
761
|
+
warning: "the Makefile block was edited by hand, left as is. Run `probe sync` to regenerate it."
|
|
762
|
+
};
|
|
772
763
|
}
|
|
773
764
|
|
|
774
765
|
// src/commands/detected.ts
|
|
@@ -804,26 +795,7 @@ function parseExpect(value) {
|
|
|
804
795
|
const codes = value.split(",").map((code) => parseInteger(code.trim(), "--expect"));
|
|
805
796
|
return codes.length === 1 ? codes[0] : codes;
|
|
806
797
|
}
|
|
807
|
-
var INTEGER = /^\d+$/;
|
|
808
798
|
var DEFAULT_PATH = "/";
|
|
809
|
-
function schemaProblem(schema, value) {
|
|
810
|
-
const result = schema.safeParse(value);
|
|
811
|
-
return result.success ? void 0 : result.error.issues[0]?.message;
|
|
812
|
-
}
|
|
813
|
-
function nameProblem(name, services) {
|
|
814
|
-
if (!SERVICE_NAME_PATTERN.test(name)) return "Letters, digits, _ and - only, e.g. api";
|
|
815
|
-
if (Object.hasOwn(services, name)) return `${name} already exists`;
|
|
816
|
-
return void 0;
|
|
817
|
-
}
|
|
818
|
-
function portProblem(port) {
|
|
819
|
-
if (!INTEGER.test(port)) return "A port number, e.g. 3000";
|
|
820
|
-
return schemaProblem(serviceSchemaV1.shape.port, Number(port)) && "A port between 1 and 65535";
|
|
821
|
-
}
|
|
822
|
-
function expectProblem(expect) {
|
|
823
|
-
const codes = expect.split(",").map((code) => code.trim());
|
|
824
|
-
if (!codes.every((code) => INTEGER.test(code))) return "A status, or several: 200 or 200,204";
|
|
825
|
-
return schemaProblem(serviceSchemaV1.shape.expect, parseExpect(expect)) && "Statuses go from 100 to 599";
|
|
826
|
-
}
|
|
827
799
|
async function guidedAdd() {
|
|
828
800
|
const loaded = await openProject();
|
|
829
801
|
const services = rawServices(loaded);
|
|
@@ -838,14 +810,14 @@ async function guidedAdd() {
|
|
|
838
810
|
{
|
|
839
811
|
message: "Service name",
|
|
840
812
|
placeholder: "api",
|
|
841
|
-
validate: (v) => nameProblem(v, services)
|
|
813
|
+
validate: (v) => nameProblem(v, Object.keys(services))
|
|
842
814
|
},
|
|
843
815
|
{ message: "Port", placeholder: "3000", validate: portProblem },
|
|
844
816
|
{
|
|
845
817
|
message: "Path to request",
|
|
846
818
|
placeholder: DEFAULT_PATH,
|
|
847
819
|
defaultValue: DEFAULT_PATH,
|
|
848
|
-
validate:
|
|
820
|
+
validate: pathProblem
|
|
849
821
|
},
|
|
850
822
|
{
|
|
851
823
|
message: "Expected status",
|
|
@@ -1295,24 +1267,6 @@ var migrate_default = defineCommand7({
|
|
|
1295
1267
|
|
|
1296
1268
|
// src/commands/open.ts
|
|
1297
1269
|
import { defineCommand as defineCommand8 } from "citty";
|
|
1298
|
-
|
|
1299
|
-
// src/open-url.ts
|
|
1300
|
-
import { spawn as nodeSpawn } from "child_process";
|
|
1301
|
-
function openerCommand(platform) {
|
|
1302
|
-
return platform === "darwin" ? "open" : "xdg-open";
|
|
1303
|
-
}
|
|
1304
|
-
function openUrl(url, platform = process.platform, spawn2 = nodeSpawn) {
|
|
1305
|
-
return new Promise((resolve2) => {
|
|
1306
|
-
const child = spawn2(openerCommand(platform), [url], { detached: true, stdio: "ignore" });
|
|
1307
|
-
child.once("error", () => resolve2(false));
|
|
1308
|
-
child.once("spawn", () => {
|
|
1309
|
-
child.unref();
|
|
1310
|
-
resolve2(true);
|
|
1311
|
-
});
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// src/commands/open.ts
|
|
1316
1270
|
var open_default = defineCommand8({
|
|
1317
1271
|
meta: { name: "open", description: "Open the URL of a service in the browser" },
|
|
1318
1272
|
args: {
|
|
@@ -1356,6 +1310,38 @@ var rm_default = defineCommand9({
|
|
|
1356
1310
|
}
|
|
1357
1311
|
});
|
|
1358
1312
|
|
|
1313
|
+
// src/commands/service-writes.ts
|
|
1314
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
1315
|
+
var DEFAULTS = { host: "localhost", path: "/", expect: void 0 };
|
|
1316
|
+
var FIELD_KEYS = ["host", "port", "path", "expect"];
|
|
1317
|
+
var sameValue = (a, b) => isDeepStrictEqual2([a].flat(), [b].flat());
|
|
1318
|
+
function withFields(service, fields, current) {
|
|
1319
|
+
const next = { ...service };
|
|
1320
|
+
for (const key of FIELD_KEYS) {
|
|
1321
|
+
if (current && sameValue(fields[key], current[key])) continue;
|
|
1322
|
+
if (isDeepStrictEqual2(fields[key], DEFAULTS[key])) delete next[key];
|
|
1323
|
+
else next[key] = fields[key];
|
|
1324
|
+
}
|
|
1325
|
+
return next;
|
|
1326
|
+
}
|
|
1327
|
+
async function writeService(paths, write) {
|
|
1328
|
+
const loaded = await loadConfig(paths);
|
|
1329
|
+
const services = rawServices(loaded);
|
|
1330
|
+
const exists = Object.hasOwn(services, write.name);
|
|
1331
|
+
if (exists === (write.kind === "add")) return null;
|
|
1332
|
+
const { [write.name]: previous, ...others } = services;
|
|
1333
|
+
const next = write.kind === "remove" ? others : {
|
|
1334
|
+
...services,
|
|
1335
|
+
[write.name]: withFields(
|
|
1336
|
+
previous ?? {},
|
|
1337
|
+
write.fields,
|
|
1338
|
+
loaded.config.services[write.name]
|
|
1339
|
+
)
|
|
1340
|
+
};
|
|
1341
|
+
const { saved, warning } = await writeServices(loaded, next);
|
|
1342
|
+
return { config: saved.config, warning };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1359
1345
|
// src/commands/status.ts
|
|
1360
1346
|
import { defineCommand as defineCommand10 } from "citty";
|
|
1361
1347
|
|
|
@@ -1810,7 +1796,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1810
1796
|
if (!asked) return false;
|
|
1811
1797
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1812
1798
|
}
|
|
1813
|
-
const { openDashboard } = await import("./open-
|
|
1799
|
+
const { openDashboard } = await import("./open-W2TWG636.js");
|
|
1814
1800
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1815
1801
|
const saves = createSaveQueue();
|
|
1816
1802
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|
|
@@ -1828,7 +1814,19 @@ async function openedDashboard(rawArgs) {
|
|
|
1828
1814
|
`${PROBE_DIR}/${CONFIG_FILE}`,
|
|
1829
1815
|
() => writeServiceTimeout(paths, name, seconds2),
|
|
1830
1816
|
`${name} is no longer in ${PROBE_DIR}/${CONFIG_FILE}`
|
|
1831
|
-
)
|
|
1817
|
+
),
|
|
1818
|
+
onServiceWrite: async (write) => {
|
|
1819
|
+
let written = null;
|
|
1820
|
+
const failure = await saves.save(
|
|
1821
|
+
`${PROBE_DIR}/${CONFIG_FILE}`,
|
|
1822
|
+
async () => {
|
|
1823
|
+
written = await writeService(paths, write);
|
|
1824
|
+
return written !== null;
|
|
1825
|
+
},
|
|
1826
|
+
write.kind === "add" ? `${write.name} is already in ${PROBE_DIR}/${CONFIG_FILE}` : `${write.name} is no longer in ${PROBE_DIR}/${CONFIG_FILE}`
|
|
1827
|
+
);
|
|
1828
|
+
return { failure, ...written };
|
|
1829
|
+
}
|
|
1832
1830
|
});
|
|
1833
1831
|
await saves.idle();
|
|
1834
1832
|
for (const failure of saves.failures()) console.error(`probe: not saved: ${failure}`);
|