@ninhache/probe 0.6.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 +23 -2
- package/dist/{chunk-YTTYVEYF.js → chunk-Z4F2X5IS.js} +8 -2
- package/dist/cli.js +518 -71
- package/dist/dist-SITQEC3X.js +2894 -0
- package/dist/{open-T3OQH2O4.js → open-2H7PHDYL.js} +1 -1
- package/package.json +2 -1
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
|
|
@@ -51,17 +54,357 @@ import {
|
|
|
51
54
|
// src/commands/add.ts
|
|
52
55
|
import { defineCommand } from "citty";
|
|
53
56
|
|
|
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
|
+
// src/ui/prompt.ts
|
|
82
|
+
async function confirm(message, streams = {}) {
|
|
83
|
+
const { confirm: ask2, isCancel } = await import("./dist-SITQEC3X.js");
|
|
84
|
+
const answer = await ask2({ message, initialValue: false, ...streams });
|
|
85
|
+
return !isCancel(answer) && answer;
|
|
86
|
+
}
|
|
87
|
+
async function ask(message, question = {}, streams = {}) {
|
|
88
|
+
const { text, isCancel } = await import("./dist-SITQEC3X.js");
|
|
89
|
+
const { placeholder, defaultValue, validate: validate2 } = question;
|
|
90
|
+
const answer = await text({
|
|
91
|
+
message,
|
|
92
|
+
placeholder,
|
|
93
|
+
defaultValue,
|
|
94
|
+
validate: (typed) => {
|
|
95
|
+
const value = typed ?? "";
|
|
96
|
+
return value === "" && defaultValue !== void 0 ? void 0 : validate2?.(value);
|
|
97
|
+
},
|
|
98
|
+
...streams
|
|
99
|
+
});
|
|
100
|
+
if (isCancel(answer)) return null;
|
|
101
|
+
return answer || (defaultValue ?? "");
|
|
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
|
+
}
|
|
396
|
+
|
|
54
397
|
// src/config/discovery.ts
|
|
55
398
|
import { statSync } from "fs";
|
|
56
|
-
import { dirname, join, resolve } from "path";
|
|
399
|
+
import { dirname, join as join2, resolve } from "path";
|
|
57
400
|
function pathsFor(root) {
|
|
58
|
-
const dir =
|
|
401
|
+
const dir = join2(root, PROBE_DIR);
|
|
59
402
|
return {
|
|
60
403
|
root,
|
|
61
404
|
dir,
|
|
62
|
-
config:
|
|
63
|
-
lock:
|
|
64
|
-
local:
|
|
405
|
+
config: join2(dir, CONFIG_FILE),
|
|
406
|
+
lock: join2(dir, LOCK_FILE),
|
|
407
|
+
local: join2(dir, LOCAL_FILE)
|
|
65
408
|
};
|
|
66
409
|
}
|
|
67
410
|
function isDirectory(path) {
|
|
@@ -70,7 +413,7 @@ function isDirectory(path) {
|
|
|
70
413
|
function findProbeDir(startDir) {
|
|
71
414
|
let current = resolve(startDir);
|
|
72
415
|
while (true) {
|
|
73
|
-
if (isDirectory(
|
|
416
|
+
if (isDirectory(join2(current, PROBE_DIR))) return pathsFor(current);
|
|
74
417
|
const parent = dirname(current);
|
|
75
418
|
if (parent === current) return null;
|
|
76
419
|
current = parent;
|
|
@@ -88,34 +431,10 @@ function requireProbeDir(startDir) {
|
|
|
88
431
|
}
|
|
89
432
|
|
|
90
433
|
// src/config/loader.ts
|
|
91
|
-
import { copyFile, readFile, writeFile } from "fs/promises";
|
|
92
|
-
import { join as
|
|
434
|
+
import { copyFile, readFile as readFile2, writeFile } from "fs/promises";
|
|
435
|
+
import { join as join3, relative } from "path";
|
|
93
436
|
import { parseDocument } from "yaml";
|
|
94
437
|
|
|
95
|
-
// src/config/schema.ts
|
|
96
|
-
import { z } from "zod";
|
|
97
|
-
var CURRENT_SCHEMA_VERSION = 1;
|
|
98
|
-
var SERVICE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
99
|
-
var statusCode = z.number().int().min(100).max(599);
|
|
100
|
-
var serviceSchemaV1 = z.strictObject({
|
|
101
|
-
host: z.string().min(1).default("localhost"),
|
|
102
|
-
port: z.number().int().min(1).max(65535),
|
|
103
|
-
path: z.string().startsWith("/").default("/"),
|
|
104
|
-
expect: z.union([statusCode, z.array(statusCode).min(1)]).optional(),
|
|
105
|
-
/** Seconds given to this service to answer, instead of the default. */
|
|
106
|
-
timeout: z.number().positive().max(MAX_TIMEOUT_SECONDS).optional()
|
|
107
|
-
});
|
|
108
|
-
var configSchemaV1 = z.strictObject({
|
|
109
|
-
schemaVersion: z.literal(1),
|
|
110
|
-
services: z.record(
|
|
111
|
-
z.string().regex(SERVICE_NAME_PATTERN, "letters, digits, _ and - only"),
|
|
112
|
-
serviceSchemaV1
|
|
113
|
-
).nullish().transform((services) => services ?? {})
|
|
114
|
-
});
|
|
115
|
-
var schemas = {
|
|
116
|
-
1: configSchemaV1
|
|
117
|
-
};
|
|
118
|
-
|
|
119
438
|
// src/config/migrations.ts
|
|
120
439
|
var migrations = {};
|
|
121
440
|
function formatIssues(error) {
|
|
@@ -222,7 +541,7 @@ function reconcileDocument(doc, value) {
|
|
|
222
541
|
async function readDocument(paths) {
|
|
223
542
|
let text;
|
|
224
543
|
try {
|
|
225
|
-
text = await
|
|
544
|
+
text = await readFile2(paths.config, "utf8");
|
|
226
545
|
} catch {
|
|
227
546
|
throw new ProbeError("CONFIG_MISSING", `cannot read ${paths.config}. Run \`probe init\`.`);
|
|
228
547
|
}
|
|
@@ -255,7 +574,7 @@ ${formatIssues(result.error)}`
|
|
|
255
574
|
return result.data;
|
|
256
575
|
}
|
|
257
576
|
function backupPath(paths, version) {
|
|
258
|
-
return
|
|
577
|
+
return join3(paths.dir, `config.v${version}.bak.yml`);
|
|
259
578
|
}
|
|
260
579
|
async function pendingMigration(paths, options = {}) {
|
|
261
580
|
const { raw } = await readDocument(paths);
|
|
@@ -294,12 +613,12 @@ async function writeServiceTimeout(paths, name, seconds2) {
|
|
|
294
613
|
}
|
|
295
614
|
|
|
296
615
|
// src/makefile/sync.ts
|
|
297
|
-
import { readFile as
|
|
298
|
-
import { join as
|
|
616
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
617
|
+
import { join as join4 } from "path";
|
|
299
618
|
|
|
300
619
|
// src/config/lock.ts
|
|
301
|
-
import { readFile as
|
|
302
|
-
import { parse, stringify } from "yaml";
|
|
620
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
621
|
+
import { parse as parse2, stringify } from "yaml";
|
|
303
622
|
import { z as z2 } from "zod";
|
|
304
623
|
var lockSchema = z2.object({
|
|
305
624
|
cliVersion: z2.string(),
|
|
@@ -308,7 +627,7 @@ var lockSchema = z2.object({
|
|
|
308
627
|
var HEADER = "# Generated by probe. Do not edit.\n";
|
|
309
628
|
async function readLock(paths) {
|
|
310
629
|
try {
|
|
311
|
-
const result = lockSchema.safeParse(
|
|
630
|
+
const result = lockSchema.safeParse(parse2(await readFile3(paths.lock, "utf8")));
|
|
312
631
|
return result.success ? result.data : null;
|
|
313
632
|
} catch {
|
|
314
633
|
return null;
|
|
@@ -319,7 +638,7 @@ async function writeLock(paths, lock) {
|
|
|
319
638
|
}
|
|
320
639
|
|
|
321
640
|
// src/version.ts
|
|
322
|
-
var CLI_VERSION = true ? "0.
|
|
641
|
+
var CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
323
642
|
|
|
324
643
|
// src/makefile/generator.ts
|
|
325
644
|
import { createHash } from "crypto";
|
|
@@ -380,11 +699,11 @@ function replaceBlock(makefile, block) {
|
|
|
380
699
|
|
|
381
700
|
// src/makefile/sync.ts
|
|
382
701
|
function makefilePath(paths) {
|
|
383
|
-
return
|
|
702
|
+
return join4(paths.root, DEFAULT_MAKEFILE);
|
|
384
703
|
}
|
|
385
704
|
async function readMakefile(paths) {
|
|
386
705
|
try {
|
|
387
|
-
return await
|
|
706
|
+
return await readFile4(makefilePath(paths), "utf8");
|
|
388
707
|
} catch (error) {
|
|
389
708
|
if (error.code === "ENOENT") return null;
|
|
390
709
|
throw error;
|
|
@@ -452,6 +771,28 @@ async function saveServices(loaded, services) {
|
|
|
452
771
|
);
|
|
453
772
|
}
|
|
454
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
|
+
|
|
455
796
|
// src/commands/add.ts
|
|
456
797
|
function parseInteger(value, flag) {
|
|
457
798
|
if (!/^\d+$/.test(value)) {
|
|
@@ -463,6 +804,71 @@ function parseExpect(value) {
|
|
|
463
804
|
const codes = value.split(",").map((code) => parseInteger(code.trim(), "--expect"));
|
|
464
805
|
return codes.length === 1 ? codes[0] : codes;
|
|
465
806
|
}
|
|
807
|
+
var INTEGER = /^\d+$/;
|
|
808
|
+
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
|
+
async function guidedAdd() {
|
|
828
|
+
const loaded = await openProject();
|
|
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;
|
|
836
|
+
const answers = [];
|
|
837
|
+
const questions = [
|
|
838
|
+
{
|
|
839
|
+
message: "Service name",
|
|
840
|
+
placeholder: "api",
|
|
841
|
+
validate: (v) => nameProblem(v, services)
|
|
842
|
+
},
|
|
843
|
+
{ message: "Port", placeholder: "3000", validate: portProblem },
|
|
844
|
+
{
|
|
845
|
+
message: "Path to request",
|
|
846
|
+
placeholder: DEFAULT_PATH,
|
|
847
|
+
defaultValue: DEFAULT_PATH,
|
|
848
|
+
validate: (v) => schemaProblem(serviceSchemaV1.shape.path, v)
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
message: "Expected status",
|
|
852
|
+
placeholder: "any 2xx",
|
|
853
|
+
defaultValue: "",
|
|
854
|
+
validate: expectProblem
|
|
855
|
+
}
|
|
856
|
+
];
|
|
857
|
+
for (const { message, ...question } of questions) {
|
|
858
|
+
const answer = await ask(message, question);
|
|
859
|
+
if (answer === null) {
|
|
860
|
+
console.log("Nothing added.");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
answers.push(answer);
|
|
864
|
+
}
|
|
865
|
+
const [name, port, path, expect] = answers;
|
|
866
|
+
const service = { port: Number(port) };
|
|
867
|
+
if (path !== DEFAULT_PATH) service.path = path;
|
|
868
|
+
if (expect) service.expect = parseExpect(expect);
|
|
869
|
+
await saveServices(loaded, { ...services, [name]: service });
|
|
870
|
+
console.log(`Added ${name}. Try: probe check ${name}`);
|
|
871
|
+
}
|
|
466
872
|
var add_default = defineCommand({
|
|
467
873
|
meta: { name: "add", description: "Add a service" },
|
|
468
874
|
args: {
|
|
@@ -502,8 +908,8 @@ var add_default = defineCommand({
|
|
|
502
908
|
import { defineCommand as defineCommand2 } from "citty";
|
|
503
909
|
|
|
504
910
|
// src/config/local.ts
|
|
505
|
-
import { chmod, readFile as
|
|
506
|
-
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";
|
|
507
913
|
import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
|
|
508
914
|
var SECTION = "dashboard";
|
|
509
915
|
var KEYS = ["interval", "timeout"];
|
|
@@ -514,7 +920,7 @@ function isDelay(value) {
|
|
|
514
920
|
}
|
|
515
921
|
async function readLocal(paths) {
|
|
516
922
|
try {
|
|
517
|
-
return await
|
|
923
|
+
return await readFile5(paths.local, "utf8");
|
|
518
924
|
} catch {
|
|
519
925
|
return null;
|
|
520
926
|
}
|
|
@@ -555,7 +961,7 @@ async function editLocal(paths, edit) {
|
|
|
555
961
|
const target = await realpath(paths.local).catch(() => paths.local);
|
|
556
962
|
const existing = await stat(target).catch(() => null);
|
|
557
963
|
const mode = existing ? existing.mode & 511 : LOCAL_FILE_MODE;
|
|
558
|
-
const draft =
|
|
964
|
+
const draft = join5(dirname2(target), LOCAL_DRAFT_FILE);
|
|
559
965
|
try {
|
|
560
966
|
await writeFile4(draft, text, { mode });
|
|
561
967
|
await chmod(draft, mode);
|
|
@@ -814,8 +1220,8 @@ var curl_default = defineCommand4({
|
|
|
814
1220
|
|
|
815
1221
|
// src/commands/init.ts
|
|
816
1222
|
import { existsSync } from "fs";
|
|
817
|
-
import { mkdir, readFile as
|
|
818
|
-
import { join as
|
|
1223
|
+
import { mkdir, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
1224
|
+
import { join as join6 } from "path";
|
|
819
1225
|
import { defineCommand as defineCommand5 } from "citty";
|
|
820
1226
|
var IGNORED = [`${PROBE_DIR}/${LOCAL_FILE}`, `${PROBE_DIR}/${BACKUP_GLOB}`];
|
|
821
1227
|
var INITIAL_CONFIG = `# probe - local services to health-check. Edit by hand or with \`probe add\`.
|
|
@@ -823,8 +1229,8 @@ schemaVersion: ${CURRENT_SCHEMA_VERSION}
|
|
|
823
1229
|
services: {}
|
|
824
1230
|
`;
|
|
825
1231
|
async function ignoreLocalFiles(root) {
|
|
826
|
-
const file =
|
|
827
|
-
const current = existsSync(file) ? await
|
|
1232
|
+
const file = join6(root, ".gitignore");
|
|
1233
|
+
const current = existsSync(file) ? await readFile6(file, "utf8") : "";
|
|
828
1234
|
const lines = current.split("\n").map((line) => line.trim());
|
|
829
1235
|
const missing = IGNORED.filter((entry) => !lines.includes(entry));
|
|
830
1236
|
if (missing.length === 0) return;
|
|
@@ -832,20 +1238,30 @@ async function ignoreLocalFiles(root) {
|
|
|
832
1238
|
await writeFile5(file, `${current}${separator}${missing.join("\n")}
|
|
833
1239
|
`);
|
|
834
1240
|
}
|
|
1241
|
+
async function initProject(root) {
|
|
1242
|
+
const paths = pathsFor(root);
|
|
1243
|
+
if (existsSync(paths.dir)) {
|
|
1244
|
+
throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
|
|
1245
|
+
}
|
|
1246
|
+
await mkdir(paths.dir);
|
|
1247
|
+
await writeFile5(paths.config, INITIAL_CONFIG);
|
|
1248
|
+
await ignoreLocalFiles(paths.root);
|
|
1249
|
+
}
|
|
835
1250
|
var init_default = defineCommand5({
|
|
836
1251
|
meta: {
|
|
837
1252
|
name: "init",
|
|
838
1253
|
description: "Create .probe/ and the .gitignore entries"
|
|
839
1254
|
},
|
|
840
1255
|
async run() {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1256
|
+
await initProject(process.cwd());
|
|
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;
|
|
844
1261
|
}
|
|
845
|
-
|
|
846
|
-
await
|
|
847
|
-
|
|
848
|
-
console.log(`Created ${PROBE_DIR}/. Next: probe add api -p 3000`);
|
|
1262
|
+
console.log(`Created ${PROBE_DIR}/.`);
|
|
1263
|
+
const added = await offerDetected(await openProject());
|
|
1264
|
+
if (!added || added.length === 0) console.log(next);
|
|
849
1265
|
}
|
|
850
1266
|
});
|
|
851
1267
|
|
|
@@ -920,7 +1336,8 @@ import { defineCommand as defineCommand9 } from "citty";
|
|
|
920
1336
|
var rm_default = defineCommand9({
|
|
921
1337
|
meta: { name: "rm", description: "Remove a service" },
|
|
922
1338
|
args: {
|
|
923
|
-
name: { type: "positional", description: "Service name", required: true }
|
|
1339
|
+
name: { type: "positional", description: "Service name", required: true },
|
|
1340
|
+
yes: { type: "boolean", alias: "y", description: "Remove without asking" }
|
|
924
1341
|
},
|
|
925
1342
|
async run({ args: args2 }) {
|
|
926
1343
|
const loaded = await openProject();
|
|
@@ -928,6 +1345,11 @@ var rm_default = defineCommand9({
|
|
|
928
1345
|
if (!Object.hasOwn(services, args2.name)) {
|
|
929
1346
|
throw new ProbeError("SERVICE_UNKNOWN", `unknown service "${args2.name}". See: probe ls`);
|
|
930
1347
|
}
|
|
1348
|
+
const asks = !args2.yes && isInteractive(process.stdin, process.stdout, process.env);
|
|
1349
|
+
if (asks && !await confirm(`Remove ${args2.name}?`)) {
|
|
1350
|
+
console.log("Nothing removed.");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
931
1353
|
const { [args2.name]: _removed, ...rest } = services;
|
|
932
1354
|
await saveServices(loaded, rest);
|
|
933
1355
|
console.log(`Removed ${args2.name}.`);
|
|
@@ -941,16 +1363,16 @@ import { defineCommand as defineCommand10 } from "citty";
|
|
|
941
1363
|
import { spawn } from "child_process";
|
|
942
1364
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
943
1365
|
import { homedir } from "os";
|
|
944
|
-
import { join as
|
|
1366
|
+
import { join as join7 } from "path";
|
|
945
1367
|
function cacheDir(env = process.env) {
|
|
946
|
-
return
|
|
1368
|
+
return join7(env.XDG_CACHE_HOME || join7(homedir(), ".cache"), "probe");
|
|
947
1369
|
}
|
|
948
1370
|
function updateCheckDisabled(env = process.env) {
|
|
949
1371
|
return env.PROBE_NO_UPDATE_CHECK === "1" || Boolean(env.CI);
|
|
950
1372
|
}
|
|
951
1373
|
function readCache(dir) {
|
|
952
1374
|
try {
|
|
953
|
-
const cache = JSON.parse(readFileSync(
|
|
1375
|
+
const cache = JSON.parse(readFileSync(join7(dir, UPDATE_CACHE_FILE), "utf8"));
|
|
954
1376
|
return typeof cache.checkedAt === "number" ? cache : null;
|
|
955
1377
|
} catch {
|
|
956
1378
|
return null;
|
|
@@ -958,11 +1380,11 @@ function readCache(dir) {
|
|
|
958
1380
|
}
|
|
959
1381
|
function writeCache(dir, cache) {
|
|
960
1382
|
mkdirSync(dir, { recursive: true });
|
|
961
|
-
writeFileSync(
|
|
1383
|
+
writeFileSync(join7(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
|
|
962
1384
|
}
|
|
963
1385
|
function isNewer(latest, current) {
|
|
964
|
-
const
|
|
965
|
-
const [a, b] = [
|
|
1386
|
+
const parse4 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
|
|
1387
|
+
const [a, b] = [parse4(latest), parse4(current)];
|
|
966
1388
|
for (let index = 0; index < 3; index++) {
|
|
967
1389
|
const difference = (a[index] || 0) - (b[index] || 0);
|
|
968
1390
|
if (difference !== 0) return difference > 0;
|
|
@@ -1182,13 +1604,13 @@ var wait_default = defineCommand12({
|
|
|
1182
1604
|
});
|
|
1183
1605
|
|
|
1184
1606
|
// src/completion/services.ts
|
|
1185
|
-
import { readFile as
|
|
1186
|
-
import { parse as
|
|
1607
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1608
|
+
import { parse as parse3 } from "yaml";
|
|
1187
1609
|
async function completableServices(cwd) {
|
|
1188
1610
|
try {
|
|
1189
1611
|
const paths = findProbeDir(cwd);
|
|
1190
1612
|
if (!paths) return [];
|
|
1191
|
-
const raw =
|
|
1613
|
+
const raw = parse3(await readFile7(paths.config, "utf8"));
|
|
1192
1614
|
const services = raw?.services;
|
|
1193
1615
|
if (typeof services !== "object" || services === null) return [];
|
|
1194
1616
|
return Object.keys(services).filter((name) => SERVICE_NAME_PATTERN.test(name));
|
|
@@ -1279,7 +1701,10 @@ var EXAMPLES = {
|
|
|
1279
1701
|
{ command: "probe add admin -p 8080 --expect 200,204", note: "Accept these statuses only" },
|
|
1280
1702
|
{ command: "probe add db -p 5432 --timeout 10", note: "Give it 10 s to answer" }
|
|
1281
1703
|
],
|
|
1282
|
-
rm: [
|
|
1704
|
+
rm: [
|
|
1705
|
+
{ command: "probe rm api", note: "Remove the service named api" },
|
|
1706
|
+
{ command: "probe rm api --yes", note: "Remove it without the question" }
|
|
1707
|
+
],
|
|
1283
1708
|
ls: [{ command: "probe ls", note: "List the services and their URL" }],
|
|
1284
1709
|
check: [
|
|
1285
1710
|
{ command: "probe check", note: "Check every service" },
|
|
@@ -1385,7 +1810,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1385
1810
|
if (!asked) return false;
|
|
1386
1811
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1387
1812
|
}
|
|
1388
|
-
const { openDashboard } = await import("./open-
|
|
1813
|
+
const { openDashboard } = await import("./open-2H7PHDYL.js");
|
|
1389
1814
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1390
1815
|
const saves = createSaveQueue();
|
|
1391
1816
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|
|
@@ -1409,6 +1834,23 @@ async function openedDashboard(rawArgs) {
|
|
|
1409
1834
|
for (const failure of saves.failures()) console.error(`probe: not saved: ${failure}`);
|
|
1410
1835
|
return true;
|
|
1411
1836
|
}
|
|
1837
|
+
async function offeredInit(error, rawArgs) {
|
|
1838
|
+
if (!(error instanceof ProbeError) || error.code !== "NO_PROBE_DIR") return false;
|
|
1839
|
+
if (rawArgs.includes("--json")) return false;
|
|
1840
|
+
if (!isInteractive(process.stdin, process.stdout, process.env)) return false;
|
|
1841
|
+
const root = process.cwd();
|
|
1842
|
+
if (!await confirm(`No ${PROBE_DIR}/ here. Create it in ${root}?`)) return false;
|
|
1843
|
+
await initProject(root);
|
|
1844
|
+
console.log(`Created ${PROBE_DIR}/.`);
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
function wantsGuidedAdd(rawArgs) {
|
|
1848
|
+
return rawArgs.length === 1 && rawArgs[0] === "add" && isInteractive(process.stdin, process.stdout, process.env);
|
|
1849
|
+
}
|
|
1850
|
+
async function dispatch(rawArgs) {
|
|
1851
|
+
if (wantsGuidedAdd(rawArgs)) return guidedAdd();
|
|
1852
|
+
if (!await openedDashboard(rawArgs)) await runCommand(main, { rawArgs });
|
|
1853
|
+
}
|
|
1412
1854
|
var UNKNOWN_COMMAND = "E_UNKNOWN_COMMAND";
|
|
1413
1855
|
function didYouMean(error, rawArgs) {
|
|
1414
1856
|
if (error.code !== UNKNOWN_COMMAND) return "";
|
|
@@ -1432,7 +1874,12 @@ async function run(rawArgs) {
|
|
|
1432
1874
|
const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
|
|
1433
1875
|
if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
|
|
1434
1876
|
try {
|
|
1435
|
-
|
|
1877
|
+
try {
|
|
1878
|
+
await dispatch(rawArgs);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (!await offeredInit(error, rawArgs)) throw error;
|
|
1881
|
+
await dispatch(rawArgs);
|
|
1882
|
+
}
|
|
1436
1883
|
} catch (error) {
|
|
1437
1884
|
process.exitCode = EXIT_ERROR;
|
|
1438
1885
|
if (error instanceof ProbeError) {
|