@tikoci/centrs 0.1.3 → 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 (90) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/GLOSSARY.txt +213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -8
  5. package/commands/AGENTS.md +7 -1
  6. package/commands/api/README.md +19 -18
  7. package/commands/api/examples.md +15 -0
  8. package/commands/btest/README.md +21 -32
  9. package/commands/check/README.md +6 -1
  10. package/commands/discover/README.md +5 -11
  11. package/commands/execute/README.md +41 -29
  12. package/commands/execute/examples.md +41 -3
  13. package/commands/explain/README.md +1716 -0
  14. package/commands/explain/examples.md +606 -0
  15. package/commands/retrieve/README.md +25 -27
  16. package/commands/retrieve/examples.md +16 -0
  17. package/commands/transfer/README.md +28 -20
  18. package/commands/transfer/examples.md +29 -0
  19. package/docs/CLI.md +353 -0
  20. package/docs/CONSTITUTION.md +66 -15
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -0
  23. package/docs/errors/quickchr/machine-not-found.md +22 -0
  24. package/docs/errors/quickchr/machine-stopped.md +20 -0
  25. package/docs/errors/quickchr/package-unavailable.md +27 -0
  26. package/docs/errors/quickchr/unsupported-via.md +35 -0
  27. package/docs/errors/quickchr/unsupported.md +35 -0
  28. package/docs/errors/routeros/version-unsupported.md +26 -0
  29. package/docs/errors/transport/provider-trust.md +29 -0
  30. package/docs/errors/usage/stdin-ignored.md +28 -0
  31. package/docs/errors/validation/unknown-attribute.md +37 -4
  32. package/package.json +45 -10
  33. package/src/api-fanout.ts +29 -14
  34. package/src/api.ts +97 -54
  35. package/src/cli/api.ts +25 -6
  36. package/src/cli/btest.ts +18 -2
  37. package/src/cli/execute.ts +25 -6
  38. package/src/cli/explain.ts +503 -0
  39. package/src/cli/retrieve.ts +33 -1
  40. package/src/cli/selection.ts +71 -11
  41. package/src/cli/settings.ts +2 -2
  42. package/src/cli/transfer.ts +23 -6
  43. package/src/cli.ts +15 -2
  44. package/src/core/envelope.ts +5 -2
  45. package/src/core/error-catalog.ts +40 -0
  46. package/src/core/preflight.ts +306 -0
  47. package/src/core/routeros-string.ts +24 -0
  48. package/src/errors.ts +1 -0
  49. package/src/execute-fanout.ts +29 -14
  50. package/src/execute.ts +114 -63
  51. package/src/explain/arg-tokens.ts +114 -0
  52. package/src/explain/args.ts +1403 -0
  53. package/src/explain/blocks.ts +132 -0
  54. package/src/explain/brace-slots.ts +143 -0
  55. package/src/explain/brace-tokens.ts +38 -0
  56. package/src/explain/catalog.ts +1345 -0
  57. package/src/explain/coordinates.ts +329 -0
  58. package/src/explain/defects.ts +217 -0
  59. package/src/explain/is-known-menu.ts +17 -0
  60. package/src/explain/menus.ts +664 -0
  61. package/src/explain/operator-tokens.ts +310 -0
  62. package/src/explain/operators.ts +458 -0
  63. package/src/explain/path-tokens.ts +150 -0
  64. package/src/explain/pathresolve.ts +1324 -0
  65. package/src/explain/quoted-string.ts +241 -0
  66. package/src/explain/scope-brace.ts +308 -0
  67. package/src/explain/segment.ts +757 -0
  68. package/src/explain/string-tokens.ts +67 -0
  69. package/src/explain/symbol-values.ts +520 -0
  70. package/src/explain/symbols.ts +1597 -0
  71. package/src/explain/token-ranges.ts +72 -0
  72. package/src/explain/transport.ts +497 -0
  73. package/src/explain/value-tokens.ts +80 -0
  74. package/src/explain/values.ts +357 -0
  75. package/src/explain/verbs.ts +64 -0
  76. package/src/explain/verbsplit.ts +813 -0
  77. package/src/explain/write.ts +815 -0
  78. package/src/explain.ts +2152 -0
  79. package/src/index.ts +206 -4
  80. package/src/protocols/mac-telnet-console.ts +61 -17
  81. package/src/resolver/facts.ts +1 -1
  82. package/src/resolver/index.ts +23 -4
  83. package/src/resolver/quickchr-provider.ts +597 -0
  84. package/src/resolver/selection.ts +99 -12
  85. package/src/resolver/service-endpoint.ts +155 -0
  86. package/src/resolver/target.ts +1 -1
  87. package/src/retrieve-fanout.ts +26 -12
  88. package/src/retrieve.ts +81 -18
  89. package/src/transfer-fanout.ts +42 -25
  90. package/src/transfer.ts +87 -44
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Boundaries (see `commands/transfer/README.md`, `docs/CONSTITUTION.md`), shared
10
10
  * with `api`/`retrieve`/`execute` fan-out:
11
- * - The CDB is loaded ONCE (`expandCdbSelection`); a cdb member is resolved with
11
+ * - The CDB is loaded ONCE (`expandSelection`); a cdb member is resolved with
12
12
  * its pre-resolved record, so `resolveCdb`'s `__default__` synthetic fallback
13
13
  * never collides ad-hoc literals.
14
14
  * - A `--default` / `__default__` member fails that one target deterministically
@@ -42,12 +42,12 @@ import { CentrsError, serializeCentrsError } from "./errors.ts";
42
42
  import { promptForWriteConfirmation } from "./execute.ts";
43
43
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
44
44
  import {
45
- type CdbSelectionExpansion,
46
- type CdbSelectionMember,
47
- type CdbSelectionResolveInput,
48
- expandCdbSelection,
45
+ expandSelection,
49
46
  isDefaultRecordTarget,
50
47
  loadEnvFileDefaults,
48
+ type SelectionExpansion,
49
+ type SelectionMember,
50
+ type SelectionResolveInput,
51
51
  type TargetSelection,
52
52
  } from "./resolver/index.ts";
53
53
  import { toYaml } from "./retrieve.ts";
@@ -71,6 +71,8 @@ export interface TransferSelectionSummary {
71
71
  all: boolean;
72
72
  default: boolean;
73
73
  positionals: readonly string[];
74
+ /** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
75
+ quickchr?: readonly string[];
74
76
  }
75
77
 
76
78
  export interface TransferFanoutRequestSummary {
@@ -101,13 +103,13 @@ export type TransferFanoutErrorEnvelope =
101
103
  export interface TransferFanoutInternals {
102
104
  expand?: (
103
105
  selection: TargetSelection,
104
- input: CdbSelectionResolveInput,
106
+ input: SelectionResolveInput,
105
107
  env: Record<string, string | undefined>,
106
108
  config?: Record<string, string | undefined>,
107
- ) => Promise<CdbSelectionExpansion>;
109
+ ) => Promise<SelectionExpansion>;
108
110
  execute?: (
109
111
  request: TransferRequest,
110
- member: CdbSelectionMember,
112
+ member: SelectionMember,
111
113
  ) => Promise<TransferSuccessEnvelope>;
112
114
  sleep?: (ms: number) => Promise<void>;
113
115
  }
@@ -127,14 +129,18 @@ function summarizeSelection(
127
129
  all: selection.all,
128
130
  default: selection.default,
129
131
  positionals: selection.positionals,
132
+ quickchr: selection.quickchr,
130
133
  };
131
134
  }
132
135
 
133
- function memberLabel(member: CdbSelectionMember): string {
134
- return member.kind === "cdb" ? member.resolution.identity : member.input;
136
+ function memberLabel(member: SelectionMember): string {
137
+ if (member.kind === "cdb") {
138
+ return member.resolution.identity;
139
+ }
140
+ return member.kind === "quickchr" ? member.name : member.input;
135
141
  }
136
142
 
137
- function memberTargetMeta(member: CdbSelectionMember): {
143
+ function memberTargetMeta(member: SelectionMember): {
138
144
  input?: string;
139
145
  identity?: string;
140
146
  recordIndex?: number;
@@ -146,6 +152,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
146
152
  recordIndex: member.recordIndex,
147
153
  };
148
154
  }
155
+ if (member.kind === "quickchr") {
156
+ return { input: member.name, identity: member.name };
157
+ }
149
158
  return { input: member.input };
150
159
  }
151
160
 
@@ -165,11 +174,11 @@ function sanitizeFilename(label: string): string {
165
174
  function planDownloadPaths(
166
175
  outDir: string,
167
176
  remote: string,
168
- members: readonly CdbSelectionMember[],
169
- ): Map<CdbSelectionMember, string> {
177
+ members: readonly SelectionMember[],
178
+ ): Map<SelectionMember, string> {
170
179
  const ext = extname(basename(remote));
171
180
  const used = new Set<string>();
172
- const paths = new Map<CdbSelectionMember, string>();
181
+ const paths = new Map<SelectionMember, string>();
173
182
  members.forEach((member, index) => {
174
183
  const label = sanitizeFilename(memberLabel(member));
175
184
  let name = `${label}${ext}`;
@@ -197,7 +206,7 @@ function planDownloadPaths(
197
206
  */
198
207
  function relabelAdhocMember(
199
208
  envelope: TransferEnvelope,
200
- member: CdbSelectionMember,
209
+ member: SelectionMember,
201
210
  ): TransferEnvelope {
202
211
  if (member.kind !== "literal") {
203
212
  return envelope;
@@ -217,7 +226,7 @@ function relabelAdhocMember(
217
226
  */
218
227
  function applyMemberMeta(
219
228
  envelope: TransferEnvelope,
220
- member: CdbSelectionMember,
229
+ member: SelectionMember,
221
230
  ): TransferEnvelope {
222
231
  if (member.kind === "literal") {
223
232
  return relabelAdhocMember(envelope, member);
@@ -268,7 +277,7 @@ export async function transferFanout(
268
277
  });
269
278
  }
270
279
 
271
- const expand = internals.expand ?? expandCdbSelection;
280
+ const expand = internals.expand ?? expandSelection;
272
281
  const expansion = await expand(
273
282
  selection,
274
283
  {
@@ -310,7 +319,7 @@ export async function transferFanout(
310
319
 
311
320
  const execute =
312
321
  internals.execute ??
313
- ((req: TransferRequest, member: CdbSelectionMember) =>
322
+ ((req: TransferRequest, member: SelectionMember) =>
314
323
  transfer(req, env, {
315
324
  // cdb members carry their pre-resolved record (CDB loaded once);
316
325
  // literals fall through to single-target resolution.
@@ -319,8 +328,8 @@ export async function transferFanout(
319
328
  const sleep = internals.sleep ?? defaultFanoutSleep;
320
329
 
321
330
  const targets = await runFanout<
322
- CdbSelectionMember,
323
- { request: TransferRequest; member: CdbSelectionMember },
331
+ SelectionMember,
332
+ { request: TransferRequest; member: SelectionMember },
324
333
  TransferEnvelope
325
334
  >({
326
335
  members: expansion.targets,
@@ -339,9 +348,17 @@ export async function transferFanout(
339
348
  context: { target: member.resolution.target },
340
349
  });
341
350
  }
342
- const targetInput =
343
- member.kind === "cdb" ? member.resolution.target : member.input;
344
- const perRequest: TransferRequest = { ...request, targetInput };
351
+ // A quickchr member carries the machine name in `quickchr` (resolved live
352
+ // inside `resolveTransferRequest`, so a stopped machine is an inner
353
+ // per-target failure), never as a `targetInput`.
354
+ const perRequest: TransferRequest =
355
+ member.kind === "quickchr"
356
+ ? { ...request, targetInput: undefined, quickchr: member.name }
357
+ : {
358
+ ...request,
359
+ targetInput:
360
+ member.kind === "cdb" ? member.resolution.target : member.input,
361
+ };
345
362
  if (downloadPaths !== undefined) {
346
363
  perRequest.local = downloadPaths.get(member);
347
364
  }
@@ -377,7 +394,7 @@ export async function transferFanout(
377
394
 
378
395
  async function assertFanoutWriteConfirmed(
379
396
  request: TransferRequest,
380
- expansion: CdbSelectionExpansion,
397
+ expansion: SelectionExpansion,
381
398
  ): Promise<void> {
382
399
  if (request.yes) {
383
400
  return;
@@ -407,7 +424,7 @@ interface TransferFanoutEnvelopeInput {
407
424
  summary: FanoutSummary;
408
425
  requestSummary: TransferFanoutRequestSummary;
409
426
  via: RouterOsProtocol | string | null;
410
- warnings: CdbSelectionExpansion["warnings"];
427
+ warnings: SelectionExpansion["warnings"];
411
428
  targets: readonly TransferEnvelope[];
412
429
  }
413
430
 
package/src/transfer.ts CHANGED
@@ -38,15 +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,
42
43
  loadEnvFileDefaults,
43
44
  parseDuration,
45
+ quickchrConnection,
44
46
  type ResolvedAuth,
45
47
  type ResolvedSetting,
46
48
  type ResolvedTarget,
47
49
  resolveAuth,
48
50
  resolveBooleanSetting,
49
51
  resolveCdb,
52
+ resolveQuickchrTarget,
50
53
  resolveStringSetting,
51
54
  resolveTarget,
52
55
  toCoreSource,
@@ -104,6 +107,12 @@ export const FILE_READ_CHUNK_BYTES = 32_768;
104
107
  export interface TransferRequest {
105
108
  verb: TransferVerb;
106
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;
107
116
  /** Local path (or `-` for stdin/stdout). Present for upload/download. */
108
117
  local?: string;
109
118
  /** RouterOS file path. Present for upload/download/remove/mkdir and copy source. */
@@ -818,19 +827,31 @@ export async function resolveTransferRequest(
818
827
  config,
819
828
  );
820
829
 
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
+ }
821
841
  // 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
- ));
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
+ )));
834
855
 
835
856
  const format = resolveTransferFormat(request, env, config);
836
857
  const validate = resolveBooleanSetting(
@@ -842,15 +863,6 @@ export async function resolveTransferRequest(
842
863
  cdbResolution?.overrides.validate,
843
864
  config,
844
865
  );
845
- const insecure = resolveBooleanSetting(
846
- request.insecure,
847
- env,
848
- "CENTRS_INSECURE",
849
- false,
850
- "insecure",
851
- cdbResolution?.overrides.insecure,
852
- config,
853
- );
854
866
  const timeoutMs = resolveTransferTimeout(
855
867
  request.timeout,
856
868
  env,
@@ -858,30 +870,59 @@ export async function resolveTransferRequest(
858
870
  cdbResolution,
859
871
  config,
860
872
  );
861
- const target = resolveTarget(
862
- {
863
- targetInput: request.targetInput,
864
- host: request.host,
865
- port: request.port,
866
- },
867
- env,
868
- protocol,
869
- cdbResolution,
870
- config,
871
- );
872
- const auth = resolveAuth(
873
- {
874
- username: request.username,
875
- password: request.password,
876
- sshKey: request.sshKey,
877
- },
878
- env,
879
- cdbResolution,
880
- config,
881
- );
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
+ );
882
922
 
883
923
  const warnings: Warning[] = [
884
924
  ...((cdbResolution?.warnings ?? []) as readonly Warning[]),
925
+ ...(connection?.warnings ?? []),
885
926
  ];
886
927
  // Report the auto hop: a large upload cannot ride the REST family's 60 KB
887
928
  // `/file/set` cap, so auto-selection moved it to sftp.
@@ -892,8 +933,10 @@ export async function resolveTransferRequest(
892
933
  "Large upload (>60 KB) auto-selected sftp; the REST family cannot write past the 60 KB `/file/set` cap.",
893
934
  });
894
935
  }
895
- // A self-signed-accepting run is an anomaly worth surfacing on every transport.
896
- 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) {
897
940
  warnings.push({
898
941
  code: "transport/insecure-trust",
899
942
  message: