@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-YTTYVEYF.js";
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 = join(root, PROBE_DIR);
401
+ const dir = join2(root, PROBE_DIR);
106
402
  return {
107
403
  root,
108
404
  dir,
109
- config: join(dir, CONFIG_FILE),
110
- lock: join(dir, LOCK_FILE),
111
- local: join(dir, LOCAL_FILE)
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(join(current, PROBE_DIR))) return pathsFor(current);
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 join2, relative } from "path";
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 readFile(paths.config, "utf8");
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 join2(paths.dir, `config.v${version}.bak.yml`);
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 readFile3, writeFile as writeFile3 } from "fs/promises";
321
- import { join as join3 } from "path";
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 readFile2, writeFile as writeFile2 } from "fs/promises";
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(parse(await readFile2(paths.lock, "utf8")));
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.7.0" : "0.0.0-dev";
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 join3(paths.root, DEFAULT_MAKEFILE);
702
+ return join4(paths.root, DEFAULT_MAKEFILE);
407
703
  }
408
704
  async function readMakefile(paths) {
409
705
  try {
410
- return await readFile3(makefilePath(paths), "utf8");
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 readFile4, realpath, rename, rm, stat, writeFile as writeFile4 } from "fs/promises";
588
- import { dirname as dirname2, join as join4 } from "path";
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 readFile4(paths.local, "utf8");
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 = join4(dirname2(target), LOCAL_DRAFT_FILE);
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 readFile5, writeFile as writeFile5 } from "fs/promises";
900
- import { join as join5 } from "path";
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 = join5(root, ".gitignore");
909
- const current = existsSync(file) ? await readFile5(file, "utf8") : "";
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
- console.log(`Created ${PROBE_DIR}/. Next: probe add api -p 3000`);
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 join6 } from "path";
1366
+ import { join as join7 } from "path";
1036
1367
  function cacheDir(env = process.env) {
1037
- return join6(env.XDG_CACHE_HOME || join6(homedir(), ".cache"), "probe");
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(join6(dir, UPDATE_CACHE_FILE), "utf8"));
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(join6(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
1383
+ writeFileSync(join7(dir, UPDATE_CACHE_FILE), JSON.stringify(cache));
1053
1384
  }
1054
1385
  function isNewer(latest, current) {
1055
- const parse3 = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
1056
- const [a, b] = [parse3(latest), parse3(current)];
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 readFile6 } from "fs/promises";
1277
- import { parse as parse2 } from "yaml";
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 = parse2(await readFile6(paths.config, "utf8"));
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-T3OQH2O4.js");
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}`;
@@ -7,7 +7,7 @@ import {
7
7
  formatCheckResults,
8
8
  runChecks,
9
9
  truncate
10
- } from "./chunk-YTTYVEYF.js";
10
+ } from "./chunk-Z4F2X5IS.js";
11
11
  import {
12
12
  __commonJS,
13
13
  __export,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninhache/probe",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Health checks for your local dev services, from the CLI or a generated Makefile",
5
5
  "license": "MIT",
6
6
  "author": "Ninhache",