@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
@@ -54,7 +54,7 @@ export interface SelectionWhereClause {
54
54
  value: string;
55
55
  }
56
56
 
57
- /** The parsed target selection a command hands to {@link expandCdbSelection}. */
57
+ /** The parsed target selection a command hands to {@link expandSelection}. */
58
58
  export interface TargetSelection {
59
59
  /** Literal/lookup positionals (NOT operation args — the caller splits those). */
60
60
  positionals: readonly string[];
@@ -70,23 +70,41 @@ export interface TargetSelection {
70
70
  near?: NearPredicate;
71
71
  /** `--bbox <south>,<west>,<north>,<east>`: a geo union predicate (device GPS in box). */
72
72
  bbox?: BboxPredicate;
73
+ /**
74
+ * Repeatable `--quickchr <name>`: quickchr machine names (the named-live-
75
+ * provider, #134). Exclusive in v1 — a selection targets quickchr machines OR
76
+ * CDB/literals, never both; the CLI enforces the usage error before expansion
77
+ * and {@link expandSelection} re-asserts it.
78
+ */
79
+ quickchr?: readonly string[];
73
80
  }
74
81
 
75
- export interface CdbSelectionResolveInput {
82
+ export interface SelectionResolveInput {
76
83
  cdbFile?: string;
77
84
  cdbPassword?: string;
78
85
  /** CLI: true (literal positionals allowed). MCP: false (CDB is the allowlist). */
79
86
  allowAdhoc: boolean;
80
87
  }
81
88
 
82
- /** One resolved selection member: a CDB record or an ad-hoc literal target. */
83
- export type CdbSelectionMember =
89
+ /**
90
+ * One resolved selection member: a CDB record or an ad-hoc literal target.
91
+ *
92
+ * This is the connection-fact origin ("Axis B") the fan-out callers consume. The
93
+ * selector-source (CDB) and literal arms carry a single endpoint materialized
94
+ * later by `./target.ts`. The `quickchr` arm is the named-live-provider (#134):
95
+ * it carries only the machine NAME here — the live descriptor is resolved in the
96
+ * per-member fan-out `resolve()` path (via `resolveQuickchrTarget`), so a
97
+ * stopped/unknown machine is an inner per-target failure, not an expansion
98
+ * error — see `docs/CONSTITUTION.md` → Resolution providers.
99
+ */
100
+ export type SelectionMember =
84
101
  | { kind: "cdb"; resolution: CdbResolution; recordIndex: number }
85
- | { kind: "literal"; input: string };
102
+ | { kind: "literal"; input: string }
103
+ | { kind: "quickchr"; name: string };
86
104
 
87
- export interface CdbSelectionExpansion {
105
+ export interface SelectionExpansion {
88
106
  /** Members ordered by record index; literals appended in positional order. */
89
- targets: readonly CdbSelectionMember[];
107
+ targets: readonly SelectionMember[];
90
108
  warnings: readonly ResolverWarning[];
91
109
  /** True when nothing matched (an empty/unknown selection is not an error). */
92
110
  empty: boolean;
@@ -113,12 +131,16 @@ const ENV_CDB_PASSWORD = "CENTRS_CDB_PASSWORD";
113
131
  * literal host when no CDB is present). An explicit `--cdb-file` that is missing
114
132
  * still throws.
115
133
  */
116
- export async function expandCdbSelection(
134
+ export async function expandSelection(
117
135
  selection: TargetSelection,
118
- input: CdbSelectionResolveInput,
136
+ input: SelectionResolveInput,
119
137
  env: Record<string, string | undefined>,
120
138
  config: Record<string, string | undefined> = {},
121
- ): Promise<CdbSelectionExpansion> {
139
+ ): Promise<SelectionExpansion> {
140
+ const quickchrNames = selection.quickchr ?? [];
141
+ if (quickchrNames.length > 0) {
142
+ return expandQuickchrSelection(selection, input, quickchrNames);
143
+ }
122
144
  const settings = resolveDevicesSettings({
123
145
  cdbFile: input.cdbFile,
124
146
  cdbPassword: input.cdbPassword,
@@ -224,7 +246,7 @@ export async function expandCdbSelection(
224
246
  }
225
247
  }
226
248
 
227
- const literals: CdbSelectionMember[] = [];
249
+ const literals: SelectionMember[] = [];
228
250
  const seenLiteral = new Set<string>();
229
251
  for (const positional of selection.positionals) {
230
252
  try {
@@ -255,7 +277,7 @@ export async function expandCdbSelection(
255
277
  }
256
278
  }
257
279
 
258
- const cdbMembers: CdbSelectionMember[] = [...indices]
280
+ const cdbMembers: SelectionMember[] = [...indices]
259
281
  .sort((a, b) => a - b)
260
282
  .map((index) => ({
261
283
  kind: "cdb",
@@ -302,3 +324,68 @@ export async function expandCdbSelection(
302
324
 
303
325
  return { targets, warnings, empty: false };
304
326
  }
327
+
328
+ /**
329
+ * Expand a quickchr-only selection. No CDB is loaded — a named-live-provider
330
+ * member never touches the registry, the `__default__` ladder, or the selector
331
+ * grammar. Members carry only the machine name (de-duped by exact name, flag
332
+ * order preserved); the live descriptor resolves per member inside the fan-out.
333
+ *
334
+ * Two guards are load-bearing:
335
+ * - Exclusivity (v1): mixing `--quickchr` with selectors/positionals is a
336
+ * usage error. The CLI throws first with flag-level wording; this re-assert
337
+ * protects library/MCP callers that build a {@link TargetSelection} directly.
338
+ * - `allowAdhoc: false` (the MCP surface) rejects quickchr members entirely:
339
+ * the CDB is the MCP allowlist, and a live provider must not widen it (#134
340
+ * non-goal).
341
+ */
342
+ function expandQuickchrSelection(
343
+ selection: TargetSelection,
344
+ input: SelectionResolveInput,
345
+ names: readonly string[],
346
+ ): SelectionExpansion {
347
+ if (!input.allowAdhoc) {
348
+ throw new CentrsError({
349
+ code: "cdb/target-not-registered",
350
+ summary:
351
+ "quickchr targets are not available on this surface; the CDB is the allowlist.",
352
+ remediation:
353
+ "Register the device with `centrs devices add` and select it by identity, or use the CLI for `--quickchr` targets.",
354
+ context: { quickchr: [...names] },
355
+ });
356
+ }
357
+ if (
358
+ selection.positionals.length > 0 ||
359
+ selection.groups.length > 0 ||
360
+ selection.all ||
361
+ selection.default ||
362
+ selection.where.length > 0 ||
363
+ selection.near !== undefined ||
364
+ selection.bbox !== undefined
365
+ ) {
366
+ throw new CentrsError({
367
+ code: "usage/conflicting-flags",
368
+ summary:
369
+ "`--quickchr` is exclusive: a command targets quickchr machines or CDB/literal targets, not both.",
370
+ remediation:
371
+ "Drop the positional/selector (or the `--quickchr` flag) so the selection has one target source.",
372
+ context: {
373
+ quickchr: [...names],
374
+ positionals: selection.positionals.length,
375
+ groups: selection.groups.length,
376
+ all: selection.all,
377
+ default: selection.default,
378
+ where: selection.where.length,
379
+ },
380
+ });
381
+ }
382
+ const seen = new Set<string>();
383
+ const targets: SelectionMember[] = [];
384
+ for (const name of names) {
385
+ if (!seen.has(name)) {
386
+ seen.add(name);
387
+ targets.push({ kind: "quickchr", name });
388
+ }
389
+ }
390
+ return { targets, warnings: [], empty: false };
391
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The neutral per-service endpoint shape — the connection-fact origin ("Axis B")
3
+ * for a resolved target, decoupled from *how* the member was selected ("Axis A").
4
+ *
5
+ * This is the load-bearing scaffold for the resolution-provider refactor
6
+ * (`docs/CONSTITUTION.md` → Resolution providers; centrs #174). Two provider
7
+ * kinds feed it:
8
+ *
9
+ * - **Selector-source** — participates in the `--group`/`--where`/`--all`/
10
+ * `--near`/`--bbox` selector grammar. **CDB** today; **TikTOML** next (#137).
11
+ * CDB is the *degenerate single-endpoint* case: one record yields one
12
+ * host/port/auth, materialized lazily by `./target.ts` once `--via` is known,
13
+ * so a CDB member never needs a populated {@link ServiceEndpointMap}.
14
+ * - **Named-live-provider** — addressed only by an explicit named flag, never
15
+ * matched by selectors, fanned out by repeating the flag; owns an ephemeral
16
+ * endpoint so it also bypasses the settings/`__default__` ladder and conflicts
17
+ * with `--host`/`--port`/`--username`/`--password`/`--ssh-key`. **quickchr**
18
+ * (`--quickchr <name>`, #134). A live provider resolves a member into a
19
+ * *populated* per-service map, because it genuinely exposes different
20
+ * host/port/auth per transport (e.g. a SLiRP-forwarded loopback port per
21
+ * service). Flattening that to a single host/port is the exact refactor #174
22
+ * exists to prevent — TikTOML's `services` block is per-service too.
23
+ *
24
+ * The shape here deliberately mirrors quickchr's published descriptor contract
25
+ * (`tikoci/quickchr` `docs/centrs-interface.md`, `ServiceEndpoint`/
26
+ * `SshServiceEndpoint`), which states that centrs reuses *this same internal
27
+ * type* for TikTOML resolution. It is provider-neutral: `source.provider` is a
28
+ * free string, not a quickchr literal. Mapping a resolved endpoint's auth onto
29
+ * centrs' {@link ../resolver/target.ts | ResolvedAuth} (notably
30
+ * `privateKeyPath` → `sshKey`) is the provider adapter's job, not this type's.
31
+ *
32
+ * Phase 1 (this file) defines the contract only; the quickchr provider adapter
33
+ * that *produces* a map, and the `--via` consumer that *reads* `map[via]`, land
34
+ * with the `--quickchr` plumbing (#134 Phase 2+).
35
+ */
36
+
37
+ import type { RouterOsProtocol } from "../protocols/index.ts";
38
+
39
+ /** Provenance for a resolved endpoint — which provider vouched for it. */
40
+ export interface ServiceEndpointSource {
41
+ /** Provider identity, e.g. `"quickchr"` or `"tiktoml"`. Neutral by design. */
42
+ provider: string;
43
+ /** Provider-internal forward name backing the service (e.g. `"api-ssl"`), for provenance. */
44
+ portMappingName?: string;
45
+ }
46
+
47
+ /** REST/native-api auth. RouterOS shares one credential across REST and native. */
48
+ export interface ServiceEndpointAuth {
49
+ username: string;
50
+ password?: string;
51
+ /** Pre-encoded HTTP Basic value, when the provider supplies one directly. */
52
+ basic?: string;
53
+ /** Pre-formed auth header value, when the provider supplies one directly. */
54
+ header?: string;
55
+ }
56
+
57
+ /** SSH auth. Same credential vocabulary `+ key path`, plus batch-mode capability. */
58
+ export interface SshEndpointAuth {
59
+ username: string;
60
+ /**
61
+ * Path to a usable private key. Populated only when the provider has actually
62
+ * verified a batch (non-interactive) login with it — maps to
63
+ * {@link ../resolver/target.ts | ResolvedAuth.sshKey} in the provider adapter.
64
+ */
65
+ privateKeyPath?: string;
66
+ /** Every auth mode the endpoint accepts. */
67
+ modes: Array<"private-key" | "agent-or-config" | "password">;
68
+ /**
69
+ * The batch-capable subset. This is the gate centrs enforces for `--via ssh`
70
+ * and `transfer --via sftp`: an empty list means no non-interactive handoff is
71
+ * possible, so centrs must emit a typed unsupported-capability error rather
72
+ * than prompt for a password or silently fall back (#134 Phase 4).
73
+ */
74
+ batchModes: Array<"private-key" | "agent-or-config">;
75
+ passwordAvailable?: boolean;
76
+ }
77
+
78
+ /** Fields shared by every available endpoint regardless of auth flavor. */
79
+ interface AvailableEndpointBase {
80
+ available: true;
81
+ /** Hostname/IP to dial — NOT a port. `"127.0.0.1"` for a SLiRP-forwarded service. */
82
+ host: string;
83
+ port: number;
84
+ guestPort?: number;
85
+ transport: "tcp" | "udp";
86
+ /** True for TLS-backed endpoints (https / api-ssl); SSH is always `false` (own transport security). */
87
+ tls: boolean;
88
+ url?: string;
89
+ source?: ServiceEndpointSource;
90
+ }
91
+
92
+ /** An endpoint the provider could not vouch for; a consumer MUST gate on `available` before dialing. */
93
+ export interface UnavailableEndpoint {
94
+ available: false;
95
+ unavailableReason: string;
96
+ /** Best-effort echoes only — never dial these without a re-resolve. */
97
+ host?: string;
98
+ port?: number;
99
+ guestPort?: number;
100
+ transport?: "tcp" | "udp";
101
+ tls?: boolean;
102
+ url?: string;
103
+ source?: ServiceEndpointSource;
104
+ }
105
+
106
+ /** One per-service endpoint (rest-api / native-api). */
107
+ export type ServiceEndpoint =
108
+ | (AvailableEndpointBase & { auth?: ServiceEndpointAuth })
109
+ | UnavailableEndpoint;
110
+
111
+ /** The SSH endpoint — same union, with SSH-specific auth substituted. */
112
+ export type SshServiceEndpoint =
113
+ | (AvailableEndpointBase & { auth: SshEndpointAuth })
114
+ | UnavailableEndpoint;
115
+
116
+ /** Any resolved endpoint, whatever the service. */
117
+ export type AnyServiceEndpoint = ServiceEndpoint | SshServiceEndpoint;
118
+
119
+ /**
120
+ * The endpoint type for a given `--via` id. `ssh` carries the SSH-specific
121
+ * {@link SshServiceEndpoint} (its available form guarantees `auth: SshEndpointAuth`
122
+ * with `batchModes`, the gate Phase 4 enforces for `--via ssh`/`transfer --via
123
+ * sftp`); every other transport uses the shared credential {@link ServiceEndpoint}.
124
+ */
125
+ export type ServiceEndpointFor<K extends RouterOsProtocol> = K extends "ssh"
126
+ ? SshServiceEndpoint
127
+ : ServiceEndpoint;
128
+
129
+ /**
130
+ * A member's per-service connection facts, keyed by centrs `--via` id.
131
+ *
132
+ * Partial by design: a provider may cover only a subset of the eight protocols
133
+ * (quickchr v1 covers `rest-api`/`native-api`/`ssh`). A `--via` whose key is
134
+ * absent is a typed unsupported-provider error at consume time, not a silent
135
+ * fallback. CDB never populates this map (its single endpoint is materialized by
136
+ * `./target.ts`); only named-live-providers do.
137
+ *
138
+ * Keyed by service so per-`--via` precision survives: `map.ssh` is a
139
+ * {@link SshServiceEndpoint}, not a widened `AnyServiceEndpoint`, so a consumer
140
+ * reading `map.ssh` keeps the SSH `auth`/`batchModes` guarantee without a cast.
141
+ */
142
+ export type ServiceEndpointMap = {
143
+ [K in RouterOsProtocol]?: ServiceEndpointFor<K>;
144
+ };
145
+
146
+ /**
147
+ * Narrow an endpoint to its available form (the only form safe to dial). Generic
148
+ * so the caller's precise union is preserved: an {@link SshServiceEndpoint} narrows
149
+ * to its available arm (`auth: SshEndpointAuth` required), not the widened base.
150
+ */
151
+ export function isEndpointAvailable<T extends AnyServiceEndpoint>(
152
+ endpoint: T,
153
+ ): endpoint is Extract<T, { available: true }> {
154
+ return endpoint.available;
155
+ }
@@ -455,6 +455,6 @@ export function defaultPortForScheme(scheme: "http" | "https"): number {
455
455
  return scheme === "https" ? 443 : 80;
456
456
  }
457
457
 
458
- function formatHostForUrl(host: string): string {
458
+ export function formatHostForUrl(host: string): string {
459
459
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
460
460
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Boundaries (see `commands/retrieve/README.md`, `docs/CONSTITUTION.md`), shared
14
14
  * with `api-fanout.ts`:
15
- * - The CDB is loaded + decrypted ONCE (`expandCdbSelection`); a cdb member is
15
+ * - The CDB is loaded + decrypted ONCE (`expandSelection`); a cdb member is
16
16
  * resolved with its pre-resolved record, so `resolveCdb`'s `__default__`
17
17
  * synthetic fallback never collides ad-hoc literals.
18
18
  * - A `--default` / `__default__` member is guarded from being dialed as the
@@ -46,15 +46,16 @@ import {
46
46
  runFanout,
47
47
  summarizeFanout,
48
48
  } from "./core/fanout.ts";
49
+ import { toYaml } from "./core/yaml.ts";
49
50
  import { CentrsError, serializeCentrsError } from "./errors.ts";
50
51
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
51
52
  import {
52
- type CdbSelectionExpansion,
53
- type CdbSelectionMember,
54
- type CdbSelectionResolveInput,
55
- expandCdbSelection,
53
+ expandSelection,
56
54
  isDefaultRecordTarget,
57
55
  loadEnvFileDefaults,
56
+ type SelectionExpansion,
57
+ type SelectionMember,
58
+ type SelectionResolveInput,
58
59
  type TargetSelection,
59
60
  } from "./resolver/index.ts";
60
61
  import {
@@ -71,7 +72,6 @@ import {
71
72
  resolveRetrieveGlobalContext,
72
73
  resolveRetrieveRequest,
73
74
  runResolvedRetrieve,
74
- toYaml,
75
75
  validateRetrieveRequestShape,
76
76
  } from "./retrieve.ts";
77
77
 
@@ -82,6 +82,8 @@ export interface RetrieveSelectionSummary {
82
82
  all: boolean;
83
83
  default: boolean;
84
84
  positionals: readonly string[];
85
+ /** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
86
+ quickchr?: readonly string[];
85
87
  }
86
88
 
87
89
  export interface RetrieveFanoutOperationMeta {
@@ -109,10 +111,10 @@ export interface RetrieveFanoutInternals {
109
111
  /** Selection expansion. Defaults to the real CDB-backed expansion. */
110
112
  expand?: (
111
113
  selection: TargetSelection,
112
- input: CdbSelectionResolveInput,
114
+ input: SelectionResolveInput,
113
115
  env: Record<string, string | undefined>,
114
116
  config?: Record<string, string | undefined>,
115
- ) => Promise<CdbSelectionExpansion>;
117
+ ) => Promise<SelectionExpansion>;
116
118
  /** Backoff sleeper. Defaults to a real timer; tests pass a no-op. */
117
119
  sleep?: (ms: number) => Promise<void>;
118
120
  }
@@ -132,10 +134,11 @@ function summarizeSelection(
132
134
  all: selection.all,
133
135
  default: selection.default,
134
136
  positionals: selection.positionals,
137
+ quickchr: selection.quickchr,
135
138
  };
136
139
  }
137
140
 
138
- function memberTargetMeta(member: CdbSelectionMember): {
141
+ function memberTargetMeta(member: SelectionMember): {
139
142
  input?: string;
140
143
  identity?: string;
141
144
  recordIndex?: number;
@@ -147,6 +150,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
147
150
  recordIndex: member.recordIndex,
148
151
  };
149
152
  }
153
+ if (member.kind === "quickchr") {
154
+ return { input: member.name, identity: member.name };
155
+ }
150
156
  return { input: member.input };
151
157
  }
152
158
 
@@ -159,7 +165,7 @@ function memberTargetMeta(member: CdbSelectionMember): {
159
165
  */
160
166
  function relabelAdhocMember(
161
167
  envelope: RetrieveEnvelope,
162
- member: CdbSelectionMember,
168
+ member: SelectionMember,
163
169
  ): RetrieveEnvelope {
164
170
  if (member.kind !== "literal") {
165
171
  return envelope;
@@ -194,7 +200,7 @@ export async function retrieveFanout(
194
200
  );
195
201
  const selectionSummary = summarizeSelection(selection);
196
202
 
197
- const expand = internals.expand ?? expandCdbSelection;
203
+ const expand = internals.expand ?? expandSelection;
198
204
  const expansion = await expand(
199
205
  selection,
200
206
  {
@@ -234,7 +240,7 @@ export async function retrieveFanout(
234
240
  const sleep = internals.sleep ?? defaultFanoutSleep;
235
241
 
236
242
  const targets = await runFanout<
237
- CdbSelectionMember,
243
+ SelectionMember,
238
244
  ResolvedRetrieveRequest,
239
245
  RetrieveEnvelope
240
246
  >({
@@ -267,6 +273,14 @@ export async function retrieveFanout(
267
273
  config,
268
274
  );
269
275
  }
276
+ if (member.kind === "quickchr") {
277
+ // The live descriptor resolves here, per member — a stopped/unknown
278
+ // machine is an inner per-target failure, not an expansion error.
279
+ return resolveRetrieveRequest(
280
+ { ...request, targetInput: undefined, quickchr: member.name },
281
+ env,
282
+ );
283
+ }
270
284
  // Ad-hoc literal: full single-target resolution (CDB load → `__default__`
271
285
  // fallback creds), mirroring single-target `retrieve`.
272
286
  return resolveRetrieveRequest(
@@ -373,7 +387,7 @@ function resolveFanoutConcurrencyProtocol(
373
387
  request: RetrieveRequest,
374
388
  env: Record<string, string | undefined>,
375
389
  globalVia: RouterOsProtocol,
376
- expansion: CdbSelectionExpansion,
390
+ expansion: SelectionExpansion,
377
391
  ): RouterOsProtocol {
378
392
  // Deliberately CLI/env only, not `config` — a `centrs.env` default is the
379
393
  // weakest precedence tier and must not be treated as a hard pin the way an