@tikoci/centrs 0.1.1 → 0.1.3

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +107 -2
  2. package/GLOSSARY.txt +87 -0
  3. package/README.md +25 -9
  4. package/SECURITY.md +3 -3
  5. package/commands/AGENTS.md +3 -2
  6. package/commands/api/AGENTS.md +212 -0
  7. package/commands/api/README.md +177 -0
  8. package/commands/api/examples.md +428 -0
  9. package/commands/btest/README.md +56 -31
  10. package/commands/btest/examples.md +35 -10
  11. package/commands/check/README.md +371 -53
  12. package/commands/check/examples.md +390 -0
  13. package/commands/devices/README.md +177 -37
  14. package/commands/devices/examples.md +148 -0
  15. package/commands/execute/README.md +48 -7
  16. package/commands/execute/examples.md +90 -6
  17. package/commands/retrieve/README.md +37 -51
  18. package/commands/retrieve/examples.md +40 -9
  19. package/commands/settings/README.md +286 -22
  20. package/commands/settings/examples.md +408 -0
  21. package/commands/terminal/README.md +6 -0
  22. package/commands/transfer/README.md +32 -2
  23. package/commands/transfer/examples.md +66 -0
  24. package/docs/CONSTITUTION.md +40 -15
  25. package/docs/MATRIX.md +69 -142
  26. package/docs/errors/README.md +2 -1
  27. package/docs/errors/cdb/empty-selection.md +19 -0
  28. package/docs/errors/input/incomplete-gps.md +9 -0
  29. package/docs/errors/input/invalid-altitude.md +9 -0
  30. package/docs/errors/input/invalid-bbox.md +9 -0
  31. package/docs/errors/input/invalid-coordinate.md +9 -0
  32. package/docs/errors/input/invalid-radius.md +9 -0
  33. package/docs/errors/internal/settings-failed.md +9 -0
  34. package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
  35. package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
  36. package/docs/errors/settings/consequential-value.md +26 -0
  37. package/docs/errors/settings/invalid-boolean.md +21 -3
  38. package/docs/errors/settings/invalid-format.md +18 -3
  39. package/docs/errors/settings/invalid-integer.md +18 -3
  40. package/docs/errors/settings/invalid-timeout.md +20 -3
  41. package/docs/errors/settings/invalid-via.md +22 -3
  42. package/docs/errors/settings/reserved-key.md +29 -0
  43. package/docs/errors/settings/skip-env-file-active.md +25 -0
  44. package/docs/errors/settings/unknown-key.md +25 -0
  45. package/docs/errors/usage/fanout-not-supported.md +16 -0
  46. package/docs/errors/usage/invalid-method.md +23 -0
  47. package/package.json +9 -9
  48. package/src/api-fanout.ts +454 -0
  49. package/src/api.ts +1627 -0
  50. package/src/btest.ts +61 -1
  51. package/src/cli/api.ts +660 -0
  52. package/src/cli/btest.ts +20 -2
  53. package/src/cli/common.ts +75 -0
  54. package/src/cli/devices.ts +235 -6
  55. package/src/cli/discover.ts +3 -1
  56. package/src/cli/execute.ts +107 -6
  57. package/src/cli/mcp.ts +8 -1
  58. package/src/cli/retrieve.ts +135 -90
  59. package/src/cli/selection.ts +200 -0
  60. package/src/cli/settings.ts +286 -0
  61. package/src/cli/terminal.ts +22 -7
  62. package/src/cli/transfer.ts +171 -47
  63. package/src/cli.ts +29 -0
  64. package/src/core/error-catalog.ts +69 -0
  65. package/src/core/fanout.ts +301 -0
  66. package/src/core/inspect.ts +170 -0
  67. package/src/devices.ts +212 -20
  68. package/src/discover.ts +7 -2
  69. package/src/execute-fanout.ts +438 -0
  70. package/src/execute.ts +134 -70
  71. package/src/index.ts +166 -9
  72. package/src/mcp/config.ts +12 -2
  73. package/src/mcp/tools.ts +137 -2
  74. package/src/protocols/adapter.ts +357 -9
  75. package/src/protocols/btest-session.ts +279 -46
  76. package/src/protocols/btest.ts +6 -2
  77. package/src/protocols/index.ts +11 -5
  78. package/src/protocols/native-api.ts +223 -30
  79. package/src/resolver/cdb.ts +10 -2
  80. package/src/resolver/comment-kv.ts +58 -4
  81. package/src/resolver/config-file.ts +136 -0
  82. package/src/resolver/facts.ts +78 -0
  83. package/src/resolver/geo.ts +453 -0
  84. package/src/resolver/index.ts +46 -0
  85. package/src/resolver/mac.ts +12 -3
  86. package/src/resolver/selection.ts +304 -0
  87. package/src/resolver/settings.ts +54 -5
  88. package/src/resolver/target.ts +20 -2
  89. package/src/retrieve-fanout.ts +232 -268
  90. package/src/retrieve.ts +69 -94
  91. package/src/settings.ts +1078 -0
  92. package/src/terminal.ts +11 -1
  93. package/src/transfer-fanout.ts +527 -0
  94. package/src/transfer.ts +61 -11
  95. package/commands/stream/README.md +0 -70
package/src/transfer.ts CHANGED
@@ -39,6 +39,7 @@ import type { RouterOsProtocol } from "./protocols/index.ts";
39
39
  import { SftpClient } from "./protocols/sftp.ts";
40
40
  import {
41
41
  type CdbResolution,
42
+ loadEnvFileDefaults,
42
43
  parseDuration,
43
44
  type ResolvedAuth,
44
45
  type ResolvedSetting,
@@ -66,6 +67,20 @@ export const transferVerbs = [
66
67
  ] as const;
67
68
  export type TransferVerb = (typeof transferVerbs)[number];
68
69
 
70
+ /**
71
+ * Verbs that mutate the device (write path) — gated by `--yes` once up front in a
72
+ * fan-out. `download`/`list` only read the device (a download writes the *local*
73
+ * filesystem, not the router).
74
+ */
75
+ export function isTransferWriteVerb(verb: TransferVerb): boolean {
76
+ return (
77
+ verb === "upload" ||
78
+ verb === "remove" ||
79
+ verb === "mkdir" ||
80
+ verb === "copy"
81
+ );
82
+ }
83
+
69
84
  /**
70
85
  * `--via` method names. These are transfer *methods*, finer than the grid
71
86
  * transport: `rest`/`native` ride rest-api/native-api; `sftp`/`scp` ride ssh;
@@ -120,6 +135,14 @@ export interface TransferRequest {
120
135
  name?: string;
121
136
  cdbFile?: string;
122
137
  cdbPassword?: string;
138
+ /** `download` fan-out: directory to write one file per target into. */
139
+ outDir?: string;
140
+ /** Fan-out write gate: confirm a mutating fan-out (upload/remove/mkdir/copy). */
141
+ yes?: boolean;
142
+ /** Fan-out TTY detection seam (defaults to `process.stdin.isTTY`). */
143
+ stdinIsTty?: boolean;
144
+ /** Fan-out confirmation prompt seam (defaults to the shared TTY prompt). */
145
+ confirm?: (prompt: string) => Promise<boolean>;
123
146
  }
124
147
 
125
148
  export type TransferOp = TransferVerb;
@@ -190,8 +213,9 @@ interface ResolvedTransferRequest {
190
213
  export async function transfer(
191
214
  request: TransferRequest,
192
215
  env: Record<string, string | undefined> = Bun.env,
216
+ options: { cdbResolution?: CdbResolution } = {},
193
217
  ): Promise<TransferSuccessEnvelope> {
194
- const resolved = await resolveTransferRequest(request, env);
218
+ const resolved = await resolveTransferRequest(request, env, options);
195
219
  const backend = createFileBackend(resolved);
196
220
  try {
197
221
  return await runResolvedTransfer(resolved, backend);
@@ -778,8 +802,10 @@ function verifyReadSize(
778
802
  export async function resolveTransferRequest(
779
803
  request: TransferRequest,
780
804
  env: Record<string, string | undefined>,
805
+ options: { cdbResolution?: CdbResolution } = {},
781
806
  ): Promise<ResolvedTransferRequest> {
782
807
  validateTransferRequestShape(request);
808
+ const config = await loadEnvFileDefaults(env);
783
809
 
784
810
  const direction: TransferDirection =
785
811
  request.verb === "upload" ? "upload" : "download";
@@ -789,18 +815,24 @@ export async function resolveTransferRequest(
789
815
  direction,
790
816
  sizeHint,
791
817
  env,
818
+ config,
792
819
  );
793
820
 
794
- const cdbResolution = await resolveCdb(
795
- {
796
- targetInput: request.targetInput,
797
- cdbFile: request.cdbFile,
798
- cdbPassword: request.cdbPassword,
799
- },
800
- env,
801
- );
821
+ // Fan-out passes the pre-resolved CDB record for a member (the CDB is loaded
822
+ // once in `expandCdbSelection`); single-target resolves it here.
823
+ const cdbResolution =
824
+ options.cdbResolution ??
825
+ (await resolveCdb(
826
+ {
827
+ targetInput: request.targetInput,
828
+ cdbFile: request.cdbFile,
829
+ cdbPassword: request.cdbPassword,
830
+ },
831
+ env,
832
+ config,
833
+ ));
802
834
 
803
- const format = resolveTransferFormat(request, env);
835
+ const format = resolveTransferFormat(request, env, config);
804
836
  const validate = resolveBooleanSetting(
805
837
  request.validate,
806
838
  env,
@@ -808,6 +840,7 @@ export async function resolveTransferRequest(
808
840
  true,
809
841
  "validate",
810
842
  cdbResolution?.overrides.validate,
843
+ config,
811
844
  );
812
845
  const insecure = resolveBooleanSetting(
813
846
  request.insecure,
@@ -816,12 +849,14 @@ export async function resolveTransferRequest(
816
849
  false,
817
850
  "insecure",
818
851
  cdbResolution?.overrides.insecure,
852
+ config,
819
853
  );
820
854
  const timeoutMs = resolveTransferTimeout(
821
855
  request.timeout,
822
856
  env,
823
857
  protocol,
824
858
  cdbResolution,
859
+ config,
825
860
  );
826
861
  const target = resolveTarget(
827
862
  {
@@ -832,6 +867,7 @@ export async function resolveTransferRequest(
832
867
  env,
833
868
  protocol,
834
869
  cdbResolution,
870
+ config,
835
871
  );
836
872
  const auth = resolveAuth(
837
873
  {
@@ -841,6 +877,7 @@ export async function resolveTransferRequest(
841
877
  },
842
878
  env,
843
879
  cdbResolution,
880
+ config,
844
881
  );
845
882
 
846
883
  const warnings: Warning[] = [
@@ -900,6 +937,7 @@ export function selectTransferMethod(
900
937
  direction: TransferDirection,
901
938
  uploadBytes: number | undefined,
902
939
  env: Record<string, string | undefined>,
940
+ config: Record<string, string | undefined> = {},
903
941
  ): {
904
942
  method: TransferMethod;
905
943
  protocol: RouterOsProtocol;
@@ -911,6 +949,9 @@ export function selectTransferMethod(
911
949
  "CENTRS_TRANSFER_VIA",
912
950
  "auto",
913
951
  "via",
952
+ undefined,
953
+ undefined,
954
+ config,
914
955
  ) as ResolvedSetting<string>;
915
956
  const requested = via.value;
916
957
 
@@ -1062,6 +1103,7 @@ function parseVerifyMode(
1062
1103
  function resolveTransferFormat(
1063
1104
  request: TransferRequest,
1064
1105
  env: Record<string, string | undefined>,
1106
+ config: Record<string, string | undefined> = {},
1065
1107
  ): ResolvedSetting<TransferOutputFormat> {
1066
1108
  return resolveStringSetting(
1067
1109
  request.format,
@@ -1079,6 +1121,8 @@ function resolveTransferFormat(
1079
1121
  remediation: `Choose one of ${transferOutputFormats.join(", ")}.`,
1080
1122
  });
1081
1123
  },
1124
+ undefined,
1125
+ config,
1082
1126
  ) as ResolvedSetting<TransferOutputFormat>;
1083
1127
  }
1084
1128
 
@@ -1087,6 +1131,7 @@ function resolveTransferTimeout(
1087
1131
  env: Record<string, string | undefined>,
1088
1132
  protocol: RouterOsProtocol,
1089
1133
  cdb?: CdbResolution,
1134
+ config: Record<string, string | undefined> = {},
1090
1135
  ): ResolvedSetting<number> {
1091
1136
  const resolved = resolveStringSetting(
1092
1137
  timeout === undefined ? undefined : String(timeout),
@@ -1106,6 +1151,7 @@ function resolveTransferTimeout(
1106
1151
  return parsed;
1107
1152
  },
1108
1153
  cdb?.overrides.timeoutMs,
1154
+ config,
1109
1155
  ) as ResolvedSetting<number>;
1110
1156
 
1111
1157
  if (protocol === "rest-api" && resolved.value > 60_000) {
@@ -1156,7 +1202,11 @@ function writeLocalSink(
1156
1202
  process.stdout.write(bytes);
1157
1203
  return;
1158
1204
  }
1159
- writeFileSync(local, bytes);
1205
+ // Downloaded RouterOS artifacts (backups/exports) can carry credentials,
1206
+ // so a newly created destination file gets owner-only permissions
1207
+ // instead of the process umask default. Mode only applies on creation —
1208
+ // an existing destination keeps its current permissions.
1209
+ writeFileSync(local, bytes, { mode: 0o600 });
1160
1210
  }
1161
1211
 
1162
1212
  function localLabel(resolved: ResolvedTransferRequest): string {
@@ -1,70 +0,0 @@
1
- # stream
2
-
3
- Follow live RouterOS output — `print follow`, `monitor`-style commands, and
4
- sniffer feeds — as a stream of envelopes.
5
-
6
- Status: `designed`. This file describes intent and flags; no implementation yet.
7
- See `docs/MATRIX.md` for the cell states. `stream` lands with a transport that
8
- can hold an open read (native-api first, then ssh); it is **not** a REST cell.
9
-
10
- ## Why a separate verb
11
-
12
- `stream` exists because open-ended follow is a different I/O contract than a
13
- one-shot read, and conflating it into `retrieve` would force every retrieve
14
- consumer to handle "maybe this is a stream." A caller that picks `stream` knows
15
- it is getting a sequence of frames, not a single envelope. Bounded, finite
16
- captures stay on `retrieve` (`--once`); interactive PTY stays on `terminal`
17
- (constitution: protocol selection).
18
-
19
- ## Intent
20
-
21
- - **Read-only**, validated like `retrieve` (path-shaped commands gate through
22
- `/console/inspect`). `stream` never writes.
23
- - Wraps RouterOS follow semantics: `once`, `follow`, `duration=`,
24
- `freeze-frame-interval=`. Targets are the `monitor`/`print follow`/sniffer
25
- family of menus.
26
- - **Transport:** true follow cannot ride REST (RouterOS REST has a 60s hard
27
- cap), so `stream` is **native-api / ssh-first**. `--via rest-api` is
28
- bounded-or-rejected, never a silent open-ended poll.
29
- - Not a fan-out surface in single-session mode: a single `stream` rejects N>1
30
- targets with `usage/fanout-not-supported` (constitution: target selection).
31
-
32
- ## Output contract (NDJSON stream of envelopes)
33
-
34
- - Each frame is **one envelope** (`{ ok, data?, warnings?, error?, meta }`),
35
- emitted as one NDJSON line under `--format json`. Text mode renders
36
- human-readable frames.
37
- - The stream is terminated by a final **summary envelope**: frame count,
38
- duration, and stop reason (`duration-elapsed`, `interrupted`, `eof`,
39
- `transport-error`).
40
- - A mid-stream RouterOS or transport error is itself a frame with `ok: false`;
41
- the **process exit code** reflects whether the stream *started* cleanly, not
42
- whether every frame was `ok`.
43
- - `--duration` / `--count` bound an otherwise open-ended follow; Ctrl-C stops it
44
- and still emits the summary envelope.
45
-
46
- ## Flags (planned)
47
-
48
- | Flag | Behavior |
49
- | ---- | -------- |
50
- | `--via <protocol>` | Pin transport (`native-api` or `ssh`). REST is bounded-or-rejected. |
51
- | `--duration <dur>` | Stop after this wall-clock window (maps to RouterOS `duration=`). |
52
- | `--count <n>` | Stop after N frames. |
53
- | `--freeze-frame-interval <dur>` | RouterOS `freeze-frame-interval=` passthrough. |
54
- | `--format <text\|json>` | `json` is NDJSON (one envelope per line); `text` is human frames. |
55
-
56
- `tail` is a log-scoped alias: `centrs tail <router> /log` ≈ `tail -f` over
57
- `/log print follow`.
58
-
59
- ## Open questions
60
-
61
- - Whether `stream` over ssh shells out to the RouterOS CLI follow or rides a
62
- structured channel; settle during the SSH transport work.
63
- - Backpressure / max-buffered-frames policy for a fast producer.
64
-
65
- Sequencing (decided): do **not** start `stream` until (1) a native-api
66
- *streaming* spike confirms the reader is solid as a long-lived follow channel
67
- (not just request/response), and (2) `retrieve` is further along and re-reviewed
68
- for UX — `stream` should inherit a settled retrieve shape, not race it. The
69
- native-api streaming reader already exists (`src/protocols/native-api.ts`) and is
70
- the first follow consumer.