@ninhache/probe 0.6.0 → 0.7.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
@@ -41,7 +41,7 @@ Want `make check` too? See [Makefile](#makefile): it is optional.
41
41
  | --------------------------------------------------------- | ---------------------------------------------------------------------- |
42
42
  | `probe init` | Create `.probe/` and the `.gitignore` entries |
43
43
  | `probe add <name> -p <port> [--host] [--path] [--expect]` | Add a service (`--timeout <seconds>` gives it its own timeout) |
44
- | `probe rm <name>` | Remove a service |
44
+ | `probe rm <name> [--yes]` | Remove a service |
45
45
  | `probe ls` | List services |
46
46
  | `probe check [name...] [--json] [--timeout <seconds>]` | Check all services, or the named ones |
47
47
  | `probe wait [name...] [--json] [--timeout <seconds>]` | Check again until every service is up, or give up after the timeout |
@@ -64,10 +64,17 @@ longest one. Unlike `check --timeout`, which is the time given to each service,
64
64
  wait; each check inside it still gets 3 s, or the service's own `timeout`. Progress goes to stderr, the final
65
65
  report to stdout; exit code `1` names the services still down.
66
66
 
67
+ `probe add` alone, on a terminal, asks for the name, the port, the path and the expected status, one at a time:
68
+ Enter alone keeps the default, Ctrl+C adds nothing. Off a terminal it needs its arguments, as before.
69
+
70
+ `probe rm` is the one command that deletes something. On a terminal it asks first, with "No" selected: `--yes`
71
+ (`-y`) skips the question. Off a terminal, in a script or in CI, it removes at once, as before.
72
+
67
73
  `probe open` uses `open` on macOS and `xdg-open` elsewhere. Without it, the URL is printed and the exit code is
68
74
  `1`. `probe curl api` prints a command to copy, or to run: `probe curl api | sh`.
69
75
 
70
- `probe` works from any sub-directory: it walks up to the nearest `.probe/`.
76
+ `probe` works from any sub-directory: it walks up to the nearest `.probe/`. When there is none, on a terminal,
77
+ it offers to create it where you are, then runs your command. Off a terminal it stops with exit code `2`.
71
78
 
72
79
  ## Completion
73
80
 
package/dist/cli.js CHANGED
@@ -51,6 +51,53 @@ import {
51
51
  // src/commands/add.ts
52
52
  import { defineCommand } from "citty";
53
53
 
54
+ // src/config/schema.ts
55
+ import { z } from "zod";
56
+ var CURRENT_SCHEMA_VERSION = 1;
57
+ var SERVICE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
58
+ var statusCode = z.number().int().min(100).max(599);
59
+ var serviceSchemaV1 = z.strictObject({
60
+ host: z.string().min(1).default("localhost"),
61
+ port: z.number().int().min(1).max(65535),
62
+ path: z.string().startsWith("/").default("/"),
63
+ expect: z.union([statusCode, z.array(statusCode).min(1)]).optional(),
64
+ /** Seconds given to this service to answer, instead of the default. */
65
+ timeout: z.number().positive().max(MAX_TIMEOUT_SECONDS).optional()
66
+ });
67
+ var configSchemaV1 = z.strictObject({
68
+ schemaVersion: z.literal(1),
69
+ services: z.record(
70
+ z.string().regex(SERVICE_NAME_PATTERN, "letters, digits, _ and - only"),
71
+ serviceSchemaV1
72
+ ).nullish().transform((services) => services ?? {})
73
+ });
74
+ var schemas = {
75
+ 1: configSchemaV1
76
+ };
77
+
78
+ // src/ui/prompt.ts
79
+ async function confirm(message, streams = {}) {
80
+ const { confirm: ask2, isCancel } = await import("./dist-SITQEC3X.js");
81
+ const answer = await ask2({ message, initialValue: false, ...streams });
82
+ return !isCancel(answer) && answer;
83
+ }
84
+ async function ask(message, question = {}, streams = {}) {
85
+ const { text, isCancel } = await import("./dist-SITQEC3X.js");
86
+ const { placeholder, defaultValue, validate: validate2 } = question;
87
+ const answer = await text({
88
+ message,
89
+ placeholder,
90
+ defaultValue,
91
+ validate: (typed) => {
92
+ const value = typed ?? "";
93
+ return value === "" && defaultValue !== void 0 ? void 0 : validate2?.(value);
94
+ },
95
+ ...streams
96
+ });
97
+ if (isCancel(answer)) return null;
98
+ return answer || (defaultValue ?? "");
99
+ }
100
+
54
101
  // src/config/discovery.ts
55
102
  import { statSync } from "fs";
56
103
  import { dirname, join, resolve } from "path";
@@ -92,30 +139,6 @@ import { copyFile, readFile, writeFile } from "fs/promises";
92
139
  import { join as join2, relative } from "path";
93
140
  import { parseDocument } from "yaml";
94
141
 
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
142
  // src/config/migrations.ts
120
143
  var migrations = {};
121
144
  function formatIssues(error) {
@@ -319,7 +342,7 @@ async function writeLock(paths, lock) {
319
342
  }
320
343
 
321
344
  // src/version.ts
322
- var CLI_VERSION = true ? "0.6.0" : "0.0.0-dev";
345
+ var CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
323
346
 
324
347
  // src/makefile/generator.ts
325
348
  import { createHash } from "crypto";
@@ -463,6 +486,65 @@ function parseExpect(value) {
463
486
  const codes = value.split(",").map((code) => parseInteger(code.trim(), "--expect"));
464
487
  return codes.length === 1 ? codes[0] : codes;
465
488
  }
489
+ var INTEGER = /^\d+$/;
490
+ var DEFAULT_PATH = "/";
491
+ function schemaProblem(schema, value) {
492
+ const result = schema.safeParse(value);
493
+ return result.success ? void 0 : result.error.issues[0]?.message;
494
+ }
495
+ function nameProblem(name, services) {
496
+ if (!SERVICE_NAME_PATTERN.test(name)) return "Letters, digits, _ and - only, e.g. api";
497
+ if (Object.hasOwn(services, name)) return `${name} already exists`;
498
+ return void 0;
499
+ }
500
+ function portProblem(port) {
501
+ if (!INTEGER.test(port)) return "A port number, e.g. 3000";
502
+ return schemaProblem(serviceSchemaV1.shape.port, Number(port)) && "A port between 1 and 65535";
503
+ }
504
+ function expectProblem(expect) {
505
+ const codes = expect.split(",").map((code) => code.trim());
506
+ if (!codes.every((code) => INTEGER.test(code))) return "A status, or several: 200 or 200,204";
507
+ return schemaProblem(serviceSchemaV1.shape.expect, parseExpect(expect)) && "Statuses go from 100 to 599";
508
+ }
509
+ async function guidedAdd() {
510
+ const loaded = await openProject();
511
+ const services = rawServices(loaded);
512
+ const answers = [];
513
+ const questions = [
514
+ {
515
+ message: "Service name",
516
+ placeholder: "api",
517
+ validate: (v) => nameProblem(v, services)
518
+ },
519
+ { message: "Port", placeholder: "3000", validate: portProblem },
520
+ {
521
+ message: "Path to request",
522
+ placeholder: DEFAULT_PATH,
523
+ defaultValue: DEFAULT_PATH,
524
+ validate: (v) => schemaProblem(serviceSchemaV1.shape.path, v)
525
+ },
526
+ {
527
+ message: "Expected status",
528
+ placeholder: "any 2xx",
529
+ defaultValue: "",
530
+ validate: expectProblem
531
+ }
532
+ ];
533
+ for (const { message, ...question } of questions) {
534
+ const answer = await ask(message, question);
535
+ if (answer === null) {
536
+ console.log("Nothing added.");
537
+ return;
538
+ }
539
+ answers.push(answer);
540
+ }
541
+ const [name, port, path, expect] = answers;
542
+ const service = { port: Number(port) };
543
+ if (path !== DEFAULT_PATH) service.path = path;
544
+ if (expect) service.expect = parseExpect(expect);
545
+ await saveServices(loaded, { ...services, [name]: service });
546
+ console.log(`Added ${name}. Try: probe check ${name}`);
547
+ }
466
548
  var add_default = defineCommand({
467
549
  meta: { name: "add", description: "Add a service" },
468
550
  args: {
@@ -832,19 +914,22 @@ async function ignoreLocalFiles(root) {
832
914
  await writeFile5(file, `${current}${separator}${missing.join("\n")}
833
915
  `);
834
916
  }
917
+ async function initProject(root) {
918
+ const paths = pathsFor(root);
919
+ if (existsSync(paths.dir)) {
920
+ throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
921
+ }
922
+ await mkdir(paths.dir);
923
+ await writeFile5(paths.config, INITIAL_CONFIG);
924
+ await ignoreLocalFiles(paths.root);
925
+ }
835
926
  var init_default = defineCommand5({
836
927
  meta: {
837
928
  name: "init",
838
929
  description: "Create .probe/ and the .gitignore entries"
839
930
  },
840
931
  async run() {
841
- const paths = pathsFor(process.cwd());
842
- if (existsSync(paths.dir)) {
843
- throw new ProbeError("ALREADY_INITIALIZED", `${PROBE_DIR}/ already exists here.`);
844
- }
845
- await mkdir(paths.dir);
846
- await writeFile5(paths.config, INITIAL_CONFIG);
847
- await ignoreLocalFiles(paths.root);
932
+ await initProject(process.cwd());
848
933
  console.log(`Created ${PROBE_DIR}/. Next: probe add api -p 3000`);
849
934
  }
850
935
  });
@@ -920,7 +1005,8 @@ import { defineCommand as defineCommand9 } from "citty";
920
1005
  var rm_default = defineCommand9({
921
1006
  meta: { name: "rm", description: "Remove a service" },
922
1007
  args: {
923
- name: { type: "positional", description: "Service name", required: true }
1008
+ name: { type: "positional", description: "Service name", required: true },
1009
+ yes: { type: "boolean", alias: "y", description: "Remove without asking" }
924
1010
  },
925
1011
  async run({ args: args2 }) {
926
1012
  const loaded = await openProject();
@@ -928,6 +1014,11 @@ var rm_default = defineCommand9({
928
1014
  if (!Object.hasOwn(services, args2.name)) {
929
1015
  throw new ProbeError("SERVICE_UNKNOWN", `unknown service "${args2.name}". See: probe ls`);
930
1016
  }
1017
+ const asks = !args2.yes && isInteractive(process.stdin, process.stdout, process.env);
1018
+ if (asks && !await confirm(`Remove ${args2.name}?`)) {
1019
+ console.log("Nothing removed.");
1020
+ return;
1021
+ }
931
1022
  const { [args2.name]: _removed, ...rest } = services;
932
1023
  await saveServices(loaded, rest);
933
1024
  console.log(`Removed ${args2.name}.`);
@@ -1279,7 +1370,10 @@ var EXAMPLES = {
1279
1370
  { command: "probe add admin -p 8080 --expect 200,204", note: "Accept these statuses only" },
1280
1371
  { command: "probe add db -p 5432 --timeout 10", note: "Give it 10 s to answer" }
1281
1372
  ],
1282
- rm: [{ command: "probe rm api", note: "Remove the service named api" }],
1373
+ rm: [
1374
+ { command: "probe rm api", note: "Remove the service named api" },
1375
+ { command: "probe rm api --yes", note: "Remove it without the question" }
1376
+ ],
1283
1377
  ls: [{ command: "probe ls", note: "List the services and their URL" }],
1284
1378
  check: [
1285
1379
  { command: "probe check", note: "Check every service" },
@@ -1409,6 +1503,23 @@ async function openedDashboard(rawArgs) {
1409
1503
  for (const failure of saves.failures()) console.error(`probe: not saved: ${failure}`);
1410
1504
  return true;
1411
1505
  }
1506
+ async function offeredInit(error, rawArgs) {
1507
+ if (!(error instanceof ProbeError) || error.code !== "NO_PROBE_DIR") return false;
1508
+ if (rawArgs.includes("--json")) return false;
1509
+ if (!isInteractive(process.stdin, process.stdout, process.env)) return false;
1510
+ const root = process.cwd();
1511
+ if (!await confirm(`No ${PROBE_DIR}/ here. Create it in ${root}?`)) return false;
1512
+ await initProject(root);
1513
+ console.log(`Created ${PROBE_DIR}/.`);
1514
+ return true;
1515
+ }
1516
+ function wantsGuidedAdd(rawArgs) {
1517
+ return rawArgs.length === 1 && rawArgs[0] === "add" && isInteractive(process.stdin, process.stdout, process.env);
1518
+ }
1519
+ async function dispatch(rawArgs) {
1520
+ if (wantsGuidedAdd(rawArgs)) return guidedAdd();
1521
+ if (!await openedDashboard(rawArgs)) await runCommand(main, { rawArgs });
1522
+ }
1412
1523
  var UNKNOWN_COMMAND = "E_UNKNOWN_COMMAND";
1413
1524
  function didYouMean(error, rawArgs) {
1414
1525
  if (error.code !== UNKNOWN_COMMAND) return "";
@@ -1432,7 +1543,12 @@ async function run(rawArgs) {
1432
1543
  const wantsVersion = rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]);
1433
1544
  if (wantsHelp || wantsVersion) return runMain(main, { rawArgs, showUsage: printUsage });
1434
1545
  try {
1435
- if (!await openedDashboard(rawArgs)) await runCommand(main, { rawArgs });
1546
+ try {
1547
+ await dispatch(rawArgs);
1548
+ } catch (error) {
1549
+ if (!await offeredInit(error, rawArgs)) throw error;
1550
+ await dispatch(rawArgs);
1551
+ }
1436
1552
  } catch (error) {
1437
1553
  process.exitCode = EXIT_ERROR;
1438
1554
  if (error instanceof ProbeError) {