@ninhache/probe 0.7.0 → 0.8.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
CHANGED
|
@@ -67,6 +67,20 @@ report to stdout; exit code `1` names the services still down.
|
|
|
67
67
|
`probe add` alone, on a terminal, asks for the name, the port, the path and the expected status, one at a time:
|
|
68
68
|
Enter alone keeps the default, Ctrl+C adds nothing. Off a terminal it needs its arguments, as before.
|
|
69
69
|
|
|
70
|
+
On a terminal, `probe init` and `probe add` alone first show the services they find in the project, as a list to
|
|
71
|
+
tick. Nothing is ticked at first, and nothing is written unless you tick it. They look at:
|
|
72
|
+
|
|
73
|
+
- the compose file (`compose.yaml`, `compose.yml`, `docker-compose.yaml` or `docker-compose.yml`) and its
|
|
74
|
+
`override` file: the ports a service publishes (`"8080:80"`), under the name of the service. A variable in a
|
|
75
|
+
port takes its value from `.env`. Https, mail, database, cache and broker ports are left out.
|
|
76
|
+
- `.env` and `.env.local`: `PORT`, and the `*_URL` that point to `http://localhost`. Only the host, the port and
|
|
77
|
+
the path are kept: no other value is kept, shown or written.
|
|
78
|
+
- `package.json`: the port a script names (`--port 4000`), or the usual one of its dev server (vite, next, nuxt,
|
|
79
|
+
astro, angular).
|
|
80
|
+
- the local ports 3000, 3001, 4200, 4321, 5173, 8000 and 8080, when an HTTP server answers there (one `GET /`
|
|
81
|
+
each, redirects not followed).
|
|
82
|
+
`PROBE_DETECT_PORTS=4000,4001` tries other ports, `PROBE_DETECT_PORTS=` none.
|
|
83
|
+
|
|
70
84
|
`probe rm` is the one command that deletes something. On a terminal it asks first, with "No" selected: `--yes`
|
|
71
85
|
(`-y`) skips the question. Off a terminal, in a script or in CI, it removes at once, as before.
|
|
72
86
|
|
|
@@ -30,6 +30,9 @@ var REFRESH_COMMAND = "__refresh-update-cache";
|
|
|
30
30
|
var COMPLETE_COMMAND = "__complete";
|
|
31
31
|
var EXIT_DOWN = 1;
|
|
32
32
|
var EXIT_ERROR = 2;
|
|
33
|
+
var DETECT_PORTS = [3e3, 3001, 4200, 4321, 5173, 8e3, 8080];
|
|
34
|
+
var DETECT_PORTS_ENV = "PROBE_DETECT_PORTS";
|
|
35
|
+
var DETECT_TIMEOUT_MS = 300;
|
|
33
36
|
|
|
34
37
|
// src/errors.ts
|
|
35
38
|
var ProbeError = class extends Error {
|
|
@@ -359,14 +362,17 @@ export {
|
|
|
359
362
|
COMPLETE_COMMAND,
|
|
360
363
|
EXIT_DOWN,
|
|
361
364
|
EXIT_ERROR,
|
|
365
|
+
DETECT_PORTS,
|
|
366
|
+
DETECT_PORTS_ENV,
|
|
367
|
+
DETECT_TIMEOUT_MS,
|
|
362
368
|
ProbeError,
|
|
369
|
+
targetUrl,
|
|
370
|
+
curlCommand,
|
|
363
371
|
createColors,
|
|
364
372
|
detectTerminal,
|
|
365
373
|
isInteractive,
|
|
366
374
|
truncate,
|
|
367
375
|
stripStyles,
|
|
368
|
-
targetUrl,
|
|
369
|
-
curlCommand,
|
|
370
376
|
runChecks,
|
|
371
377
|
formatCheckResults,
|
|
372
378
|
formatServices
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
DEFAULT_WAIT_INTERVAL_SECONDS,
|
|
12
12
|
DEFAULT_WAIT_MAX_INTERVAL_SECONDS,
|
|
13
13
|
DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
14
|
+
DETECT_PORTS,
|
|
15
|
+
DETECT_PORTS_ENV,
|
|
16
|
+
DETECT_TIMEOUT_MS,
|
|
14
17
|
EXIT_DOWN,
|
|
15
18
|
EXIT_ERROR,
|
|
16
19
|
LOCAL_DRAFT_FILE,
|
|
@@ -37,7 +40,7 @@ import {
|
|
|
37
40
|
runChecks,
|
|
38
41
|
stripStyles,
|
|
39
42
|
targetUrl
|
|
40
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-Z4F2X5IS.js";
|
|
41
44
|
import "./chunk-M3ASEG5F.js";
|
|
42
45
|
|
|
43
46
|
// src/cli.ts
|
|
@@ -97,18 +100,311 @@ async function ask(message, question = {}, streams = {}) {
|
|
|
97
100
|
if (isCancel(answer)) return null;
|
|
98
101
|
return answer || (defaultValue ?? "");
|
|
99
102
|
}
|
|
103
|
+
async function choose(message, choices, streams = {}) {
|
|
104
|
+
const { multiselect, isCancel } = await import("./dist-SITQEC3X.js");
|
|
105
|
+
const options = choices.map((choice, value) => ({ value, ...choice }));
|
|
106
|
+
const picked = await multiselect({ message, options, required: false, ...streams });
|
|
107
|
+
return isCancel(picked) ? null : picked;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/detect/index.ts
|
|
111
|
+
import { readFile } from "fs/promises";
|
|
112
|
+
import { join } from "path";
|
|
113
|
+
|
|
114
|
+
// src/detect/sources/compose.ts
|
|
115
|
+
import { parse } from "yaml";
|
|
116
|
+
|
|
117
|
+
// src/detect/shared.ts
|
|
118
|
+
var DEFAULT_HOST = "localhost";
|
|
119
|
+
var DEFAULT_NAME = "app";
|
|
120
|
+
var NOT_HTTP_PORTS = [
|
|
121
|
+
25,
|
|
122
|
+
443,
|
|
123
|
+
465,
|
|
124
|
+
587,
|
|
125
|
+
1025,
|
|
126
|
+
1433,
|
|
127
|
+
2181,
|
|
128
|
+
3306,
|
|
129
|
+
5432,
|
|
130
|
+
5672,
|
|
131
|
+
6379,
|
|
132
|
+
8443,
|
|
133
|
+
9092,
|
|
134
|
+
11211,
|
|
135
|
+
27017
|
|
136
|
+
];
|
|
137
|
+
var isHttpPort = (port) => !NOT_HTTP_PORTS.includes(port);
|
|
138
|
+
var address = (service) => `${service.host ?? DEFAULT_HOST}:${service.port}`;
|
|
139
|
+
function serviceName(text) {
|
|
140
|
+
const name = text.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
141
|
+
return name || DEFAULT_NAME;
|
|
142
|
+
}
|
|
143
|
+
var ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
|
|
144
|
+
function unquoted(value) {
|
|
145
|
+
const quote = value[0];
|
|
146
|
+
const quoted2 = (quote === '"' || quote === "'") && value.endsWith(quote) && value.length > 1;
|
|
147
|
+
return quoted2 ? value.slice(1, -1) : value;
|
|
148
|
+
}
|
|
149
|
+
function parseEnvFile(text) {
|
|
150
|
+
const entries = [];
|
|
151
|
+
for (const line of text.split("\n")) {
|
|
152
|
+
const [, key, raw] = ENV_LINE.exec(line) ?? [];
|
|
153
|
+
if (key && raw !== void 0) entries.push([key, unquoted(raw)]);
|
|
154
|
+
}
|
|
155
|
+
return entries;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/detect/sources/compose.ts
|
|
159
|
+
var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
160
|
+
var OVERRIDE_FILES = [
|
|
161
|
+
"compose.override.yaml",
|
|
162
|
+
"compose.override.yml",
|
|
163
|
+
"docker-compose.override.yaml",
|
|
164
|
+
"docker-compose.override.yml"
|
|
165
|
+
];
|
|
166
|
+
var VARIABLES_FILE = ".env";
|
|
167
|
+
var VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?-)([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
168
|
+
var PORT = /^\d+$/;
|
|
169
|
+
var substituted = (text, variables) => text.replace(VARIABLE, (_match, braced, operator, fallback, bare) => {
|
|
170
|
+
const value = variables.get(braced ?? bare);
|
|
171
|
+
const missing = value === void 0 || operator === ":-" && value === "";
|
|
172
|
+
return missing ? fallback ?? "" : value;
|
|
173
|
+
});
|
|
174
|
+
function published(entry, variables) {
|
|
175
|
+
let host;
|
|
176
|
+
let container;
|
|
177
|
+
if (typeof entry === "string") {
|
|
178
|
+
const [mapping = "", protocol = "tcp"] = substituted(entry, variables).split("/");
|
|
179
|
+
if (protocol !== "tcp") return null;
|
|
180
|
+
const parts = mapping.split(":");
|
|
181
|
+
if (parts.length < 2) return null;
|
|
182
|
+
[host = "", container = ""] = parts.slice(-2);
|
|
183
|
+
} else if (entry && typeof entry === "object") {
|
|
184
|
+
const long = entry;
|
|
185
|
+
if ((long.protocol ?? "tcp") !== "tcp") return null;
|
|
186
|
+
host = substituted(String(long.published ?? ""), variables);
|
|
187
|
+
container = String(long.target ?? "");
|
|
188
|
+
} else {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!PORT.test(host) || !PORT.test(container)) return null;
|
|
192
|
+
return { host: Number(host), container: Number(container) };
|
|
193
|
+
}
|
|
194
|
+
function inFile(file, text, variables) {
|
|
195
|
+
let document;
|
|
196
|
+
try {
|
|
197
|
+
document = parse(text, { merge: true }) ?? {};
|
|
198
|
+
} catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
if (!document.services || typeof document.services !== "object") return [];
|
|
202
|
+
const found = [];
|
|
203
|
+
for (const [name, definition] of Object.entries(document.services)) {
|
|
204
|
+
const ports = definition?.ports;
|
|
205
|
+
if (!Array.isArray(ports)) continue;
|
|
206
|
+
const reachable = ports.map((entry) => published(entry, variables)).filter((port) => port !== null).filter((port) => isHttpPort(port.host) && isHttpPort(port.container));
|
|
207
|
+
reachable.forEach(({ host }, index) => {
|
|
208
|
+
found.push({
|
|
209
|
+
// A service that publishes several ports: the first keeps its name.
|
|
210
|
+
name: serviceName(index === 0 ? name : `${name}-${host}`),
|
|
211
|
+
service: { port: host },
|
|
212
|
+
source: `${file} (${name} service)`
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
218
|
+
var composeSource = {
|
|
219
|
+
id: "compose",
|
|
220
|
+
async find({ read }) {
|
|
221
|
+
const variables = new Map(parseEnvFile(await read(VARIABLES_FILE) ?? ""));
|
|
222
|
+
const found = [];
|
|
223
|
+
for (const names of [COMPOSE_FILES, OVERRIDE_FILES]) {
|
|
224
|
+
for (const file of names) {
|
|
225
|
+
const text = await read(file);
|
|
226
|
+
if (text === null) continue;
|
|
227
|
+
found.push(...inFile(file, text, variables));
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/detect/sources/env.ts
|
|
236
|
+
var ENV_FILES = [".env", ".env.local"];
|
|
237
|
+
var LOCAL_HOSTS = [DEFAULT_HOST, "127.0.0.1", "::1"];
|
|
238
|
+
var URL_KEY = /^(?:VITE_|NEXT_PUBLIC_|REACT_APP_|PUBLIC_)?(.*)_URL$/;
|
|
239
|
+
function serviceOfUrl(value) {
|
|
240
|
+
let url;
|
|
241
|
+
try {
|
|
242
|
+
url = new URL(value);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
247
|
+
if (url.protocol !== "http:" || !LOCAL_HOSTS.includes(host)) return null;
|
|
248
|
+
const service = { port: Number(url.port || 80) };
|
|
249
|
+
if (host !== DEFAULT_HOST) service.host = host;
|
|
250
|
+
if (url.pathname !== "/") service.path = url.pathname;
|
|
251
|
+
return service;
|
|
252
|
+
}
|
|
253
|
+
function inFile2(file, text) {
|
|
254
|
+
const found = [];
|
|
255
|
+
for (const [key, value] of parseEnvFile(text)) {
|
|
256
|
+
const source = `${file} (${key})`;
|
|
257
|
+
const urlName = URL_KEY.exec(key)?.[1];
|
|
258
|
+
if (key === "PORT" && /^\d+$/.test(value)) {
|
|
259
|
+
found.push({ name: DEFAULT_NAME, service: { port: Number(value) }, source });
|
|
260
|
+
} else if (urlName !== void 0) {
|
|
261
|
+
const service = serviceOfUrl(value);
|
|
262
|
+
if (service) found.push({ name: serviceName(urlName), service, source });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return found;
|
|
266
|
+
}
|
|
267
|
+
var envSource = {
|
|
268
|
+
id: "env",
|
|
269
|
+
async find({ read }) {
|
|
270
|
+
const found = [];
|
|
271
|
+
for (const file of ENV_FILES) {
|
|
272
|
+
const text = await read(file);
|
|
273
|
+
if (text !== null) found.push(...inFile2(file, text));
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// src/detect/sources/package-json.ts
|
|
280
|
+
var MANIFEST = "package.json";
|
|
281
|
+
var SCRIPT_PORT = /(?:--port[= ]|-p |\bPORT=)(\d{2,5})\b/;
|
|
282
|
+
var TOOL_PORTS = [
|
|
283
|
+
[/\bvite\b/, 5173],
|
|
284
|
+
[/\bnext\b/, 3e3],
|
|
285
|
+
[/\bnuxt\b/, 3e3],
|
|
286
|
+
[/\bastro\b/, 4321],
|
|
287
|
+
[/\bng serve\b/, 4200]
|
|
288
|
+
];
|
|
289
|
+
var MAIN_SCRIPTS = ["dev", "start", "serve"];
|
|
290
|
+
function inManifest(text) {
|
|
291
|
+
let manifest;
|
|
292
|
+
try {
|
|
293
|
+
manifest = JSON.parse(text);
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
if (!manifest.scripts || typeof manifest.scripts !== "object") return [];
|
|
298
|
+
const project = typeof manifest.name === "string" ? manifest.name.replace(/^@[^/]*\//, "") : "";
|
|
299
|
+
const found = [];
|
|
300
|
+
for (const [script, command] of Object.entries(manifest.scripts)) {
|
|
301
|
+
if (typeof command !== "string") continue;
|
|
302
|
+
const main2 = MAIN_SCRIPTS.includes(script);
|
|
303
|
+
const named = SCRIPT_PORT.exec(command)?.[1];
|
|
304
|
+
const usual = main2 ? TOOL_PORTS.find(([tool]) => tool.test(command))?.[1] : void 0;
|
|
305
|
+
const port = named ? Number(named) : usual;
|
|
306
|
+
if (port === void 0) continue;
|
|
307
|
+
const name = serviceName(main2 ? project : script);
|
|
308
|
+
found.push({ name, service: { port }, source: `${MANIFEST} (${script} script)` });
|
|
309
|
+
}
|
|
310
|
+
return found;
|
|
311
|
+
}
|
|
312
|
+
var packageJsonSource = {
|
|
313
|
+
id: "package-json",
|
|
314
|
+
async find({ read }) {
|
|
315
|
+
const text = await read(MANIFEST);
|
|
316
|
+
return text === null ? [] : inManifest(text);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/detect/sources/ports.ts
|
|
321
|
+
function detectPorts(env) {
|
|
322
|
+
const listed = env[DETECT_PORTS_ENV];
|
|
323
|
+
if (listed === void 0) return [...DETECT_PORTS];
|
|
324
|
+
return listed.split(",").map((port) => Number(port.trim())).filter((port) => Number.isInteger(port) && port > 0 && port <= 65535);
|
|
325
|
+
}
|
|
326
|
+
async function answersHttp(port) {
|
|
327
|
+
const url = targetUrl(serviceSchemaV1.parse({ port }));
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch(url, {
|
|
330
|
+
signal: AbortSignal.timeout(DETECT_TIMEOUT_MS),
|
|
331
|
+
redirect: "manual"
|
|
332
|
+
});
|
|
333
|
+
await response.body?.cancel();
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
var portsSource = {
|
|
340
|
+
id: "ports",
|
|
341
|
+
async find({ ports, answers, isKnown }) {
|
|
342
|
+
const tried = ports.filter((port) => !isKnown({ port }));
|
|
343
|
+
const open = await Promise.all(
|
|
344
|
+
tried.map(async (port) => await answers(port) ? port : null)
|
|
345
|
+
);
|
|
346
|
+
return open.filter((port) => port !== null).map(
|
|
347
|
+
(port) => ({
|
|
348
|
+
name: `port-${port}`,
|
|
349
|
+
service: { port },
|
|
350
|
+
source: `answers on ${DEFAULT_HOST}:${port}`
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/detect/registry.ts
|
|
357
|
+
var SOURCES = [
|
|
358
|
+
composeSource,
|
|
359
|
+
envSource,
|
|
360
|
+
packageJsonSource,
|
|
361
|
+
portsSource
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
// src/detect/index.ts
|
|
365
|
+
async function detectServices(root, existing, { read, sources, ports, answers }) {
|
|
366
|
+
const names = new Set(Object.keys(existing));
|
|
367
|
+
const addresses = new Set(Object.values(existing).map(address));
|
|
368
|
+
const kept = [];
|
|
369
|
+
const context = {
|
|
370
|
+
ports,
|
|
371
|
+
answers,
|
|
372
|
+
read: (file) => read(join(root, file)),
|
|
373
|
+
isKnown: (service) => addresses.has(address(service))
|
|
374
|
+
};
|
|
375
|
+
for (const source of sources) {
|
|
376
|
+
for (const { name, service, source: origin } of await source.find(context)) {
|
|
377
|
+
const valid = serviceSchemaV1.safeParse(service).success && SERVICE_NAME_PATTERN.test(name);
|
|
378
|
+
if (!valid || !isHttpPort(service.port) || context.isKnown(service)) continue;
|
|
379
|
+
let free = name;
|
|
380
|
+
for (let suffix = 2; names.has(free); suffix++) free = `${name}-${suffix}`;
|
|
381
|
+
names.add(free);
|
|
382
|
+
addresses.add(address(service));
|
|
383
|
+
kept.push({ name: free, service, source: origin });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return kept;
|
|
387
|
+
}
|
|
388
|
+
function systemDeps(env) {
|
|
389
|
+
return {
|
|
390
|
+
read: (path) => readFile(path, "utf8").catch(() => null),
|
|
391
|
+
sources: SOURCES,
|
|
392
|
+
ports: detectPorts(env),
|
|
393
|
+
answers: answersHttp
|
|
394
|
+
};
|
|
395
|
+
}
|
|
100
396
|
|
|
101
397
|
// src/config/discovery.ts
|
|
102
398
|
import { statSync } from "fs";
|
|
103
|
-
import { dirname, join, resolve } from "path";
|
|
399
|
+
import { dirname, join as join2, resolve } from "path";
|
|
104
400
|
function pathsFor(root) {
|
|
105
|
-
const dir =
|
|
401
|
+
const dir = join2(root, PROBE_DIR);
|
|
106
402
|
return {
|
|
107
403
|
root,
|
|
108
404
|
dir,
|
|
109
|
-
config:
|
|
110
|
-
lock:
|
|
111
|
-
local:
|
|
405
|
+
config: join2(dir, CONFIG_FILE),
|
|
406
|
+
lock: join2(dir, LOCK_FILE),
|
|
407
|
+
local: join2(dir, LOCAL_FILE)
|
|
112
408
|
};
|
|
113
409
|
}
|
|
114
410
|
function isDirectory(path) {
|
|
@@ -117,7 +413,7 @@ function isDirectory(path) {
|
|
|
117
413
|
function findProbeDir(startDir) {
|
|
118
414
|
let current = resolve(startDir);
|
|
119
415
|
while (true) {
|
|
120
|
-
if (isDirectory(
|
|
416
|
+
if (isDirectory(join2(current, PROBE_DIR))) return pathsFor(current);
|
|
121
417
|
const parent = dirname(current);
|
|
122
418
|
if (parent === current) return null;
|
|
123
419
|
current = parent;
|
|
@@ -135,8 +431,8 @@ function requireProbeDir(startDir) {
|
|
|
135
431
|
}
|
|
136
432
|
|
|
137
433
|
// src/config/loader.ts
|
|
138
|
-
import { copyFile, readFile, writeFile } from "fs/promises";
|
|
139
|
-
import { join as
|
|
434
|
+
import { copyFile, readFile as readFile2, writeFile } from "fs/promises";
|
|
435
|
+
import { join as join3, relative } from "path";
|
|
140
436
|
import { parseDocument } from "yaml";
|
|
141
437
|
|
|
142
438
|
// src/config/migrations.ts
|
|
@@ -245,7 +541,7 @@ function reconcileDocument(doc, value) {
|
|
|
245
541
|
async function readDocument(paths) {
|
|
246
542
|
let text;
|
|
247
543
|
try {
|
|
248
|
-
text = await
|
|
544
|
+
text = await readFile2(paths.config, "utf8");
|
|
249
545
|
} catch {
|
|
250
546
|
throw new ProbeError("CONFIG_MISSING", `cannot read ${paths.config}. Run \`probe init\`.`);
|
|
251
547
|
}
|
|
@@ -278,7 +574,7 @@ ${formatIssues(result.error)}`
|
|
|
278
574
|
return result.data;
|
|
279
575
|
}
|
|
280
576
|
function backupPath(paths, version) {
|
|
281
|
-
return
|
|
577
|
+
return join3(paths.dir, `config.v${version}.bak.yml`);
|
|
282
578
|
}
|
|
283
579
|
async function pendingMigration(paths, options = {}) {
|
|
284
580
|
const { raw } = await readDocument(paths);
|
|
@@ -317,12 +613,12 @@ async function writeServiceTimeout(paths, name, seconds2) {
|
|
|
317
613
|
}
|
|
318
614
|
|
|
319
615
|
// src/makefile/sync.ts
|
|
320
|
-
import { readFile as
|
|
321
|
-
import { join as
|
|
616
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
617
|
+
import { join as join4 } from "path";
|
|
322
618
|
|
|
323
619
|
// src/config/lock.ts
|
|
324
|
-
import { readFile as
|
|
325
|
-
import { parse, stringify } from "yaml";
|
|
620
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
621
|
+
import { parse as parse2, stringify } from "yaml";
|
|
326
622
|
import { z as z2 } from "zod";
|
|
327
623
|
var lockSchema = z2.object({
|
|
328
624
|
cliVersion: z2.string(),
|
|
@@ -331,7 +627,7 @@ var lockSchema = z2.object({
|
|
|
331
627
|
var HEADER = "# Generated by probe. Do not edit.\n";
|
|
332
628
|
async function readLock(paths) {
|
|
333
629
|
try {
|
|
334
|
-
const result = lockSchema.safeParse(
|
|
630
|
+
const result = lockSchema.safeParse(parse2(await readFile3(paths.lock, "utf8")));
|
|
335
631
|
return result.success ? result.data : null;
|
|
336
632
|
} catch {
|
|
337
633
|
return null;
|
|
@@ -342,7 +638,7 @@ async function writeLock(paths, lock) {
|
|
|
342
638
|
}
|
|
343
639
|
|
|
344
640
|
// src/version.ts
|
|
345
|
-
var CLI_VERSION = true ? "0.
|
|
641
|
+
var CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
346
642
|
|
|
347
643
|
// src/makefile/generator.ts
|
|
348
644
|
import { createHash } from "crypto";
|
|
@@ -403,11 +699,11 @@ function replaceBlock(makefile, block) {
|
|
|
403
699
|
|
|
404
700
|
// src/makefile/sync.ts
|
|
405
701
|
function makefilePath(paths) {
|
|
406
|
-
return
|
|
702
|
+
return join4(paths.root, DEFAULT_MAKEFILE);
|
|
407
703
|
}
|
|
408
704
|
async function readMakefile(paths) {
|
|
409
705
|
try {
|
|
410
|
-
return await
|
|
706
|
+
return await readFile4(makefilePath(paths), "utf8");
|
|
411
707
|
} catch (error) {
|
|
412
708
|
if (error.code === "ENOENT") return null;
|
|
413
709
|
throw error;
|
|
@@ -475,6 +771,28 @@ async function saveServices(loaded, services) {
|
|
|
475
771
|
);
|
|
476
772
|
}
|
|
477
773
|
|
|
774
|
+
// src/commands/detected.ts
|
|
775
|
+
async function offerDetected(loaded) {
|
|
776
|
+
const services = rawServices(loaded);
|
|
777
|
+
const candidates = await detectServices(loaded.paths.root, services, systemDeps(process.env));
|
|
778
|
+
if (candidates.length === 0) return [];
|
|
779
|
+
const urls = candidates.map(({ service }) => targetUrl(serviceSchemaV1.parse(service)));
|
|
780
|
+
const nameWidth = Math.max(...candidates.map(({ name }) => name.length));
|
|
781
|
+
const urlWidth = Math.max(...urls.map((url) => url.length));
|
|
782
|
+
const choices = candidates.map(({ name, source }, index) => ({
|
|
783
|
+
label: `${name.padEnd(nameWidth)} ${(urls[index] ?? "").padEnd(urlWidth)} ${source}`
|
|
784
|
+
}));
|
|
785
|
+
const picked = await choose("Services found here: which ones to add?", choices);
|
|
786
|
+
if (picked === null) return null;
|
|
787
|
+
const added = picked.map((index) => candidates[index]).filter((candidate) => !!candidate);
|
|
788
|
+
if (added.length === 0) return [];
|
|
789
|
+
const entries = Object.fromEntries(added.map(({ name, service }) => [name, service]));
|
|
790
|
+
await saveServices(loaded, { ...services, ...entries });
|
|
791
|
+
const names = added.map(({ name }) => name);
|
|
792
|
+
console.log(`Added ${names.join(", ")}. Try: probe check`);
|
|
793
|
+
return names;
|
|
794
|
+
}
|
|
795
|
+
|
|
478
796
|
// src/commands/add.ts
|
|
479
797
|
function parseInteger(value, flag) {
|
|
480
798
|
if (!/^\d+$/.test(value)) {
|
|
@@ -509,6 +827,12 @@ function expectProblem(expect) {
|
|
|
509
827
|
async function guidedAdd() {
|
|
510
828
|
const loaded = await openProject();
|
|
511
829
|
const services = rawServices(loaded);
|
|
830
|
+
const detected = await offerDetected(loaded);
|
|
831
|
+
if (detected === null) {
|
|
832
|
+
console.log("Nothing added.");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (detected.length > 0) return;
|
|
512
836
|
const answers = [];
|
|
513
837
|
const questions = [
|
|
514
838
|
{
|
|
@@ -584,8 +908,8 @@ var add_default = defineCommand({
|
|
|
584
908
|
import { defineCommand as defineCommand2 } from "citty";
|
|
585
909
|
|
|
586
910
|
// src/config/local.ts
|
|
587
|
-
import { chmod, readFile as
|
|
588
|
-
import { dirname as dirname2, join as
|
|
911
|
+
import { chmod, readFile as readFile5, realpath, rename, rm, stat, writeFile as writeFile4 } from "fs/promises";
|
|
912
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
589
913
|
import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
|
|
590
914
|
var SECTION = "dashboard";
|
|
591
915
|
var KEYS = ["interval", "timeout"];
|
|
@@ -596,7 +920,7 @@ function isDelay(value) {
|
|
|
596
920
|
}
|
|
597
921
|
async function readLocal(paths) {
|
|
598
922
|
try {
|
|
599
|
-
return await
|
|
923
|
+
return await readFile5(paths.local, "utf8");
|
|
600
924
|
} catch {
|
|
601
925
|
return null;
|
|
602
926
|
}
|
|
@@ -637,7 +961,7 @@ async function editLocal(paths, edit) {
|
|
|
637
961
|
const target = await realpath(paths.local).catch(() => paths.local);
|
|
638
962
|
const existing = await stat(target).catch(() => null);
|
|
639
963
|
const mode = existing ? existing.mode & 511 : LOCAL_FILE_MODE;
|
|
640
|
-
const draft =
|
|
964
|
+
const draft = join5(dirname2(target), LOCAL_DRAFT_FILE);
|
|
641
965
|
try {
|
|
642
966
|
await writeFile4(draft, text, { mode });
|
|
643
967
|
await chmod(draft, mode);
|
|
@@ -896,8 +1220,8 @@ var curl_default = defineCommand4({
|
|
|
896
1220
|
|
|
897
1221
|
// src/commands/init.ts
|
|
898
1222
|
import { existsSync } from "fs";
|
|
899
|
-
import { mkdir, readFile as
|
|
900
|
-
import { join as
|
|
1223
|
+
import { mkdir, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
1224
|
+
import { join as join6 } from "path";
|
|
901
1225
|
import { defineCommand as defineCommand5 } from "citty";
|
|
902
1226
|
var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
|
|
903
1227
|
var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
|
|
@@ -905,8 +1229,8 @@ schemaVersion: ${CURRENT_SCHEMA_VERSION}
|
|
|
905
1229
|
services: {}
|
|
906
1230
|
`;
|
|
907
1231
|
async function ignoreLocalFiles(root) {
|
|
908
|
-
const file =
|
|
909
|
-
const current = existsSync(file) ? await
|
|
1232
|
+
const file = join6(root, ".gitignore");
|
|
1233
|
+
const current = existsSync(file) ? await readFile6(file, "utf8") : "";
|
|
910
1234
|
const lines = current.split("\n").map((line) => line.trim());
|
|
911
1235
|
const missing = IGNORED.filter((entry) => !lines.includes(entry));
|
|
912
1236
|
if (missing.length === 0) return;
|
|
@@ -930,7 +1254,14 @@ var init_default = defineCommand5({
|
|
|
930
1254
|
},
|
|
931
1255
|
async run() {
|
|
932
1256
|
await initProject(process.cwd());
|
|
933
|
-
|
|
1257
|
+
const next = "Next: probe add api -p 3000";
|
|
1258
|
+
if (!isInteractive(process.stdin, process.stdout, process.env)) {
|
|
1259
|
+
console.log(`Created ${PROBE_DIR}/. ${next}`);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
console.log(`Created ${PROBE_DIR}/.`);
|
|
1263
|
+
const added = await offerDetected(await openProject());
|
|
1264
|
+
if (!added || added.length === 0) console.log(next);
|
|
934
1265
|
}
|
|
935
1266
|
});
|
|
936
1267
|
|
|
@@ -1032,16 +1363,16 @@ import { defineCommand as defineCommand10 } from "citty";
|
|
|
1032
1363
|
import { spawn } from "child_process";
|
|
1033
1364
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1034
1365
|
import { homedir } from "os";
|
|
1035
|
-
import { join as
|
|
1366
|
+
import { join as join7 } from "path";
|
|
1036
1367
|
function cacheDir(env = process.env) {
|
|
1037
|
-
return
|
|
1368
|
+
return join7(env.XDG_CACHE_HOME || join7(homedir(), ".cache"), "probe");
|
|
1038
1369
|
}
|
|
1039
1370
|
function updateCheckDisabled(env = process.env) {
|
|
1040
1371
|
return env.PROBE_NO_UPDATE_CHECK === "1" || Boolean(env.CI);
|
|
1041
1372
|
}
|
|
1042
1373
|
function readCache(dir) {
|
|
1043
1374
|
try {
|
|
1044
|
-
const cache = JSON.parse(readFileSync(
|
|
1375
|
+
const cache = JSON.parse(readFileSync(join7(dir, UPDATE_CACHE_FILE), "utf8"));
|
|
1045
1376
|
return typeof cache.checkedAt === "number" ? cache : null;
|
|
1046
1377
|
} catch {
|
|
1047
1378
|
return null;
|
|
@@ -1049,11 +1380,11 @@ function readCache(dir) {
|
|
|
1049
1380
|
}
|
|
1050
1381
|
function writeCache(dir, cache) {
|
|
1051
1382
|
mkdirSync(dir, { recursive: true });
|
|
1052
|
-
writeFileSync(
|
|
1383
|
+
writeFileSync(join7(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
|
|
1053
1384
|
}
|
|
1054
1385
|
function isNewer(latest, current) {
|
|
1055
|
-
const
|
|
1056
|
-
const [a, b] = [
|
|
1386
|
+
const parse4 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
|
|
1387
|
+
const [a, b] = [parse4(latest), parse4(current)];
|
|
1057
1388
|
for (let index = 0; index < 3; index++) {
|
|
1058
1389
|
const difference = (a[index] || 0) - (b[index] || 0);
|
|
1059
1390
|
if (difference !== 0) return difference > 0;
|
|
@@ -1273,13 +1604,13 @@ var wait_default = defineCommand12({
|
|
|
1273
1604
|
});
|
|
1274
1605
|
|
|
1275
1606
|
// src/completion/services.ts
|
|
1276
|
-
import { readFile as
|
|
1277
|
-
import { parse as
|
|
1607
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1608
|
+
import { parse as parse3 } from "yaml";
|
|
1278
1609
|
async function completableServices(cwd) {
|
|
1279
1610
|
try {
|
|
1280
1611
|
const paths = findProbeDir(cwd);
|
|
1281
1612
|
if (!paths) return [];
|
|
1282
|
-
const raw =
|
|
1613
|
+
const raw = parse3(await readFile7(paths.config, "utf8"));
|
|
1283
1614
|
const services = raw?.services;
|
|
1284
1615
|
if (typeof services !== "object" || services === null) return [];
|
|
1285
1616
|
return Object.keys(services).filter((name) => SERVICE_NAME_PATTERN.test(name));
|
|
@@ -1479,7 +1810,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1479
1810
|
if (!asked) return false;
|
|
1480
1811
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1481
1812
|
}
|
|
1482
|
-
const { openDashboard } = await import("./open-
|
|
1813
|
+
const { openDashboard } = await import("./open-2H7PHDYL.js");
|
|
1483
1814
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1484
1815
|
const saves = createSaveQueue();
|
|
1485
1816
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|