@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/retrieve.ts CHANGED
@@ -6,6 +6,15 @@ import type {
6
6
  SettingSource as CoreSettingSource,
7
7
  EnvelopeValidationMeta,
8
8
  } from "./core/envelope.ts";
9
+ import {
10
+ extractCompletionNames,
11
+ inspectChildren,
12
+ inspectChildrenOrEmpty,
13
+ inspectCompletions,
14
+ isArgumentNode,
15
+ isCommandNode,
16
+ pathTokens,
17
+ } from "./core/inspect.ts";
9
18
  import { CentrsError, serializeCentrsError } from "./errors.ts";
10
19
  import {
11
20
  createProtocolAdapter,
@@ -19,6 +28,7 @@ import {
19
28
  import {
20
29
  type CdbResolution,
21
30
  isIpTransport,
31
+ loadEnvFileDefaults,
22
32
  parseDuration,
23
33
  parseResolvePolicy,
24
34
  type ResolvedAuth,
@@ -101,20 +111,6 @@ export type RetrieveSuccessEnvelope = CentrsSuccessEnvelope<
101
111
  >;
102
112
  export type RetrieveErrorEnvelope = CentrsErrorEnvelope<RetrieveOperationMeta>;
103
113
 
104
- interface InspectChildItem {
105
- type?: string;
106
- name?: string;
107
- "node-type"?: string;
108
- }
109
-
110
- interface InspectCompletionItem {
111
- type?: string;
112
- name?: string;
113
- completion?: string;
114
- value?: string;
115
- text?: string;
116
- }
117
-
118
114
  interface RetrieveInspection {
119
115
  command: "get" | "print";
120
116
  }
@@ -623,9 +619,10 @@ export function buildResolvedRetrieve(
623
619
  cdbResolution: CdbResolution | undefined,
624
620
  attributeSelections: readonly string[],
625
621
  macResolution?: { mac: string; ip: string },
622
+ config: Record<string, string | undefined> = {},
626
623
  ): ResolvedRetrieveRequest {
627
- const via = resolveProtocol(request, env, cdbResolution);
628
- const format = resolveFormat(request, env);
624
+ const via = resolveProtocol(request, env, cdbResolution, config);
625
+ const format = resolveFormat(request, env, config);
629
626
  const validate = resolveBooleanSetting(
630
627
  request.validate,
631
628
  env,
@@ -633,18 +630,22 @@ export function buildResolvedRetrieve(
633
630
  true,
634
631
  "validate",
635
632
  cdbResolution?.overrides.validate,
633
+ config,
636
634
  );
637
635
  const timeoutMs = resolveTimeoutSetting(
638
636
  request.timeout,
639
637
  env,
640
638
  via.value,
641
639
  cdbResolution?.overrides.timeoutMs,
640
+ config,
642
641
  );
643
642
  const maxResultsBytes = resolveOptionalIntegerSetting(
644
643
  request.maxResultsBytes,
645
644
  env,
646
645
  "CENTRS_MAX_RESULTS",
647
646
  "max-results",
647
+ undefined,
648
+ config,
648
649
  );
649
650
  const target = resolveTarget(
650
651
  {
@@ -696,22 +697,33 @@ export function resolveRetrieveGlobalContext(
696
697
  request: RetrieveRequest,
697
698
  env: Record<string, string | undefined>,
698
699
  attributeSelections: readonly string[],
700
+ config: Record<string, string | undefined> = {},
699
701
  ): RetrieveGlobalContext {
700
- const via = resolveProtocol(request, env);
701
- const format = resolveFormat(request, env);
702
+ const via = resolveProtocol(request, env, undefined, config);
703
+ const format = resolveFormat(request, env, config);
702
704
  const validate = resolveBooleanSetting(
703
705
  request.validate,
704
706
  env,
705
707
  "CENTRS_VALIDATE",
706
708
  true,
707
709
  "validate",
710
+ undefined,
711
+ config,
712
+ );
713
+ const timeoutMs = resolveTimeoutSetting(
714
+ request.timeout,
715
+ env,
716
+ via.value,
717
+ undefined,
718
+ config,
708
719
  );
709
- const timeoutMs = resolveTimeoutSetting(request.timeout, env, via.value);
710
720
  const maxResultsBytes = resolveOptionalIntegerSetting(
711
721
  request.maxResultsBytes,
712
722
  env,
713
723
  "CENTRS_MAX_RESULTS",
714
724
  "max-results",
725
+ undefined,
726
+ config,
715
727
  );
716
728
 
717
729
  return {
@@ -744,6 +756,7 @@ export async function resolveRetrieveRequest(
744
756
  env: Record<string, string | undefined>,
745
757
  ): Promise<ResolvedRetrieveRequest> {
746
758
  const attributeSelections = validateRetrieveRequestShape(request);
759
+ const config = await loadEnvFileDefaults(env);
747
760
 
748
761
  const cdbResolution = await resolveCdb(
749
762
  {
@@ -752,12 +765,14 @@ export async function resolveRetrieveRequest(
752
765
  cdbPassword: request.cdbPassword,
753
766
  },
754
767
  env,
768
+ config,
755
769
  );
756
770
 
757
771
  const macResolution = await resolveMacForRetrieve(
758
772
  request,
759
773
  env,
760
774
  cdbResolution,
775
+ config,
761
776
  );
762
777
 
763
778
  return buildResolvedRetrieve(
@@ -766,6 +781,7 @@ export async function resolveRetrieveRequest(
766
781
  cdbResolution,
767
782
  attributeSelections,
768
783
  macResolution,
784
+ config,
769
785
  );
770
786
  }
771
787
 
@@ -779,8 +795,9 @@ export async function resolveMacForRetrieve(
779
795
  request: RetrieveRequest,
780
796
  env: Record<string, string | undefined>,
781
797
  cdb?: CdbResolution,
798
+ config: Record<string, string | undefined> = {},
782
799
  ): Promise<{ mac: string; ip: string } | undefined> {
783
- if (!isIpTransport(resolveProtocol(request, env, cdb).value)) {
800
+ if (!isIpTransport(resolveProtocol(request, env, cdb, config).value)) {
784
801
  return undefined;
785
802
  }
786
803
  return resolveMacTarget({
@@ -788,7 +805,10 @@ export async function resolveMacForRetrieve(
788
805
  targetInput: request.targetInput,
789
806
  cdbTarget: cdb?.target,
790
807
  env,
791
- policy: parseResolvePolicy(request.resolve ?? env["CENTRS_RESOLVE"]),
808
+ config,
809
+ policy: parseResolvePolicy(
810
+ request.resolve ?? env["CENTRS_RESOLVE"] ?? config["CENTRS_RESOLVE"],
811
+ ),
792
812
  operation: "retrieve",
793
813
  });
794
814
  }
@@ -797,6 +817,7 @@ function resolveProtocol(
797
817
  request: RetrieveRequest,
798
818
  env: Record<string, string | undefined>,
799
819
  cdb?: CdbResolution,
820
+ config: Record<string, string | undefined> = {},
800
821
  ): ResolvedSetting<RouterOsProtocol> {
801
822
  const via = resolveStringSetting(
802
823
  request.via,
@@ -806,6 +827,7 @@ function resolveProtocol(
806
827
  "via",
807
828
  undefined,
808
829
  cdb?.overrides.via,
830
+ config,
809
831
  );
810
832
  if (!via) {
811
833
  throw new CentrsError({
@@ -864,6 +886,7 @@ function resolveProtocol(
864
886
  function resolveFormat(
865
887
  request: RetrieveRequest,
866
888
  env: Record<string, string | undefined>,
889
+ config: Record<string, string | undefined> = {},
867
890
  ): ResolvedSetting<RetrieveOutputFormat> {
868
891
  return resolveStringSetting(
869
892
  request.format,
@@ -872,45 +895,25 @@ function resolveFormat(
872
895
  "text",
873
896
  "format",
874
897
  parseOutputFormat,
898
+ undefined,
899
+ config,
875
900
  ) as ResolvedSetting<RetrieveOutputFormat>;
876
901
  }
877
902
 
878
903
  /**
879
- * Path-existence probe. An invalid path raises a native trap that
880
- * {@link mapRouterOsError} normalizes to `routeros/unknown-path` (REST instead
881
- * returns an empty child list). Both the legacy `routeros/api-trap` catch-all
882
- * and the grounded `routeros/unknown-path` classification are flattened to an
883
- * empty list so the orchestrator surfaces a single `validation/unknown-path`.
884
- * Traps during attribute/completion discovery do NOT use this probe — they
885
- * surface as-is.
904
+ * Resolve the read command (`print` vs `get`) for a path via a `request=child`
905
+ * existence probe. Uses {@link inspectChildrenOrEmpty} so an invalid path (a
906
+ * native trap, or REST's empty child list) flattens to no children and surfaces
907
+ * as a single `validation/unknown-path` here rather than leaking a transport
908
+ * trap. Attribute/completion discovery does NOT use the swallowing probe — traps
909
+ * there surface as-is.
886
910
  */
887
- async function inspectChildrenForProbe(
888
- backend: ProtocolAdapter,
889
- path: string,
890
- ): Promise<unknown[]> {
891
- try {
892
- return await backend.inspect("child", path);
893
- } catch (error) {
894
- if (
895
- error instanceof CentrsError &&
896
- (error.code === "routeros/api-trap" ||
897
- error.code === "routeros/unknown-path")
898
- ) {
899
- return [];
900
- }
901
- throw error;
902
- }
903
- }
904
-
905
911
  async function inspectRetrievePath(
906
912
  resolved: ResolvedRetrieveRequest,
907
913
  backend: ProtocolAdapter,
908
914
  ): Promise<RetrieveInspection> {
909
- const pathTokens = pathTokensForInspect(resolved.path);
910
- const rootChildren = (await inspectChildrenForProbe(
911
- backend,
912
- pathToInspectString(pathTokens),
913
- )) as InspectChildItem[];
915
+ const tokens = pathTokens(resolved.path);
916
+ const rootChildren = await inspectChildrenOrEmpty(backend, tokens);
914
917
 
915
918
  const supportsPrint = rootChildren.some((child) =>
916
919
  isCommandNode(child, "print"),
@@ -943,21 +946,24 @@ async function inspectAttributes(
943
946
  inspection: RetrieveInspection,
944
947
  backend: ProtocolAdapter,
945
948
  ): Promise<string[]> {
946
- const pathTokens = pathTokensForInspect(resolved.path);
949
+ const tokens = pathTokens(resolved.path);
947
950
  const argument = inspection.command === "get" ? "value-name" : "proplist";
948
- const completionRows = (await backend.inspect(
949
- "completion",
950
- pathToInspectString([...pathTokens, inspection.command, argument]),
951
- )) as InspectCompletionItem[];
952
- const completions = extractCompletionNames(completionRows);
951
+ const completionRows = await inspectCompletions(backend, [
952
+ ...tokens,
953
+ inspection.command,
954
+ argument,
955
+ ]);
956
+ const completions = [
957
+ ...new Set(extractCompletionNames(completionRows)),
958
+ ].sort();
953
959
  if (completions.length > 0) {
954
960
  return completions;
955
961
  }
956
962
 
957
- const commandChildren = (await backend.inspect(
958
- "child",
959
- pathToInspectString([...pathTokens, inspection.command]),
960
- )) as InspectChildItem[];
963
+ const commandChildren = await inspectChildren(backend, [
964
+ ...tokens,
965
+ inspection.command,
966
+ ]);
961
967
  return commandChildren
962
968
  .filter(isArgumentNode)
963
969
  .map((child) => child.name)
@@ -985,35 +991,6 @@ async function executeRetrieve(
985
991
  });
986
992
  }
987
993
 
988
- function pathTokensForInspect(path: string): string[] {
989
- return path.split("/").filter(Boolean);
990
- }
991
-
992
- function isCommandNode(
993
- child: InspectChildItem,
994
- name: "get" | "print",
995
- ): boolean {
996
- return (
997
- child.name === name &&
998
- (child.type === "cmd" || child["node-type"] === "cmd")
999
- );
1000
- }
1001
-
1002
- function isArgumentNode(child: InspectChildItem): boolean {
1003
- return child.type === "arg" || child["node-type"] === "arg";
1004
- }
1005
-
1006
- function extractCompletionNames(
1007
- rows: readonly InspectCompletionItem[],
1008
- ): string[] {
1009
- const names = rows
1010
- .flatMap((row) => [row.completion, row.name, row.value, row.text])
1011
- .filter((value): value is string => typeof value === "string")
1012
- .map((value) => value.replace(/=.*$/, "").trim())
1013
- .filter((value) => value.length > 0);
1014
- return [...new Set(names)].sort();
1015
- }
1016
-
1017
994
  function isKnownSingletonPath(path: string): boolean {
1018
995
  return ["/system/resource", "/system/identity"].includes(
1019
996
  path.replace(/\/$/, ""),
@@ -1043,6 +1020,7 @@ function resolveTimeoutSetting(
1043
1020
  env: Record<string, string | undefined>,
1044
1021
  via: RouterOsProtocol,
1045
1022
  commentKv?: ResolvedSetting<number>,
1023
+ config: Record<string, string | undefined> = {},
1046
1024
  ): ResolvedSetting<number> {
1047
1025
  const resolved = resolveStringSetting(
1048
1026
  timeout === undefined ? undefined : String(timeout),
@@ -1063,6 +1041,7 @@ function resolveTimeoutSetting(
1063
1041
  return parsed;
1064
1042
  },
1065
1043
  commentKv,
1044
+ config,
1066
1045
  );
1067
1046
  if (!resolved) {
1068
1047
  throw new Error("timeout resolution produced no value");
@@ -1122,10 +1101,6 @@ function parseOutputFormat(value: string): RetrieveOutputFormat {
1122
1101
  });
1123
1102
  }
1124
1103
 
1125
- function pathToInspectString(path: readonly string[]): string {
1126
- return path.join(",");
1127
- }
1128
-
1129
1104
  function countResultObjects(data: unknown): number {
1130
1105
  if (Array.isArray(data)) {
1131
1106
  return data.length;