@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
@@ -0,0 +1,286 @@
1
+ /**
2
+ * `centrs settings` CLI surface: command metadata, subcommand parsing,
3
+ * dispatch, and error-to-envelope rendering. Core logic lives in
4
+ * `../settings.ts`; this file only does CLI-shaped plumbing, mirroring
5
+ * `./devices.ts`'s pattern for the other transport-less command.
6
+ *
7
+ * Bare `centrs settings` (no subcommand) behaves like `print` regardless of
8
+ * TTY — the interactive first-time-setup flow (`@clack/prompts`) is an
9
+ * explicitly deferred later slice (`commands/settings/README.md`), not built
10
+ * here.
11
+ */
12
+
13
+ import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
14
+ import { describeCentrs } from "../index.ts";
15
+ import { defaultSettingsPath } from "../resolver/index.ts";
16
+ import {
17
+ buildSettingsErrorEnvelope,
18
+ renderSettingsEnvelope,
19
+ type SettingsCommand,
20
+ type SettingsEnvelope,
21
+ type SettingsOutputFormat,
22
+ settingsGet,
23
+ settingsOutputFormats,
24
+ settingsPrint,
25
+ settingsReset,
26
+ settingsSet,
27
+ } from "../settings.ts";
28
+ import {
29
+ type CliCommandMetadata,
30
+ expectValue,
31
+ renderCommandHelp,
32
+ unknownFlagError,
33
+ } from "./common.ts";
34
+
35
+ export const settingsCommand: CliCommandMetadata = {
36
+ name: "settings",
37
+ usage: "centrs settings [print|get|set|reset] [args] [flags]",
38
+ summary:
39
+ "Manage centrs's own global preferences (`centrs.env`) and inspect the `__default__` CDB record.",
40
+ options: [
41
+ {
42
+ flag: "--all",
43
+ description:
44
+ "`print` only — also list unrecognized CENTRS_* lines found in the file.",
45
+ },
46
+ {
47
+ flag: "--cdb-file",
48
+ valueName: "<path>",
49
+ description:
50
+ "`print` only — CDB to probe for the `__default__` record summary (falls back to normal CDB resolution; never centrs.env).",
51
+ },
52
+ {
53
+ flag: "--cdb-password",
54
+ valueName: "<secret>",
55
+ description: "`print` only — decrypt password for the probed CDB.",
56
+ },
57
+ {
58
+ flag: "--skip-env-file",
59
+ description:
60
+ "`print` only — note that other commands in this environment would not see centrs.env (settings itself always reads the real file).",
61
+ },
62
+ {
63
+ flag: "--format",
64
+ valueName: `<${settingsOutputFormats.join("|")}>`,
65
+ description: "Output format for the CLI response.",
66
+ },
67
+ { flag: "--json", description: "Shortcut for `--format json`." },
68
+ { flag: "--help", description: "Show this help." },
69
+ ],
70
+ };
71
+
72
+ type SettingsSubcommand = SettingsCommand;
73
+
74
+ interface SettingsCliArgs {
75
+ help?: boolean;
76
+ subcommand: SettingsSubcommand;
77
+ attr?: string;
78
+ value?: string;
79
+ all?: boolean;
80
+ cdbFile?: string;
81
+ cdbPassword?: string;
82
+ skipEnvFile?: boolean;
83
+ format?: SettingsOutputFormat;
84
+ }
85
+
86
+ function parseSettingsCliArgs(args: readonly string[]): SettingsCliArgs {
87
+ const positional: string[] = [];
88
+ let help = false;
89
+ let all = false;
90
+ let cdbFile: string | undefined;
91
+ let cdbPassword: string | undefined;
92
+ let skipEnvFile = false;
93
+ let format: SettingsOutputFormat | undefined;
94
+
95
+ for (let index = 0; index < args.length; index += 1) {
96
+ const arg = args[index];
97
+ if (arg === undefined) {
98
+ continue;
99
+ }
100
+ switch (arg) {
101
+ case "--help":
102
+ case "-h":
103
+ help = true;
104
+ break;
105
+ case "--all":
106
+ all = true;
107
+ break;
108
+ case "--cdb-file":
109
+ cdbFile = expectValue(args, ++index, arg);
110
+ break;
111
+ case "--cdb-password":
112
+ cdbPassword = expectValue(args, ++index, arg);
113
+ break;
114
+ case "--skip-env-file":
115
+ skipEnvFile = true;
116
+ break;
117
+ case "--format": {
118
+ const value = expectValue(args, ++index, arg);
119
+ if (!settingsOutputFormats.includes(value as SettingsOutputFormat)) {
120
+ throw new Error(
121
+ `--format must be one of ${settingsOutputFormats.join(", ")}; got ${value}.`,
122
+ );
123
+ }
124
+ format = value as SettingsOutputFormat;
125
+ break;
126
+ }
127
+ case "--json":
128
+ format = "json";
129
+ break;
130
+ default:
131
+ // A negative-integer value (e.g. `settings set max-results -1`) looks
132
+ // like a flag but is a legitimate positional — `settings` has no flag
133
+ // shaped like a bare negative number, so this is unambiguous.
134
+ if (arg.startsWith("-") && !/^-\d+$/.test(arg)) {
135
+ throw unknownFlagError("settings", arg, settingsCommand.options);
136
+ }
137
+ positional.push(arg);
138
+ break;
139
+ }
140
+ }
141
+
142
+ const result: SettingsCliArgs = {
143
+ help,
144
+ subcommand: "print",
145
+ all,
146
+ cdbFile,
147
+ cdbPassword,
148
+ skipEnvFile,
149
+ format,
150
+ };
151
+ if (help) {
152
+ return result;
153
+ }
154
+
155
+ const [rawSub, ...rest] = positional;
156
+ // Bare `centrs settings` (no subcommand at all) behaves like `print`.
157
+ if (rawSub === undefined) {
158
+ return result;
159
+ }
160
+
161
+ const subcommands: readonly SettingsSubcommand[] = [
162
+ "print",
163
+ "get",
164
+ "set",
165
+ "reset",
166
+ ];
167
+ if (!subcommands.includes(rawSub as SettingsSubcommand)) {
168
+ throw new Error(
169
+ `Unknown settings subcommand: ${rawSub}. Use print, get, set, or reset (or omit the subcommand for print).`,
170
+ );
171
+ }
172
+ result.subcommand = rawSub as SettingsSubcommand;
173
+
174
+ if (result.subcommand === "print" || result.subcommand === "reset") {
175
+ if (rest.length > 1) {
176
+ throw new Error(
177
+ `\`centrs settings ${result.subcommand}\` accepts at most one <attr>.`,
178
+ );
179
+ }
180
+ result.attr = rest[0];
181
+ } else if (result.subcommand === "get") {
182
+ if (rest.length !== 1) {
183
+ throw new Error("`centrs settings get` requires exactly one <attr>.");
184
+ }
185
+ result.attr = rest[0];
186
+ } else if (result.subcommand === "set") {
187
+ if (rest.length !== 2) {
188
+ throw new Error("`centrs settings set` requires exactly <attr> <value>.");
189
+ }
190
+ result.attr = rest[0];
191
+ result.value = rest[1];
192
+ }
193
+
194
+ return result;
195
+ }
196
+
197
+ export async function runSettingsCli(args: readonly string[]): Promise<number> {
198
+ let parsed: SettingsCliArgs | undefined;
199
+ let envSnapshot: Record<string, string | undefined> | undefined;
200
+ try {
201
+ parsed = parseSettingsCliArgs(args);
202
+ if (parsed.help) {
203
+ console.log(renderCommandHelp(describeCentrs(), settingsCommand));
204
+ return 0;
205
+ }
206
+
207
+ envSnapshot = Bun.env;
208
+ let envelope: SettingsEnvelope<unknown>;
209
+ switch (parsed.subcommand) {
210
+ case "print":
211
+ envelope = await settingsPrint({
212
+ attr: parsed.attr,
213
+ all: parsed.all,
214
+ env: envSnapshot,
215
+ skipEnvFile: parsed.skipEnvFile,
216
+ cdbFile: parsed.cdbFile,
217
+ cdbPassword: parsed.cdbPassword,
218
+ });
219
+ break;
220
+ case "get":
221
+ if (!parsed.attr) {
222
+ throw new Error("Missing <attr> for settings get.");
223
+ }
224
+ envelope = await settingsGet({ attr: parsed.attr, env: envSnapshot });
225
+ break;
226
+ case "set":
227
+ if (!parsed.attr || parsed.value === undefined) {
228
+ throw new Error("Missing <attr> <value> for settings set.");
229
+ }
230
+ envelope = await settingsSet({
231
+ attr: parsed.attr,
232
+ value: parsed.value,
233
+ env: envSnapshot,
234
+ });
235
+ break;
236
+ case "reset":
237
+ envelope = await settingsReset({
238
+ attr: parsed.attr,
239
+ env: envSnapshot,
240
+ });
241
+ break;
242
+ default:
243
+ throw new Error("Unreachable settings subcommand.");
244
+ }
245
+
246
+ const format = parsed.format ?? "text";
247
+ // codeql[js/clear-text-logging] passwordSet is a has-password boolean, never the secret; the property-NAME heuristic (maybePassword) matches the field name, not its value type.
248
+ console.log(renderSettingsEnvelope(envelope, format));
249
+ return 0;
250
+ } catch (error) {
251
+ const format = parsed?.format ?? inferSettingsFormat(args);
252
+ const settingsFile = defaultSettingsPath(envSnapshot ?? Bun.env);
253
+ const centrsError = asCentrsError(error, {
254
+ code: "input/invalid-command",
255
+ summary: error instanceof Error ? error.message : String(error),
256
+ remediation:
257
+ "Use `centrs settings --help` to inspect the supported subcommands and flags.",
258
+ });
259
+ const envelope = buildSettingsErrorEnvelope(
260
+ parsed?.subcommand ?? "print",
261
+ settingsFile,
262
+ [],
263
+ centrsError,
264
+ );
265
+ if (format === "json" || format === "yaml") {
266
+ // codeql[js/clear-text-logging] sink renders only a literal error string (e.g. "Missing value for --password."), never the real value; CodeQL over-taints via shared expectValue/CentrsError code.
267
+ console.error(renderSettingsEnvelope(envelope, format));
268
+ } else {
269
+ // codeql[js/clear-text-logging] sink renders only a literal error string (e.g. "Missing value for --password."), never the real value; CodeQL over-taints via shared expectValue/CentrsError code.
270
+ console.error(formatCentrsErrorText(centrsError));
271
+ }
272
+ return 1;
273
+ }
274
+ }
275
+
276
+ function inferSettingsFormat(args: readonly string[]): SettingsOutputFormat {
277
+ if (args.includes("--json")) {
278
+ return "json";
279
+ }
280
+ const formatIndex = args.indexOf("--format");
281
+ const value = formatIndex >= 0 ? args[formatIndex + 1] : undefined;
282
+ if (value && settingsOutputFormats.includes(value as SettingsOutputFormat)) {
283
+ return value as SettingsOutputFormat;
284
+ }
285
+ return "text";
286
+ }
@@ -6,7 +6,11 @@
6
6
  * `process.*`); the core relay in `../terminal.ts` stays io-agnostic.
7
7
  */
8
8
 
9
- import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
9
+ import {
10
+ asCentrsError,
11
+ CentrsError,
12
+ formatCentrsErrorText,
13
+ } from "../errors.ts";
10
14
  import {
11
15
  buildTerminalErrorEnvelope,
12
16
  describeCentrs,
@@ -19,6 +23,7 @@ import {
19
23
  type CliCommandMetadata,
20
24
  expectValue,
21
25
  renderCommandHelp,
26
+ unknownFlagError,
22
27
  } from "./common.ts";
23
28
  import {
24
29
  buildTargetSelectionTips,
@@ -28,6 +33,7 @@ import {
28
33
  missingTargetError,
29
34
  withTips,
30
35
  } from "./missing-target.ts";
36
+ import { selectionFlagTokens } from "./selection.ts";
31
37
 
32
38
  export const terminalCommand: CliCommandMetadata = {
33
39
  name: "terminal",
@@ -181,10 +187,12 @@ export async function runTerminalCli(args: readonly string[]): Promise<number> {
181
187
  if (format === "json" || format === "yaml") {
182
188
  const envelope = withTips(buildTerminalErrorEnvelope(error), tips);
183
189
  console.error(
190
+ // codeql[js/clear-text-logging] CLI error renderer false positive; password reaches neither the error nor the stderr renderers. Dismissed across all CLI commands.
184
191
  format === "yaml" ? toYaml(envelope) : JSON.stringify(envelope),
185
192
  );
186
193
  } else {
187
194
  console.error(
195
+ // codeql[js/clear-text-logging] Password parsed from args never reaches error.message in the text-format catch path; the parsed request (which holds the raw password) is never rendered.
188
196
  formatCentrsErrorText(
189
197
  asCentrsError(error, {
190
198
  code: "input/invalid-command",
@@ -205,7 +213,7 @@ interface ParsedTerminal {
205
213
  request: TerminalRequest;
206
214
  }
207
215
 
208
- function parseTerminalCliArgs(args: readonly string[]): ParsedTerminal {
216
+ export function parseTerminalCliArgs(args: readonly string[]): ParsedTerminal {
209
217
  const flags: TerminalRequest = {};
210
218
  const positional: string[] = [];
211
219
  let help = false;
@@ -268,14 +276,21 @@ function parseTerminalCliArgs(args: readonly string[]): ParsedTerminal {
268
276
  flags.verbose = true;
269
277
  break;
270
278
  default:
271
- if (arg.startsWith("-")) {
272
- throw asCentrsError(new Error(`Unknown terminal flag: ${arg}`), {
273
- code: "input/invalid-command",
274
- summary: `Unknown terminal flag: ${arg}`,
279
+ // A single interactive session cannot fan out: reject a target-selection
280
+ // flag with the shared `usage/fanout-not-supported` (mirrors `api --stream`).
281
+ if ((selectionFlagTokens as readonly string[]).includes(arg)) {
282
+ throw new CentrsError({
283
+ code: "usage/fanout-not-supported",
284
+ summary:
285
+ "`centrs terminal` is a single interactive session and cannot fan out across a multi-target selection.",
275
286
  remediation:
276
- "Remove the flag or run `centrs terminal --help` for the supported options.",
287
+ "Open a terminal to one router at a time; use `execute` / `api` for multi-target commands.",
288
+ context: { flag: arg, capability: "terminal" },
277
289
  });
278
290
  }
291
+ if (arg.startsWith("-")) {
292
+ throw unknownFlagError("terminal", arg, terminalCommand.options);
293
+ }
279
294
  positional.push(arg);
280
295
  break;
281
296
  }