@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.
- package/CHANGELOG.md +107 -2
- package/GLOSSARY.txt +87 -0
- package/README.md +25 -9
- package/SECURITY.md +3 -3
- package/commands/AGENTS.md +3 -2
- package/commands/api/AGENTS.md +212 -0
- package/commands/api/README.md +177 -0
- package/commands/api/examples.md +428 -0
- package/commands/btest/README.md +56 -31
- package/commands/btest/examples.md +35 -10
- package/commands/check/README.md +371 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/execute/README.md +48 -7
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +37 -51
- package/commands/retrieve/examples.md +40 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +32 -2
- package/commands/transfer/examples.md +66 -0
- package/docs/CONSTITUTION.md +40 -15
- package/docs/MATRIX.md +69 -142
- package/docs/errors/README.md +2 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
- package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/docs/errors/usage/invalid-method.md +23 -0
- package/package.json +9 -9
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +1627 -0
- package/src/btest.ts +61 -1
- package/src/cli/api.ts +660 -0
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +75 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +107 -6
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +135 -90
- package/src/cli/selection.ts +200 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +171 -47
- package/src/cli.ts +29 -0
- package/src/core/error-catalog.ts +69 -0
- package/src/core/fanout.ts +301 -0
- package/src/core/inspect.ts +170 -0
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +134 -70
- package/src/index.ts +166 -9
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +357 -9
- package/src/protocols/btest-session.ts +279 -46
- package/src/protocols/btest.ts +6 -2
- package/src/protocols/index.ts +11 -5
- package/src/protocols/native-api.ts +223 -30
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +46 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/selection.ts +304 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +20 -2
- package/src/retrieve-fanout.ts +232 -268
- package/src/retrieve.ts +69 -94
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +527 -0
- package/src/transfer.ts +61 -11
- 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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
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.
|