@tikoci/centrs 0.1.3 → 0.1.6

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 (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
package/src/retrieve.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  isCommandNode,
16
16
  pathTokens,
17
17
  } from "./core/inspect.ts";
18
+ import { toYaml } from "./core/yaml.ts";
18
19
  import { CentrsError, serializeCentrsError } from "./errors.ts";
19
20
  import {
20
21
  createProtocolAdapter,
@@ -26,11 +27,14 @@ import {
26
27
  type RouterOsProtocol,
27
28
  } from "./protocols/index.ts";
28
29
  import {
30
+ assertNoQuickchrOverrideConflict,
29
31
  type CdbResolution,
30
32
  isIpTransport,
31
33
  loadEnvFileDefaults,
32
34
  parseDuration,
33
35
  parseResolvePolicy,
36
+ type QuickchrResolution,
37
+ quickchrConnection,
34
38
  type ResolvedAuth,
35
39
  type ResolvedSetting,
36
40
  type ResolvedTarget,
@@ -39,6 +43,7 @@ import {
39
43
  resolveCdb,
40
44
  resolveMacTarget,
41
45
  resolveOptionalIntegerSetting,
46
+ resolveQuickchrTarget,
42
47
  resolveStringSetting,
43
48
  resolveTarget,
44
49
  toCoreSource,
@@ -49,6 +54,12 @@ export type RetrieveOutputFormat = (typeof retrieveOutputFormats)[number];
49
54
 
50
55
  export interface RetrieveRequest {
51
56
  targetInput?: string;
57
+ /**
58
+ * quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
59
+ * the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
60
+ * Resolution providers). Conflicts with host/port/username/password.
61
+ */
62
+ quickchr?: string;
52
63
  path: string;
53
64
  via?: string;
54
65
  host?: string;
@@ -123,6 +134,14 @@ export interface ResolvedRetrieveRequest {
123
134
  timeoutMs: ResolvedSetting<number>;
124
135
  format: ResolvedSetting<RetrieveOutputFormat>;
125
136
  validate: ResolvedSetting<boolean>;
137
+ /**
138
+ * Relax TLS peer verification. retrieve has no `--insecure` flag; this is set
139
+ * only by the quickchr provider (trust-by-provenance for the VM's self-signed
140
+ * TLS forward — see `src/resolver/quickchr-provider.ts`), so when present its
141
+ * source is always `provider` and it is surfaced as `meta.settings.insecure`
142
+ * for auditability.
143
+ */
144
+ insecure?: ResolvedSetting<boolean>;
126
145
  maxResultsBytes?: ResolvedSetting<number>;
127
146
  attributes: readonly string[];
128
147
  allAttributes: boolean;
@@ -161,6 +180,7 @@ export async function runResolvedRetrieve(
161
180
  username: resolved.auth.username,
162
181
  password: resolved.auth.password,
163
182
  timeoutMs: resolved.timeoutMs.value,
183
+ insecure: resolved.insecure?.value,
164
184
  });
165
185
 
166
186
  try {
@@ -212,7 +232,8 @@ export async function runResolvedRetrieve(
212
232
  "Check the attribute name, or use `--list-attributes` to inspect the available properties first.",
213
233
  context: {
214
234
  path: resolved.path,
215
- requested: resolved.attributes,
235
+ parameter: missing[0],
236
+ requestedAttributes: resolved.attributes,
216
237
  availableAttributes,
217
238
  },
218
239
  });
@@ -462,6 +483,9 @@ function metaFromResolved(
462
483
  timeoutMs: toCoreSource(resolved.timeoutMs.source),
463
484
  format: toCoreSource(resolved.format.source),
464
485
  validate: toCoreSource(resolved.validate.source),
486
+ insecure: resolved.insecure
487
+ ? toCoreSource(resolved.insecure.source)
488
+ : undefined,
465
489
  maxResultsBytes: resolved.maxResultsBytes
466
490
  ? toCoreSource(resolved.maxResultsBytes.source)
467
491
  : undefined,
@@ -620,6 +644,7 @@ export function buildResolvedRetrieve(
620
644
  attributeSelections: readonly string[],
621
645
  macResolution?: { mac: string; ip: string },
622
646
  config: Record<string, string | undefined> = {},
647
+ quickchrResolution?: QuickchrResolution,
623
648
  ): ResolvedRetrieveRequest {
624
649
  const via = resolveProtocol(request, env, cdbResolution, config);
625
650
  const format = resolveFormat(request, env, config);
@@ -647,22 +672,33 @@ export function buildResolvedRetrieve(
647
672
  undefined,
648
673
  config,
649
674
  );
650
- const target = resolveTarget(
651
- {
652
- targetInput: request.targetInput,
653
- host: request.host,
654
- port: request.port,
655
- macResolution,
656
- },
657
- env,
658
- via.value,
659
- cdbResolution,
660
- );
661
- const auth = resolveAuth(
662
- { username: request.username, password: request.password },
663
- env,
664
- cdbResolution,
665
- );
675
+ // A quickchr member substitutes the live descriptor's per-`--via` endpoint
676
+ // for the CDB/env target+auth path (`docs/CONSTITUTION.md` → Resolution
677
+ // providers); everything else (via/format/validate/timeout) resolved above
678
+ // stays on the normal ladder.
679
+ const connection = quickchrResolution
680
+ ? quickchrConnection(quickchrResolution, via.value)
681
+ : null;
682
+ const target = connection
683
+ ? connection.target
684
+ : resolveTarget(
685
+ {
686
+ targetInput: request.targetInput,
687
+ host: request.host,
688
+ port: request.port,
689
+ macResolution,
690
+ },
691
+ env,
692
+ via.value,
693
+ cdbResolution,
694
+ );
695
+ const auth = connection
696
+ ? connection.auth
697
+ : resolveAuth(
698
+ { username: request.username, password: request.password },
699
+ env,
700
+ cdbResolution,
701
+ );
666
702
 
667
703
  return {
668
704
  path: request.path,
@@ -672,12 +708,24 @@ export function buildResolvedRetrieve(
672
708
  timeoutMs,
673
709
  format,
674
710
  validate,
711
+ insecure:
712
+ connection?.insecure === true && quickchrResolution
713
+ ? {
714
+ value: true,
715
+ source: {
716
+ kind: "provider" as const,
717
+ key: `quickchr:${quickchrResolution.name}`,
718
+ },
719
+ }
720
+ : undefined,
675
721
  maxResultsBytes,
676
722
  attributes: attributeSelections,
677
723
  allAttributes: request.allAttributes ?? false,
678
724
  listAttributes: request.listAttributes ?? false,
679
725
  verbose: request.verbose ?? false,
680
- warnings: cdbResolution?.warnings ?? [],
726
+ warnings: connection
727
+ ? [...connection.warnings]
728
+ : (cdbResolution?.warnings ?? []),
681
729
  };
682
730
  }
683
731
 
@@ -758,6 +806,22 @@ export async function resolveRetrieveRequest(
758
806
  const attributeSelections = validateRetrieveRequestShape(request);
759
807
  const config = await loadEnvFileDefaults(env);
760
808
 
809
+ // A quickchr target bypasses CDB and MAC resolution entirely — the live
810
+ // descriptor is the only connection-fact source for those fields.
811
+ if (request.quickchr !== undefined) {
812
+ assertNoQuickchrOverrideConflict(request, request.quickchr);
813
+ const quickchrResolution = await resolveQuickchrTarget(request.quickchr);
814
+ return buildResolvedRetrieve(
815
+ request,
816
+ env,
817
+ undefined,
818
+ attributeSelections,
819
+ undefined,
820
+ config,
821
+ quickchrResolution,
822
+ );
823
+ }
824
+
761
825
  const cdbResolution = await resolveCdb(
762
826
  {
763
827
  targetInput: request.targetInput,
@@ -1129,62 +1193,3 @@ function exhaustiveOutputFormat(value: never): never {
1129
1193
  function isPlainObject(value: unknown): value is Record<string, unknown> {
1130
1194
  return typeof value === "object" && value !== null && !Array.isArray(value);
1131
1195
  }
1132
-
1133
- export function toYaml(value: unknown, indent = 0): string {
1134
- const prefix = " ".repeat(indent);
1135
- if (value === null || value === undefined) {
1136
- return "null";
1137
- }
1138
- if (typeof value === "string") {
1139
- return yamlScalar(value);
1140
- }
1141
- if (typeof value === "number" || typeof value === "boolean") {
1142
- return String(value);
1143
- }
1144
- if (Array.isArray(value)) {
1145
- if (value.length === 0) {
1146
- return `${prefix}[]`;
1147
- }
1148
- return value
1149
- .map((item) => {
1150
- if (isScalar(item)) {
1151
- return `${prefix}- ${toYaml(item, indent + 2)}`;
1152
- }
1153
- return `${prefix}-\n${toYaml(item, indent + 2)}`;
1154
- })
1155
- .join("\n");
1156
- }
1157
- if (isPlainObject(value)) {
1158
- const entries = Object.entries(value).filter(
1159
- ([, entryValue]) => entryValue !== undefined,
1160
- );
1161
- if (entries.length === 0) {
1162
- return `${prefix}{}`;
1163
- }
1164
- return entries
1165
- .map(([key, entryValue]) => {
1166
- if (isScalar(entryValue)) {
1167
- return `${prefix}${key}: ${toYaml(entryValue, indent + 2)}`;
1168
- }
1169
- return `${prefix}${key}:\n${toYaml(entryValue, indent + 2)}`;
1170
- })
1171
- .join("\n");
1172
- }
1173
- return yamlScalar(JSON.stringify(value));
1174
- }
1175
-
1176
- function isScalar(value: unknown): value is boolean | number | string | null {
1177
- return (
1178
- value === null ||
1179
- typeof value === "string" ||
1180
- typeof value === "number" ||
1181
- typeof value === "boolean"
1182
- );
1183
- }
1184
-
1185
- function yamlScalar(value: string): string {
1186
- if (value.length === 0) {
1187
- return '""';
1188
- }
1189
- return JSON.stringify(value);
1190
- }
package/src/settings.ts CHANGED
@@ -24,6 +24,7 @@ import type {
24
24
  Warning,
25
25
  } from "./core/envelope.ts";
26
26
  import { buildTip } from "./core/envelope.ts";
27
+ import { toYaml } from "./core/yaml.ts";
27
28
  import { loadCdb, resolveDevicesSettings } from "./devices.ts";
28
29
  import { CentrsError, serializeCentrsError } from "./errors.ts";
29
30
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
@@ -37,7 +38,7 @@ import {
37
38
  readSettingsFileRaw,
38
39
  resolveOptionalIntegerSetting,
39
40
  } from "./resolver/index.ts";
40
- import { retrieveOutputFormats, toYaml } from "./retrieve.ts";
41
+ import { retrieveOutputFormats } from "./retrieve.ts";
41
42
 
42
43
  // ── Envelope shape ───────────────────────────────────────────────────────────
43
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
@@ -38,19 +38,19 @@ import {
38
38
  runFanout,
39
39
  summarizeFanout,
40
40
  } from "./core/fanout.ts";
41
+ import { toYaml } from "./core/yaml.ts";
41
42
  import { CentrsError, serializeCentrsError } from "./errors.ts";
42
43
  import { promptForWriteConfirmation } from "./execute.ts";
43
44
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
44
45
  import {
45
- type CdbSelectionExpansion,
46
- type CdbSelectionMember,
47
- type CdbSelectionResolveInput,
48
- expandCdbSelection,
46
+ expandSelection,
49
47
  isDefaultRecordTarget,
50
48
  loadEnvFileDefaults,
49
+ type SelectionExpansion,
50
+ type SelectionMember,
51
+ type SelectionResolveInput,
51
52
  type TargetSelection,
52
53
  } from "./resolver/index.ts";
53
- import { toYaml } from "./retrieve.ts";
54
54
  import {
55
55
  buildTransferErrorEnvelope,
56
56
  isTransferWriteVerb,
@@ -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
@@ -29,6 +29,7 @@ import type {
29
29
  EnvelopeValidationMeta,
30
30
  Warning,
31
31
  } from "./core/envelope.ts";
32
+ import { toYaml } from "./core/yaml.ts";
32
33
  import { CentrsError, serializeCentrsError } from "./errors.ts";
33
34
  import {
34
35
  createProtocolAdapter,
@@ -38,20 +39,22 @@ import {
38
39
  import type { RouterOsProtocol } from "./protocols/index.ts";
39
40
  import { SftpClient } from "./protocols/sftp.ts";
40
41
  import {
42
+ assertNoQuickchrOverrideConflict,
41
43
  type CdbResolution,
42
44
  loadEnvFileDefaults,
43
45
  parseDuration,
46
+ quickchrConnection,
44
47
  type ResolvedAuth,
45
48
  type ResolvedSetting,
46
49
  type ResolvedTarget,
47
50
  resolveAuth,
48
51
  resolveBooleanSetting,
49
52
  resolveCdb,
53
+ resolveQuickchrTarget,
50
54
  resolveStringSetting,
51
55
  resolveTarget,
52
56
  toCoreSource,
53
57
  } from "./resolver/index.ts";
54
- import { toYaml } from "./retrieve.ts";
55
58
 
56
59
  export const transferOutputFormats = ["text", "json", "yaml"] as const;
57
60
  export type TransferOutputFormat = (typeof transferOutputFormats)[number];
@@ -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: