@tikoci/centrs 0.1.2 → 0.1.4

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 (134) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/GLOSSARY.txt +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +55 -11
  5. package/SECURITY.md +3 -3
  6. package/commands/AGENTS.md +7 -1
  7. package/commands/api/README.md +46 -27
  8. package/commands/api/examples.md +124 -6
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +376 -53
  11. package/commands/check/examples.md +390 -0
  12. package/commands/devices/README.md +177 -37
  13. package/commands/devices/examples.md +148 -0
  14. package/commands/discover/README.md +5 -11
  15. package/commands/execute/README.md +73 -26
  16. package/commands/execute/examples.md +131 -9
  17. package/commands/explain/README.md +1716 -0
  18. package/commands/explain/examples.md +606 -0
  19. package/commands/retrieve/README.md +55 -71
  20. package/commands/retrieve/examples.md +56 -9
  21. package/commands/settings/README.md +286 -22
  22. package/commands/settings/examples.md +408 -0
  23. package/commands/terminal/README.md +6 -0
  24. package/commands/transfer/README.md +57 -19
  25. package/commands/transfer/examples.md +95 -0
  26. package/docs/CLI.md +353 -0
  27. package/docs/CONSTITUTION.md +102 -26
  28. package/docs/MATRIX.md +106 -173
  29. package/docs/errors/README.md +5 -1
  30. package/docs/errors/cdb/empty-selection.md +19 -0
  31. package/docs/errors/input/incomplete-gps.md +9 -0
  32. package/docs/errors/input/invalid-altitude.md +9 -0
  33. package/docs/errors/input/invalid-bbox.md +9 -0
  34. package/docs/errors/input/invalid-coordinate.md +9 -0
  35. package/docs/errors/input/invalid-radius.md +9 -0
  36. package/docs/errors/internal/settings-failed.md +9 -0
  37. package/docs/errors/quickchr/machine-not-found.md +22 -0
  38. package/docs/errors/quickchr/machine-stopped.md +20 -0
  39. package/docs/errors/quickchr/package-unavailable.md +27 -0
  40. package/docs/errors/quickchr/unsupported-via.md +35 -0
  41. package/docs/errors/quickchr/unsupported.md +35 -0
  42. package/docs/errors/routeros/version-unsupported.md +26 -0
  43. package/docs/errors/settings/consequential-value.md +26 -0
  44. package/docs/errors/settings/invalid-boolean.md +21 -3
  45. package/docs/errors/settings/invalid-format.md +18 -3
  46. package/docs/errors/settings/invalid-integer.md +18 -3
  47. package/docs/errors/settings/invalid-timeout.md +20 -3
  48. package/docs/errors/settings/invalid-via.md +22 -3
  49. package/docs/errors/settings/reserved-key.md +29 -0
  50. package/docs/errors/settings/skip-env-file-active.md +25 -0
  51. package/docs/errors/settings/unknown-key.md +25 -0
  52. package/docs/errors/transport/provider-trust.md +29 -0
  53. package/docs/errors/usage/fanout-not-supported.md +16 -0
  54. package/docs/errors/usage/stdin-ignored.md +28 -0
  55. package/docs/errors/validation/unknown-attribute.md +37 -4
  56. package/package.json +49 -14
  57. package/src/api-fanout.ts +469 -0
  58. package/src/api.ts +171 -56
  59. package/src/cli/api.ts +156 -13
  60. package/src/cli/btest.ts +38 -4
  61. package/src/cli/common.ts +11 -0
  62. package/src/cli/devices.ts +235 -6
  63. package/src/cli/discover.ts +3 -1
  64. package/src/cli/execute.ts +129 -9
  65. package/src/cli/explain.ts +503 -0
  66. package/src/cli/mcp.ts +8 -1
  67. package/src/cli/retrieve.ts +167 -90
  68. package/src/cli/selection.ts +260 -0
  69. package/src/cli/settings.ts +286 -0
  70. package/src/cli/terminal.ts +22 -7
  71. package/src/cli/transfer.ts +192 -51
  72. package/src/cli.ts +22 -2
  73. package/src/core/envelope.ts +5 -2
  74. package/src/core/error-catalog.ts +95 -0
  75. package/src/core/fanout.ts +143 -2
  76. package/src/core/preflight.ts +306 -0
  77. package/src/core/routeros-string.ts +24 -0
  78. package/src/devices.ts +212 -20
  79. package/src/discover.ts +7 -2
  80. package/src/errors.ts +1 -0
  81. package/src/execute-fanout.ts +453 -0
  82. package/src/execute.ts +216 -68
  83. package/src/explain/arg-tokens.ts +114 -0
  84. package/src/explain/args.ts +1403 -0
  85. package/src/explain/blocks.ts +132 -0
  86. package/src/explain/brace-slots.ts +143 -0
  87. package/src/explain/brace-tokens.ts +38 -0
  88. package/src/explain/catalog.ts +1345 -0
  89. package/src/explain/coordinates.ts +329 -0
  90. package/src/explain/defects.ts +217 -0
  91. package/src/explain/is-known-menu.ts +17 -0
  92. package/src/explain/menus.ts +664 -0
  93. package/src/explain/operator-tokens.ts +310 -0
  94. package/src/explain/operators.ts +458 -0
  95. package/src/explain/path-tokens.ts +150 -0
  96. package/src/explain/pathresolve.ts +1324 -0
  97. package/src/explain/quoted-string.ts +241 -0
  98. package/src/explain/scope-brace.ts +308 -0
  99. package/src/explain/segment.ts +757 -0
  100. package/src/explain/string-tokens.ts +67 -0
  101. package/src/explain/symbol-values.ts +520 -0
  102. package/src/explain/symbols.ts +1597 -0
  103. package/src/explain/token-ranges.ts +72 -0
  104. package/src/explain/transport.ts +497 -0
  105. package/src/explain/value-tokens.ts +80 -0
  106. package/src/explain/values.ts +357 -0
  107. package/src/explain/verbs.ts +64 -0
  108. package/src/explain/verbsplit.ts +813 -0
  109. package/src/explain/write.ts +815 -0
  110. package/src/explain.ts +2152 -0
  111. package/src/index.ts +307 -1
  112. package/src/mcp/config.ts +12 -2
  113. package/src/mcp/tools.ts +137 -2
  114. package/src/protocols/adapter.ts +38 -18
  115. package/src/protocols/index.ts +11 -5
  116. package/src/protocols/mac-telnet-console.ts +61 -17
  117. package/src/resolver/cdb.ts +10 -2
  118. package/src/resolver/comment-kv.ts +58 -4
  119. package/src/resolver/config-file.ts +136 -0
  120. package/src/resolver/facts.ts +78 -0
  121. package/src/resolver/geo.ts +453 -0
  122. package/src/resolver/index.ts +65 -0
  123. package/src/resolver/mac.ts +12 -3
  124. package/src/resolver/quickchr-provider.ts +597 -0
  125. package/src/resolver/selection.ts +391 -0
  126. package/src/resolver/service-endpoint.ts +155 -0
  127. package/src/resolver/settings.ts +54 -5
  128. package/src/resolver/target.ts +21 -3
  129. package/src/retrieve-fanout.ts +239 -133
  130. package/src/retrieve.ts +120 -25
  131. package/src/settings.ts +1078 -0
  132. package/src/terminal.ts +11 -1
  133. package/src/transfer-fanout.ts +544 -0
  134. package/src/transfer.ts +133 -40
package/src/transfer.ts CHANGED
@@ -38,14 +38,18 @@ import {
38
38
  import type { RouterOsProtocol } from "./protocols/index.ts";
39
39
  import { SftpClient } from "./protocols/sftp.ts";
40
40
  import {
41
+ assertNoQuickchrOverrideConflict,
41
42
  type CdbResolution,
43
+ loadEnvFileDefaults,
42
44
  parseDuration,
45
+ quickchrConnection,
43
46
  type ResolvedAuth,
44
47
  type ResolvedSetting,
45
48
  type ResolvedTarget,
46
49
  resolveAuth,
47
50
  resolveBooleanSetting,
48
51
  resolveCdb,
52
+ resolveQuickchrTarget,
49
53
  resolveStringSetting,
50
54
  resolveTarget,
51
55
  toCoreSource,
@@ -66,6 +70,20 @@ export const transferVerbs = [
66
70
  ] as const;
67
71
  export type TransferVerb = (typeof transferVerbs)[number];
68
72
 
73
+ /**
74
+ * Verbs that mutate the device (write path) — gated by `--yes` once up front in a
75
+ * fan-out. `download`/`list` only read the device (a download writes the *local*
76
+ * filesystem, not the router).
77
+ */
78
+ export function isTransferWriteVerb(verb: TransferVerb): boolean {
79
+ return (
80
+ verb === "upload" ||
81
+ verb === "remove" ||
82
+ verb === "mkdir" ||
83
+ verb === "copy"
84
+ );
85
+ }
86
+
69
87
  /**
70
88
  * `--via` method names. These are transfer *methods*, finer than the grid
71
89
  * transport: `rest`/`native` ride rest-api/native-api; `sftp`/`scp` ride ssh;
@@ -89,6 +107,12 @@ export const FILE_READ_CHUNK_BYTES = 32_768;
89
107
  export interface TransferRequest {
90
108
  verb: TransferVerb;
91
109
  targetInput?: string;
110
+ /**
111
+ * quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
112
+ * the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
113
+ * Resolution providers). Conflicts with host/port/username/password/sshKey.
114
+ */
115
+ quickchr?: string;
92
116
  /** Local path (or `-` for stdin/stdout). Present for upload/download. */
93
117
  local?: string;
94
118
  /** RouterOS file path. Present for upload/download/remove/mkdir and copy source. */
@@ -120,6 +144,14 @@ export interface TransferRequest {
120
144
  name?: string;
121
145
  cdbFile?: string;
122
146
  cdbPassword?: string;
147
+ /** `download` fan-out: directory to write one file per target into. */
148
+ outDir?: string;
149
+ /** Fan-out write gate: confirm a mutating fan-out (upload/remove/mkdir/copy). */
150
+ yes?: boolean;
151
+ /** Fan-out TTY detection seam (defaults to `process.stdin.isTTY`). */
152
+ stdinIsTty?: boolean;
153
+ /** Fan-out confirmation prompt seam (defaults to the shared TTY prompt). */
154
+ confirm?: (prompt: string) => Promise<boolean>;
123
155
  }
124
156
 
125
157
  export type TransferOp = TransferVerb;
@@ -190,8 +222,9 @@ interface ResolvedTransferRequest {
190
222
  export async function transfer(
191
223
  request: TransferRequest,
192
224
  env: Record<string, string | undefined> = Bun.env,
225
+ options: { cdbResolution?: CdbResolution } = {},
193
226
  ): Promise<TransferSuccessEnvelope> {
194
- const resolved = await resolveTransferRequest(request, env);
227
+ const resolved = await resolveTransferRequest(request, env, options);
195
228
  const backend = createFileBackend(resolved);
196
229
  try {
197
230
  return await runResolvedTransfer(resolved, backend);
@@ -778,8 +811,10 @@ function verifyReadSize(
778
811
  export async function resolveTransferRequest(
779
812
  request: TransferRequest,
780
813
  env: Record<string, string | undefined>,
814
+ options: { cdbResolution?: CdbResolution } = {},
781
815
  ): Promise<ResolvedTransferRequest> {
782
816
  validateTransferRequestShape(request);
817
+ const config = await loadEnvFileDefaults(env);
783
818
 
784
819
  const direction: TransferDirection =
785
820
  request.verb === "upload" ? "upload" : "download";
@@ -789,18 +824,36 @@ export async function resolveTransferRequest(
789
824
  direction,
790
825
  sizeHint,
791
826
  env,
827
+ config,
792
828
  );
793
829
 
794
- const cdbResolution = await resolveCdb(
795
- {
796
- targetInput: request.targetInput,
797
- cdbFile: request.cdbFile,
798
- cdbPassword: request.cdbPassword,
799
- },
800
- env,
801
- );
830
+ // A quickchr target is a named-live-provider: host/port/auth come from the
831
+ // live descriptor, bypassing CDB and the `__default__` ladder. For
832
+ // `--via sftp`/`scp`, `quickchrConnection` additionally gates on the SSH
833
+ // endpoint's batch-capable auth modes (typed error, never a password prompt).
834
+ let quickchrResolution: Awaited<
835
+ ReturnType<typeof resolveQuickchrTarget>
836
+ > | null = null;
837
+ if (request.quickchr !== undefined) {
838
+ assertNoQuickchrOverrideConflict(request, request.quickchr);
839
+ quickchrResolution = await resolveQuickchrTarget(request.quickchr);
840
+ }
841
+ // Fan-out passes the pre-resolved CDB record for a member (the CDB is loaded
842
+ // once in `expandSelection`); single-target resolves it here.
843
+ const cdbResolution = quickchrResolution
844
+ ? undefined
845
+ : (options.cdbResolution ??
846
+ (await resolveCdb(
847
+ {
848
+ targetInput: request.targetInput,
849
+ cdbFile: request.cdbFile,
850
+ cdbPassword: request.cdbPassword,
851
+ },
852
+ env,
853
+ config,
854
+ )));
802
855
 
803
- const format = resolveTransferFormat(request, env);
856
+ const format = resolveTransferFormat(request, env, config);
804
857
  const validate = resolveBooleanSetting(
805
858
  request.validate,
806
859
  env,
@@ -808,43 +861,68 @@ export async function resolveTransferRequest(
808
861
  true,
809
862
  "validate",
810
863
  cdbResolution?.overrides.validate,
811
- );
812
- const insecure = resolveBooleanSetting(
813
- request.insecure,
814
- env,
815
- "CENTRS_INSECURE",
816
- false,
817
- "insecure",
818
- cdbResolution?.overrides.insecure,
864
+ config,
819
865
  );
820
866
  const timeoutMs = resolveTransferTimeout(
821
867
  request.timeout,
822
868
  env,
823
869
  protocol,
824
870
  cdbResolution,
871
+ config,
825
872
  );
826
- const target = resolveTarget(
827
- {
828
- targetInput: request.targetInput,
829
- host: request.host,
830
- port: request.port,
831
- },
832
- env,
833
- protocol,
834
- cdbResolution,
835
- );
836
- const auth = resolveAuth(
837
- {
838
- username: request.username,
839
- password: request.password,
840
- sshKey: request.sshKey,
841
- },
842
- env,
843
- cdbResolution,
844
- );
873
+ const connection = quickchrResolution
874
+ ? quickchrConnection(quickchrResolution, protocol)
875
+ : null;
876
+ // A quickchr TLS/SSH endpoint is trusted by provenance (self-signed VM cert /
877
+ // ephemeral host key) — `insecure` is forced with provider provenance and the
878
+ // provider-trust warning replaces the generic `--insecure` anomaly warning.
879
+ const insecure =
880
+ connection?.insecure === true
881
+ ? {
882
+ value: true,
883
+ source: {
884
+ kind: "provider" as const,
885
+ key: `quickchr:${request.quickchr}`,
886
+ },
887
+ }
888
+ : resolveBooleanSetting(
889
+ request.insecure,
890
+ env,
891
+ "CENTRS_INSECURE",
892
+ false,
893
+ "insecure",
894
+ cdbResolution?.overrides.insecure,
895
+ config,
896
+ );
897
+ const target = connection
898
+ ? connection.target
899
+ : resolveTarget(
900
+ {
901
+ targetInput: request.targetInput,
902
+ host: request.host,
903
+ port: request.port,
904
+ },
905
+ env,
906
+ protocol,
907
+ cdbResolution,
908
+ config,
909
+ );
910
+ const auth = connection
911
+ ? connection.auth
912
+ : resolveAuth(
913
+ {
914
+ username: request.username,
915
+ password: request.password,
916
+ sshKey: request.sshKey,
917
+ },
918
+ env,
919
+ cdbResolution,
920
+ config,
921
+ );
845
922
 
846
923
  const warnings: Warning[] = [
847
924
  ...((cdbResolution?.warnings ?? []) as readonly Warning[]),
925
+ ...(connection?.warnings ?? []),
848
926
  ];
849
927
  // Report the auto hop: a large upload cannot ride the REST family's 60 KB
850
928
  // `/file/set` cap, so auto-selection moved it to sftp.
@@ -855,8 +933,10 @@ export async function resolveTransferRequest(
855
933
  "Large upload (>60 KB) auto-selected sftp; the REST family cannot write past the 60 KB `/file/set` cap.",
856
934
  });
857
935
  }
858
- // A self-signed-accepting run is an anomaly worth surfacing on every transport.
859
- if (insecure.value) {
936
+ // A self-signed-accepting run is an anomaly worth surfacing on every
937
+ // transport — unless the trust came from the provider, whose own
938
+ // provider-trust warning already rides above.
939
+ if (insecure.value && connection?.insecure !== true) {
860
940
  warnings.push({
861
941
  code: "transport/insecure-trust",
862
942
  message:
@@ -900,6 +980,7 @@ export function selectTransferMethod(
900
980
  direction: TransferDirection,
901
981
  uploadBytes: number | undefined,
902
982
  env: Record<string, string | undefined>,
983
+ config: Record<string, string | undefined> = {},
903
984
  ): {
904
985
  method: TransferMethod;
905
986
  protocol: RouterOsProtocol;
@@ -911,6 +992,9 @@ export function selectTransferMethod(
911
992
  "CENTRS_TRANSFER_VIA",
912
993
  "auto",
913
994
  "via",
995
+ undefined,
996
+ undefined,
997
+ config,
914
998
  ) as ResolvedSetting<string>;
915
999
  const requested = via.value;
916
1000
 
@@ -1062,6 +1146,7 @@ function parseVerifyMode(
1062
1146
  function resolveTransferFormat(
1063
1147
  request: TransferRequest,
1064
1148
  env: Record<string, string | undefined>,
1149
+ config: Record<string, string | undefined> = {},
1065
1150
  ): ResolvedSetting<TransferOutputFormat> {
1066
1151
  return resolveStringSetting(
1067
1152
  request.format,
@@ -1079,6 +1164,8 @@ function resolveTransferFormat(
1079
1164
  remediation: `Choose one of ${transferOutputFormats.join(", ")}.`,
1080
1165
  });
1081
1166
  },
1167
+ undefined,
1168
+ config,
1082
1169
  ) as ResolvedSetting<TransferOutputFormat>;
1083
1170
  }
1084
1171
 
@@ -1087,6 +1174,7 @@ function resolveTransferTimeout(
1087
1174
  env: Record<string, string | undefined>,
1088
1175
  protocol: RouterOsProtocol,
1089
1176
  cdb?: CdbResolution,
1177
+ config: Record<string, string | undefined> = {},
1090
1178
  ): ResolvedSetting<number> {
1091
1179
  const resolved = resolveStringSetting(
1092
1180
  timeout === undefined ? undefined : String(timeout),
@@ -1106,6 +1194,7 @@ function resolveTransferTimeout(
1106
1194
  return parsed;
1107
1195
  },
1108
1196
  cdb?.overrides.timeoutMs,
1197
+ config,
1109
1198
  ) as ResolvedSetting<number>;
1110
1199
 
1111
1200
  if (protocol === "rest-api" && resolved.value > 60_000) {
@@ -1156,7 +1245,11 @@ function writeLocalSink(
1156
1245
  process.stdout.write(bytes);
1157
1246
  return;
1158
1247
  }
1159
- writeFileSync(local, bytes);
1248
+ // Downloaded RouterOS artifacts (backups/exports) can carry credentials,
1249
+ // so a newly created destination file gets owner-only permissions
1250
+ // instead of the process umask default. Mode only applies on creation —
1251
+ // an existing destination keeps its current permissions.
1252
+ writeFileSync(local, bytes, { mode: 0o600 });
1160
1253
  }
1161
1254
 
1162
1255
  function localLabel(resolved: ResolvedTransferRequest): string {