@hediet/linkrpc-cli 0.0.1

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.
@@ -0,0 +1,3949 @@
1
+ import { ErrorCode, InMemoryManagedIdentity, JsonRpcChannel, RpcError, SigningSender, TransportPair, capabilityFreshAt, computeInterfaceHash, defineInterface, isAssignable, permissionMatchesTarget, requestType, traceMessageTransport } from "@hediet/linkrpc";
2
+ import { z } from "zod";
3
+ import { LINKRPC_ENDPOINT_VAR, LINKRPC_TOKEN_VAR, WebSocketTransport, connectNdjson, createManagedPrincipal, createSelfManagedPrincipal, createSelfManagedPrincipalFromFile, isHubEndpoint, loadOrCreateIdentity, openWebSocket, parseEndpointUri, runInitializeHandshake } from "@hediet/linkrpc/node";
4
+ import { Hub, HubConnectionAcceptor, RootOverlay, anonymousHandler, createHubServiceInterfaces, registerHubServices, registerIdentityServices } from "@hediet/linkrpc-hub/hub/server/client";
5
+ import { SocketServer } from "@hediet/linkrpc-hub/hub/server/node";
6
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
7
+ import * as fs$1 from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path$1 from "node:path";
10
+ import { resolve } from "node:path";
11
+ import { spawnCommand, spawnCommand as spawnCommand$1 } from "@hediet/linkrpc-hub/spawn";
12
+ import { tapTransport } from "@hediet/linkrpc-hub/hub/server/transit";
13
+ import * as net from "node:net";
14
+ import "@hediet/linkrpc-hub/config";
15
+ import * as fs from "node:fs/promises";
16
+ import { chmod, mkdir, open, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
17
+ import { fetchSchema as fetchSchema$1, findMethodInSchema, hubAccessInterface, walkHub, walkHubDetailed } from "@hediet/linkrpc/hub/common";
18
+ //#region src/mcpForward.interface.ts
19
+ /**
20
+ * Transparent MCP tunnel.
21
+ *
22
+ * A single long-lived {@link mcpForwardInterface.connect} request opens one MCP
23
+ * session "leg". The MCP JSON-RPC byte stream rides the linkrpc `$stream` duplex
24
+ * correlated to that request — **no MCP method is modeled here**. Payloads are
25
+ * opaque JSON-RPC messages, exactly as MCP emits them, so the forwarder that
26
+ * serves this interface stays dumb: it shuttles frames between a child process'
27
+ * stdio and the stream without ever parsing them.
28
+ *
29
+ * Why one duplex stream instead of request/response per MCP message: MCP is
30
+ * bidirectional and asynchronous (server-initiated `notifications/.../
31
+ * list_changed`, sampling requests). `$stream` already provides an ordered,
32
+ * request-correlated, cancellable duplex that the runtime keeps alive with
33
+ * periodic pings, so one stream == one MCP session leg.
34
+ *
35
+ * Lifecycle:
36
+ * - The consumer (the aggregator) calls `connect()`, obtaining
37
+ * `{ result, send, cancel, onMessage }`.
38
+ * - `send({ frame })` carries a frame **to** the child (stdin);
39
+ * `onMessage(({ frame }) => …)` receives frames **from** the child (stdout).
40
+ * - Child exit → the forwarder resolves the request (`connect` returns) → the
41
+ * consumer drops the client.
42
+ * - Consumer dispose / service removed → `cancel()` → the forwarder kills the
43
+ * child.
44
+ *
45
+ * Lives in the CLI package because the producer (`hub mcp-forward`) is a CLI
46
+ * command; the in-extension aggregator imports this contract from here too.
47
+ */
48
+ const mcpForwardInterface = defineInterface({
49
+ id: "vscode.mcp-forward",
50
+ description: "Transparent MCP tunnel: one streaming request carries a full MCP JSON-RPC session as opaque duplex stream frames."
51
+ }, { connect: requestType(z.object({
52
+ /**
53
+ * Advertised so a consumer can label/version the leg without
54
+ * opening the inner MCP session. Purely informational.
55
+ */
56
+ clientInfo: z.object({
57
+ name: z.string(),
58
+ version: z.string()
59
+ }).optional() }), z.object({
60
+ /** Informational server identity, if the forwarder knows it. */
61
+ serverInfo: z.object({
62
+ name: z.string(),
63
+ version: z.string()
64
+ }).optional() }), { description: "Open one MCP session leg. Resolves (void-ish) only when the session ends (child exits or the caller cancels). The MCP traffic is the stream, not the result." }).withStream({
65
+ client: z.object({ frame: z.unknown() }),
66
+ server: z.object({ frame: z.unknown() })
67
+ }) });
68
+ //#endregion
69
+ //#region src/methodRef.ts
70
+ /**
71
+ * A method reference as accepted on the CLI: `[serviceId::][interfaceId::]name[@hash]`.
72
+ */
73
+ var MethodRefWithOptHash = class MethodRefWithOptHash {
74
+ serviceId;
75
+ interfaceId;
76
+ methodName;
77
+ hash;
78
+ static parseMethodRef(input) {
79
+ if (input.length === 0) throw new Error("Method reference is empty.");
80
+ let hash;
81
+ let core = input;
82
+ const atIdx = input.lastIndexOf("@");
83
+ if (atIdx >= 0) {
84
+ const candidate = input.slice(atIdx + 1);
85
+ if (candidate.length > 0 && !candidate.includes("::")) {
86
+ hash = candidate;
87
+ core = input.slice(0, atIdx);
88
+ }
89
+ }
90
+ const parts = core.split("::");
91
+ if (parts.length === 0 || parts.some((p) => p.length === 0)) throw new Error(`Invalid method reference: "${input}"`);
92
+ if (parts.length > 3) throw new Error(`Invalid method reference: too many "::" separators in "${input}"`);
93
+ if (parts.length === 1) return new MethodRefWithOptHash(void 0, void 0, parts[0], hash);
94
+ if (parts.length === 2) return new MethodRefWithOptHash(void 0, parts[0], parts[1], hash);
95
+ return new MethodRefWithOptHash(parts[0], parts[1], parts[2], hash);
96
+ }
97
+ constructor(serviceId, interfaceId, methodName, hash) {
98
+ this.serviceId = serviceId;
99
+ this.interfaceId = interfaceId;
100
+ this.methodName = methodName;
101
+ this.hash = hash;
102
+ }
103
+ /** Method name as it goes on the wire (no hash, no whitespace). */
104
+ getMethodOnWire() {
105
+ if (this.serviceId !== void 0 && this.interfaceId !== void 0) return `${this.serviceId}::${this.interfaceId}::${this.methodName}`;
106
+ if (this.interfaceId !== void 0) return `${this.interfaceId}::${this.methodName}`;
107
+ return this.methodName;
108
+ }
109
+ };
110
+ //#endregion
111
+ //#region src/paramParsing.ts
112
+ function parseParamOverride(raw) {
113
+ const eq = raw.indexOf("=");
114
+ if (eq <= 0) throw new Error(`Invalid --param "${raw}" (expected key=value)`);
115
+ const key = raw.slice(0, eq);
116
+ const valueStr = raw.slice(eq + 1);
117
+ const path = key.split(".");
118
+ if (path.some((p) => p.length === 0)) throw new Error(`Invalid --param key "${key}" (empty segment)`);
119
+ return {
120
+ path,
121
+ value: parseScalar(valueStr)
122
+ };
123
+ }
124
+ function parseScalar(raw) {
125
+ if (raw.length === 0) return "";
126
+ const first = raw[0];
127
+ if (first === "\"" || first === "{" || first === "[" || first === "-" || first === "t" || first === "f" || first === "n" || first >= "0" && first <= "9") try {
128
+ return JSON.parse(raw);
129
+ } catch {}
130
+ return raw;
131
+ }
132
+ function mergeParams(opts) {
133
+ const base = opts.base !== void 0 ? cloneJson(opts.base) : void 0;
134
+ const overrides = opts.overrides ?? [];
135
+ if (overrides.length === 0) return base;
136
+ let root = base;
137
+ for (const raw of overrides) {
138
+ const { path, value } = parseParamOverride(raw);
139
+ root = setDeep(root, path, value);
140
+ }
141
+ return root;
142
+ }
143
+ function setDeep(root, path, value) {
144
+ if (path.length === 1) {
145
+ const target = asPlainObject(root) ?? {};
146
+ target[path[0]] = value;
147
+ return target;
148
+ }
149
+ const target = asPlainObject(root) ?? {};
150
+ const [head, ...rest] = path;
151
+ target[head] = setDeep(target[head], rest, value);
152
+ return target;
153
+ }
154
+ function asPlainObject(v) {
155
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
156
+ }
157
+ function cloneJson(v) {
158
+ if (v === void 0) return v;
159
+ return JSON.parse(JSON.stringify(v));
160
+ }
161
+ //#endregion
162
+ //#region src/validation.ts
163
+ /**
164
+ * Validate a concrete value against an `SvcJsonSchema`. Reuses linkrpc's
165
+ * structural assignability — the value is lowered to a closed, const-shaped
166
+ * schema and then asked "is this assignable to the target?". This avoids
167
+ * pulling in a separate JSON-Schema validator and stays consistent with how
168
+ * the connection layer reasons about interface compatibility.
169
+ *
170
+ * Returns `undefined` if the value is valid, or a short reason string.
171
+ */
172
+ function validateValueAgainstSchema(value, target, components = {}) {
173
+ const actual = valueToConstSchema(value);
174
+ try {
175
+ return isAssignable(actual, target, { schemas: components }) ? void 0 : "does not match schema";
176
+ } catch (e) {
177
+ return e.message;
178
+ }
179
+ }
180
+ /**
181
+ * Lower a JSON value to the tightest `SvcJsonSchema` that matches only it.
182
+ * Primitives become `{ const }`; arrays become tuples with `items: false`
183
+ * (forbidding extras); objects become closed records with every property
184
+ * required.
185
+ *
186
+ * `undefined` becomes the empty closed object — that's the
187
+ * "no params supplied" case, which is only assignable to a target that has
188
+ * no required properties.
189
+ */
190
+ function valueToConstSchema(v) {
191
+ if (v === void 0) return {
192
+ type: "object",
193
+ properties: {},
194
+ additionalProperties: false
195
+ };
196
+ if (v === null || typeof v === "boolean" || typeof v === "number" || typeof v === "string") return { const: v };
197
+ if (Array.isArray(v)) return {
198
+ type: "array",
199
+ prefixItems: v.map(valueToConstSchema),
200
+ items: false
201
+ };
202
+ const obj = v;
203
+ const properties = {};
204
+ const required = [];
205
+ for (const [k, val] of Object.entries(obj)) {
206
+ properties[k] = valueToConstSchema(val);
207
+ required.push(k);
208
+ }
209
+ return {
210
+ type: "object",
211
+ properties,
212
+ required,
213
+ additionalProperties: false
214
+ };
215
+ }
216
+ /**
217
+ * Walk `value` against `schema` and collect every mismatch we can pinpoint.
218
+ * Returns `[]` on success. Each issue has a JSON-style path plus a single
219
+ * line saying why that location is wrong — designed to be printed directly
220
+ * under a "Param validation failed:" header.
221
+ *
222
+ * Coverage is best-effort: scalar / object / array / tuple / const / enum /
223
+ * union / `$ref`. Unions report the branch with the fewest mismatches
224
+ * (heuristic) so the user gets one concrete trail to fix instead of a
225
+ * cascade of "no branch matched".
226
+ */
227
+ function explainValidation(value, schema, components = {}) {
228
+ const issues = [];
229
+ _walk(value, schema, "", components, issues);
230
+ return issues;
231
+ }
232
+ function _walk(value, schema, path, components, out) {
233
+ if (schema === true) return;
234
+ if (schema === false) {
235
+ out.push({
236
+ path,
237
+ reason: "no value is valid here"
238
+ });
239
+ return;
240
+ }
241
+ if ("$ref" in schema) {
242
+ const resolved = _resolveRef(schema.$ref, components);
243
+ if (!resolved) {
244
+ out.push({
245
+ path,
246
+ reason: `unresolved $ref ${schema.$ref}`
247
+ });
248
+ return;
249
+ }
250
+ _walk(value, resolved, path, components, out);
251
+ return;
252
+ }
253
+ if ("const" in schema) {
254
+ if (!_jsonEq(value, schema.const)) out.push({
255
+ path,
256
+ reason: `expected ${_jsonShow(schema.const)}, got ${_describeValue(value)}`
257
+ });
258
+ return;
259
+ }
260
+ if ("enum" in schema) {
261
+ if (!schema.enum.some((v) => _jsonEq(value, v))) {
262
+ const opts = schema.enum.slice(0, 5).map(_jsonShow).join(" | ");
263
+ const more = schema.enum.length > 5 ? ` | …` : "";
264
+ out.push({
265
+ path,
266
+ reason: `expected one of ${opts}${more}, got ${_describeValue(value)}`
267
+ });
268
+ }
269
+ return;
270
+ }
271
+ if ("anyOf" in schema || "oneOf" in schema) {
272
+ const branches = "anyOf" in schema ? schema.anyOf : schema.oneOf;
273
+ let best;
274
+ for (const b of branches) {
275
+ const sub = [];
276
+ _walk(value, b, path, components, sub);
277
+ if (sub.length === 0) return;
278
+ if (!best || sub.length < best.length) best = sub;
279
+ }
280
+ if (best) out.push(...best);
281
+ else out.push({
282
+ path,
283
+ reason: "value does not match any union branch"
284
+ });
285
+ return;
286
+ }
287
+ switch (schema.type) {
288
+ case "null":
289
+ if (value !== null) out.push({
290
+ path,
291
+ reason: `expected null, got ${_describeValue(value)}`
292
+ });
293
+ return;
294
+ case "boolean":
295
+ if (typeof value !== "boolean") out.push({
296
+ path,
297
+ reason: `expected boolean, got ${_describeValue(value)}`
298
+ });
299
+ return;
300
+ case "number":
301
+ if (typeof value !== "number") out.push({
302
+ path,
303
+ reason: `expected number, got ${_describeValue(value)}`
304
+ });
305
+ return;
306
+ case "integer":
307
+ if (typeof value !== "number" || !Number.isInteger(value)) out.push({
308
+ path,
309
+ reason: `expected integer, got ${_describeValue(value)}`
310
+ });
311
+ return;
312
+ case "string":
313
+ if (typeof value !== "string") out.push({
314
+ path,
315
+ reason: `expected string, got ${_describeValue(value)}`
316
+ });
317
+ return;
318
+ case "array":
319
+ _walkArray(value, schema, path, components, out);
320
+ return;
321
+ case "object":
322
+ _walkObject(value, schema, path, components, out);
323
+ return;
324
+ default: out.push({
325
+ path,
326
+ reason: "does not match schema"
327
+ });
328
+ }
329
+ }
330
+ function _walkObject(value, schema, path, components, out) {
331
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
332
+ out.push({
333
+ path: path || "(root)",
334
+ reason: `expected object, got ${_describeValue(value)}`
335
+ });
336
+ return;
337
+ }
338
+ const obj = value;
339
+ for (const req of schema.required ?? []) if (!(req in obj)) {
340
+ const p = _joinKey(path, req);
341
+ const propSchema = schema.properties[req];
342
+ const typeHint = propSchema ? ` (${_typeHint(propSchema, components)})` : "";
343
+ out.push({
344
+ path: p,
345
+ reason: `required${typeHint}, but missing`
346
+ });
347
+ }
348
+ for (const [k, v] of Object.entries(obj)) {
349
+ const propSchema = schema.properties[k];
350
+ const p = _joinKey(path, k);
351
+ if (propSchema !== void 0) _walk(v, propSchema, p, components, out);
352
+ else if (schema.additionalProperties === false) out.push({
353
+ path: p,
354
+ reason: "unknown property"
355
+ });
356
+ else _walk(v, schema.additionalProperties, p, components, out);
357
+ }
358
+ }
359
+ function _walkArray(value, schema, path, components, out) {
360
+ if (!Array.isArray(value)) {
361
+ out.push({
362
+ path: path || "(root)",
363
+ reason: `expected array, got ${_describeValue(value)}`
364
+ });
365
+ return;
366
+ }
367
+ const prefix = schema.prefixItems ?? [];
368
+ for (let i = 0; i < value.length; i++) if (i < prefix.length) _walk(value[i], prefix[i], `${path}[${i}]`, components, out);
369
+ else if (schema.items === false) out.push({
370
+ path: `${path}[${i}]`,
371
+ reason: "extra element (tuple is closed)"
372
+ });
373
+ else if (schema.items !== void 0) _walk(value[i], schema.items, `${path}[${i}]`, components, out);
374
+ if (prefix.length > 0 && value.length < prefix.length && schema.items === false) out.push({
375
+ path: path || "(root)",
376
+ reason: `expected tuple of length ${prefix.length}, got ${value.length}`
377
+ });
378
+ }
379
+ /**
380
+ * Render an `SvcJsonSchema` as a short, copy-paste-friendly type expression.
381
+ * Used as a hint next to required-property errors (`"name required (string)"`)
382
+ * and as the "Expected" footer when full-on params reporting fires.
383
+ */
384
+ function describeSchema(schema, components = {}, depth = 0) {
385
+ if (schema === true) return "any";
386
+ if (schema === false) return "never";
387
+ if ("$ref" in schema) return schema.$ref.split("/").pop() ?? schema.$ref;
388
+ if ("const" in schema) return _jsonShow(schema.const);
389
+ if ("enum" in schema) return schema.enum.slice(0, 6).map(_jsonShow).join(" | ") + (schema.enum.length > 6 ? " | …" : "");
390
+ if ("anyOf" in schema || "oneOf" in schema) return ("anyOf" in schema ? schema.anyOf : schema.oneOf).map((b) => describeSchema(b, components, depth + 1)).join(" | ");
391
+ const t = schema.type;
392
+ switch (t) {
393
+ case "null":
394
+ case "boolean":
395
+ case "number":
396
+ case "integer":
397
+ case "string": return t;
398
+ case "array": {
399
+ const arr = schema;
400
+ if (arr.prefixItems && arr.prefixItems.length > 0) {
401
+ const head = arr.prefixItems.map((s) => describeSchema(s, components, depth + 1)).join(", ");
402
+ if (arr.items === false || arr.items === void 0) return `[${head}]`;
403
+ return `[${head}, …${describeSchema(arr.items, components, depth + 1)}]`;
404
+ }
405
+ return arr.items === false || arr.items === void 0 ? "[]" : `${describeSchema(arr.items, components, depth + 1)}[]`;
406
+ }
407
+ case "object": {
408
+ if (depth >= 2) return "object";
409
+ const obj = schema;
410
+ const reqSet = new Set(obj.required ?? []);
411
+ return `{ ${Object.entries(obj.properties).map(([k, v]) => {
412
+ return `${k}${reqSet.has(k) ? "" : "?"}: ${describeSchema(v, components, depth + 1)}`;
413
+ }).join("; ")} }`;
414
+ }
415
+ }
416
+ return "any";
417
+ }
418
+ /**
419
+ * Multi-line table of an object schema's properties: name, required-marker,
420
+ * type, and description. Used as the "Expected params:" footer printed under
421
+ * a validation error. Returns `undefined` if `schema` is not an object —
422
+ * fall back to a single `describeSchema` line in that case.
423
+ */
424
+ function describeObjectParams(schema, components = {}) {
425
+ const resolved = _resolveTop(schema, components);
426
+ if (!resolved || typeof resolved !== "object" || !("type" in resolved) || resolved.type !== "object") return;
427
+ const obj = resolved;
428
+ const required = new Set(obj.required ?? []);
429
+ const rows = [];
430
+ for (const [k, v] of Object.entries(obj.properties)) {
431
+ const resolvedV = _resolveTop(v, components);
432
+ const desc = (resolvedV && typeof resolvedV === "object" && "description" in resolvedV ? resolvedV.description : void 0) ?? "";
433
+ rows.push({
434
+ name: k,
435
+ type: describeSchema(v, components, 1),
436
+ req: required.has(k) ? "required" : "optional",
437
+ desc
438
+ });
439
+ }
440
+ if (rows.length === 0) return "(no params)";
441
+ const nameW = Math.max(...rows.map((r) => r.name.length));
442
+ const typeW = Math.max(...rows.map((r) => r.type.length));
443
+ const reqW = Math.max(...rows.map((r) => r.req.length));
444
+ return rows.map((r) => {
445
+ const head = ` ${r.name.padEnd(nameW)} ${r.type.padEnd(typeW)} ${r.req.padEnd(reqW)}`;
446
+ return r.desc ? `${head} ${r.desc}` : head;
447
+ }).join("\n");
448
+ }
449
+ function _resolveTop(schema, components) {
450
+ if (schema === true || schema === false) return schema;
451
+ if ("$ref" in schema) return _resolveRef(schema.$ref, components);
452
+ return schema;
453
+ }
454
+ function _resolveRef(ref, components) {
455
+ if (!ref.startsWith("#/components/schemas/")) return void 0;
456
+ return components[ref.slice(21)];
457
+ }
458
+ function _typeHint(schema, components) {
459
+ return describeSchema(schema, components, 1);
460
+ }
461
+ function _joinKey(parent, key) {
462
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) return `${parent}.${key}`;
463
+ return `${parent}[${JSON.stringify(key)}]`;
464
+ }
465
+ function _jsonEq(a, b) {
466
+ return JSON.stringify(a) === JSON.stringify(b);
467
+ }
468
+ function _jsonShow(v) {
469
+ return typeof v === "string" ? JSON.stringify(v) : String(v);
470
+ }
471
+ function _describeValue(v) {
472
+ if (v === null) return "null";
473
+ if (v === void 0) return "undefined";
474
+ if (Array.isArray(v)) return `array (length ${v.length})`;
475
+ if (typeof v === "object") return "object";
476
+ if (typeof v === "string") return `string (${JSON.stringify(v.length > 40 ? v.slice(0, 37) + "…" : v)})`;
477
+ return `${typeof v} (${JSON.stringify(v)})`;
478
+ }
479
+ //#endregion
480
+ //#region ../../packages-private/linkrpc-client/src/localHub.ts
481
+ /** Default serviceId namespace the local-hub child may claim (and sees via `hubGrantedServiceId::get`). */
482
+ const DEFAULT_LOCAL_NAMESPACE = "local";
483
+ /** Subfolder (under the linkrpc data dir) holding provisioned identity slots. */
484
+ const PROVISION_SUBDIR = "provisioned-identities";
485
+ /** Provisioned slots untouched for longer than this are swept on next run. */
486
+ const PROVISION_MAX_AGE_MS = 2592e6;
487
+ /**
488
+ * Start an in-process hub on a private socket, spawn `command` as a participant
489
+ * (handing it the socket + token via `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN`), and
490
+ * resolve once the child has registered a service under {@link LOCAL_NAMESPACE}.
491
+ *
492
+ * The hub is single-tenant and local: no claim policy (every well-formed claim
493
+ * is allowed) and no provenance. Identity is either ephemeral (fresh per run)
494
+ * or, when `provisionSlot` is set, a persisted managed identity so the child's
495
+ * HPKE wrap/unwrap keys survive across runs.
496
+ */
497
+ async function startLocalHub(opts) {
498
+ const resolveIdentity = _makeIdentityResolver(opts.provisionSlot);
499
+ const grantedNs = DEFAULT_LOCAL_NAMESPACE;
500
+ const hub = new Hub();
501
+ createHubServiceInterfaces(hub);
502
+ const socketPath = SocketServer.allocSocketPath();
503
+ const token = randomBytes(16).toString("hex");
504
+ const socketServer = await SocketServer.start({ endpoint: socketPath });
505
+ const acceptor = new HubConnectionAcceptor({
506
+ server: socketServer,
507
+ hub,
508
+ handlers: [anonymousHandler({
509
+ grantedServiceIdNamespace: grantedNs,
510
+ ...resolveIdentity ? { resolveIdentity } : {}
511
+ })]
512
+ });
513
+ const child = spawnCommand(opts.command, {
514
+ stdio: [
515
+ "inherit",
516
+ "inherit",
517
+ "inherit"
518
+ ],
519
+ env: {
520
+ ...process.env,
521
+ ...opts.env,
522
+ LINKRPC_ENDPOINT: socketPath,
523
+ LINKRPC_TOKEN: token
524
+ },
525
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
526
+ });
527
+ let childExited = false;
528
+ child.once("exit", () => {
529
+ childExited = true;
530
+ });
531
+ const dispose = () => {
532
+ if (!child.killed) child.kill();
533
+ acceptor.dispose();
534
+ socketServer.dispose();
535
+ if (process.platform !== "win32") try {
536
+ fs$1.unlinkSync(socketPath);
537
+ } catch {}
538
+ };
539
+ try {
540
+ await _waitForClaim(hub, grantedNs, () => childExited, opts.readyTimeoutMs ?? 3e4);
541
+ } catch (e) {
542
+ dispose();
543
+ throw e;
544
+ }
545
+ return {
546
+ socketPath,
547
+ token,
548
+ dispose
549
+ };
550
+ }
551
+ /**
552
+ * The overlay's claim front door has no routing table to write: the tunnel owns
553
+ * the real claim on the source hub, and the splitter relays every uplink request
554
+ * to the child regardless. So `hubGrantedServiceId::register` succeeds as a
555
+ * no-op through this stub link.
556
+ */
557
+ const _noopUpstream = {
558
+ claimPrefix: () => {},
559
+ releasePrefix: () => false,
560
+ edgeId: "overlay-uplink",
561
+ dispose: () => {}
562
+ };
563
+ async function startLocalOverlay(opts) {
564
+ const resolveIdentity = _makeIdentityResolver(opts.provisionSlot);
565
+ const socketPath = SocketServer.allocSocketPath();
566
+ const token = randomBytes(16).toString("hex");
567
+ const socketServer = await SocketServer.start({ endpoint: socketPath });
568
+ const accepted = new Promise((resolve) => {
569
+ socketServer.setConnectionHandler((t) => resolve(t));
570
+ });
571
+ const child = spawnCommand(opts.command, {
572
+ stdio: [
573
+ "ignore",
574
+ "inherit",
575
+ "inherit"
576
+ ],
577
+ env: {
578
+ ...process.env,
579
+ ...opts.env,
580
+ LINKRPC_ENDPOINT: socketPath,
581
+ LINKRPC_TOKEN: token
582
+ },
583
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
584
+ });
585
+ const pair = new TransportPair();
586
+ const overlay = new RootOverlay({ uplink: pair.a });
587
+ registerHubServices(overlay.root, _noopUpstream, { grantedServiceIdNamespace: opts.grantedNamespace });
588
+ if (resolveIdentity !== void 0) registerIdentityServices(overlay.root, { resolveIdentity });
589
+ const dispose = () => {
590
+ if (!child.killed) child.kill();
591
+ overlay.dispose();
592
+ pair.a.dispose();
593
+ pair.b.dispose();
594
+ socketServer.dispose();
595
+ if (process.platform !== "win32") try {
596
+ fs$1.unlinkSync(socketPath);
597
+ } catch {}
598
+ };
599
+ const childTransport = await Promise.race([accepted, new Promise((_resolve, reject) => {
600
+ child.once("exit", (code) => reject(/* @__PURE__ */ new Error(`overlay: cmd-env child exited (code ${code ?? "?"}) before connecting`)));
601
+ })]).catch((err) => {
602
+ dispose();
603
+ throw err;
604
+ });
605
+ overlay.connectParticipant(childTransport);
606
+ return {
607
+ uplink: pair.b,
608
+ dispose
609
+ };
610
+ }
611
+ /**
612
+ * Build the hub's `resolveIdentity`, or `undefined` when no identity should be
613
+ * provided. Without `provisionSlot` the hub serves no identity at all — a child
614
+ * that needs `identity::*` will fail. With it, a single persisted managed
615
+ * identity is shared by all connections (stale slots swept first), so the
616
+ * child's HPKE wrap/unwrap keys survive across runs.
617
+ */
618
+ function _makeIdentityResolver(provisionSlot) {
619
+ if (provisionSlot === void 0) return;
620
+ const dir = _provisionDir();
621
+ _sweepProvisionedIdentities(dir);
622
+ let shared;
623
+ return () => {
624
+ if (!shared) shared = (async () => {
625
+ const persisted = await loadOrCreateIdentity({
626
+ id: provisionSlot,
627
+ storeDir: dir
628
+ });
629
+ return new InMemoryManagedIdentity(persisted.keypair, persisted.wrapKeypair);
630
+ })();
631
+ return shared;
632
+ };
633
+ }
634
+ /** Resolve the slot id for a `--provision-identity` / `--provision-identity-slot` run. */
635
+ function resolveProvisionSlot(explicitSlot, provisionIdentity, commandString) {
636
+ if (explicitSlot !== void 0) return explicitSlot;
637
+ if (provisionIdentity) return JSON.stringify({
638
+ cwd: process.cwd(),
639
+ cmdStr: commandString
640
+ });
641
+ }
642
+ /** Dedicated provisioned-identity folder (sibling of linkrpc's user identities). */
643
+ function _provisionDir() {
644
+ const home = os.homedir();
645
+ let base;
646
+ if (process.platform === "win32") base = process.env.APPDATA ?? path$1.join(home, "AppData", "Roaming");
647
+ else if (process.platform === "darwin") base = path$1.join(home, "Library", "Application Support");
648
+ else base = process.env.XDG_CONFIG_HOME ?? path$1.join(home, ".config");
649
+ return path$1.join(base, "linkrpc", PROVISION_SUBDIR);
650
+ }
651
+ /** Delete provisioned identity files whose mtime is older than the max age. */
652
+ function _sweepProvisionedIdentities(dir) {
653
+ let entries;
654
+ try {
655
+ entries = fs$1.readdirSync(dir);
656
+ } catch {
657
+ return;
658
+ }
659
+ const cutoff = Date.now() - PROVISION_MAX_AGE_MS;
660
+ for (const name of entries) {
661
+ if (!name.endsWith(".json")) continue;
662
+ const file = path$1.join(dir, name);
663
+ try {
664
+ if (fs$1.statSync(file).mtimeMs < cutoff) fs$1.unlinkSync(file);
665
+ } catch {}
666
+ }
667
+ }
668
+ /** Resolve once the child claims `prefix` (or a sub-prefix), else reject. */
669
+ function _waitForClaim(hub, prefix, childExited, timeoutMs) {
670
+ const deadline = Date.now() + timeoutMs;
671
+ return new Promise((resolve, reject) => {
672
+ const check = () => {
673
+ if (hub.claimedPrefixes().some((p) => p === prefix || p.startsWith(`${prefix}/`))) {
674
+ resolve();
675
+ return;
676
+ }
677
+ if (childExited()) {
678
+ reject(/* @__PURE__ */ new Error("connect: command exited before registering a service"));
679
+ return;
680
+ }
681
+ if (Date.now() >= deadline) {
682
+ reject(/* @__PURE__ */ new Error(`connect: timed out waiting for command to register a service under '${prefix}'`));
683
+ return;
684
+ }
685
+ setTimeout(check, 50);
686
+ };
687
+ check();
688
+ });
689
+ }
690
+ //#endregion
691
+ //#region ../../packages-private/linkrpc-client/src/endpoint.ts
692
+ /**
693
+ * Endpoint resolution for the CLI = "where the linkrpc server lives, and how to
694
+ * reach (or start) it". The parsed truth is the {@link ResolvedEndpoint} union from
695
+ * `@hediet/linkrpc/node`; this module turns the ergonomic flags / env vars into
696
+ * one.
697
+ *
698
+ * Sources, highest precedence first:
699
+ * 1. `--endpoint-cmd <command>` → spawn a server, connect via injected env
700
+ * 2. `--endpoint-cmd-stdio <command>` → spawn a child, talk over its stdio
701
+ * 3. `--endpoint <uri>` → a literal strict endpoint URI
702
+ * 4. `LINKRPC_ENDPOINT` / `HUBRPC_ENDPOINT` env vars → bare path / ws url
703
+ *
704
+ * `--endpoint-token` only applies to socket / ws endpoint URIs that contain an
705
+ * exact `token=%` placeholder. Env vars support both `LINKRPC_*` and legacy
706
+ * `HUBRPC_*`, with `LINKRPC_*` taking precedence. At most one of `--endpoint*`
707
+ * may be given.
708
+ * `ws-no-init:` preserves its query string verbatim and therefore does not use
709
+ * `--endpoint-token`.
710
+ */
711
+ const LEGACY_ENDPOINT_VAR = "HUBRPC_ENDPOINT";
712
+ const LEGACY_TOKEN_VAR = "HUBRPC_TOKEN";
713
+ const TOKEN_PLACEHOLDER = "%";
714
+ const TOKEN_PLACEHOLDER_PATTERN = /(?:[?&])token=(?<value>[^&#]*)/g;
715
+ /** Apply a token override to socket / ws specs; commands carry no token. */
716
+ function _withToken(spec, token) {
717
+ if (token === void 0) return spec;
718
+ if (spec.kind === "socket") return {
719
+ ...spec,
720
+ token
721
+ };
722
+ if (spec.kind === "ws") return {
723
+ ...spec,
724
+ token
725
+ };
726
+ return spec;
727
+ }
728
+ function _getEnvValue(env, currentName, legacyName) {
729
+ return env[currentName] ?? env[legacyName];
730
+ }
731
+ function _resolveUriEndpoint(uri, tokenOverride, fallbackToken, tokenFlagName, endpointName) {
732
+ const spec = parseEndpointUri(uri);
733
+ if (tokenOverride !== void 0) return _applyTokenOverride(spec, uri, tokenOverride, tokenFlagName, endpointName);
734
+ const rawTokenParams = [...uri.matchAll(TOKEN_PLACEHOLDER_PATTERN)].map((match) => match.groups?.value ?? "");
735
+ if (rawTokenParams.includes(TOKEN_PLACEHOLDER)) {
736
+ if (rawTokenParams.length !== 1) throw new Error(`${endpointName} must contain at most one token parameter`);
737
+ if (fallbackToken === void 0) throw new Error(`${endpointName} contains token=% but no token value was provided`);
738
+ return _withToken(spec, fallbackToken);
739
+ }
740
+ return _withToken(spec, ("token" in spec ? spec.token : void 0) ?? fallbackToken);
741
+ }
742
+ function _applyTokenOverride(spec, uri, token, tokenFlagName, endpointName) {
743
+ if (spec.kind !== "socket" && spec.kind !== "ws") throw new Error(`${tokenFlagName} is only supported for socket/unix/npipe and ws/wss ${endpointName} values`);
744
+ if (token === TOKEN_PLACEHOLDER) throw new Error(`${tokenFlagName} must not be '${TOKEN_PLACEHOLDER}'`);
745
+ const tokenParams = [...uri.matchAll(TOKEN_PLACEHOLDER_PATTERN)].map((m) => m.groups?.value ?? "");
746
+ if (tokenParams.length !== 1 || tokenParams[0] !== TOKEN_PLACEHOLDER) throw new Error(`${tokenFlagName} requires ${endpointName} to contain exactly '?token=%' or '&token=%'`);
747
+ return _withToken(spec, token);
748
+ }
749
+ /**
750
+ * Resolve the effective endpoint from flags + environment. Returns
751
+ * `{ endpoint: undefined }` when nothing is configured (the caller may then
752
+ * error out). Mutually-exclusive `--endpoint*` flags yield an `error`.
753
+ */
754
+ function resolveEndpoint(input) {
755
+ const env = input.env ?? process.env;
756
+ const envEndpoint = _getEnvValue(env, LINKRPC_ENDPOINT_VAR, LEGACY_ENDPOINT_VAR);
757
+ if ([
758
+ input.endpoint,
759
+ input.endpointCmd,
760
+ input.endpointCmdStdio
761
+ ].filter((v) => v !== void 0).length > 1) return {
762
+ endpoint: void 0,
763
+ error: "specify at most one of --endpoint, --endpoint-cmd, --endpoint-cmd-stdio"
764
+ };
765
+ if ((input.provisionIdentity === true || input.provisionIdentitySlot !== void 0) && input.endpointCmd === void 0 && input.provisioningHandledElsewhere !== true) return {
766
+ endpoint: void 0,
767
+ error: "--provision-identity / --provision-identity-slot require --endpoint-cmd"
768
+ };
769
+ if (input.endpointToken !== void 0 && input.endpoint === void 0 && envEndpoint === void 0) return {
770
+ endpoint: void 0,
771
+ error: "--endpoint-token requires --endpoint or an endpoint environment variable containing token=%"
772
+ };
773
+ const cmdEnv = input.endpointCmdEnv;
774
+ const cmdCwd = input.endpointCmdCwd;
775
+ if (cmdEnv !== void 0 && Object.keys(cmdEnv).length > 0 && input.endpointCmd === void 0 && input.endpointCmdStdio === void 0) return {
776
+ endpoint: void 0,
777
+ error: "--endpoint-cmd-env requires --endpoint-cmd or --endpoint-cmd-stdio"
778
+ };
779
+ if (cmdCwd !== void 0 && input.endpointCmd === void 0 && input.endpointCmdStdio === void 0) return {
780
+ endpoint: void 0,
781
+ error: "--endpoint-cmd-cwd requires --endpoint-cmd or --endpoint-cmd-stdio"
782
+ };
783
+ try {
784
+ if (input.endpointCmd !== void 0) {
785
+ const provisionSlot = resolveProvisionSlot(input.provisionIdentitySlot, input.provisionIdentity === true, input.endpointCmd);
786
+ return {
787
+ endpoint: {
788
+ kind: "cmd-env",
789
+ command: { command: input.endpointCmd },
790
+ ...provisionSlot !== void 0 ? { provisionSlot } : {},
791
+ ...cmdEnv !== void 0 ? { env: cmdEnv } : {},
792
+ ...cmdCwd !== void 0 ? { cwd: cmdCwd } : {}
793
+ },
794
+ error: void 0
795
+ };
796
+ }
797
+ if (input.endpointCmdStdio !== void 0) return {
798
+ endpoint: {
799
+ kind: "cmd-stdio",
800
+ command: { command: input.endpointCmdStdio },
801
+ ...cmdEnv !== void 0 ? { env: cmdEnv } : {},
802
+ ...cmdCwd !== void 0 ? { cwd: cmdCwd } : {}
803
+ },
804
+ error: void 0
805
+ };
806
+ if (input.endpoint !== void 0) return {
807
+ endpoint: _resolveUriEndpoint(input.endpoint, input.endpointToken, void 0, "--endpoint-token", "--endpoint"),
808
+ error: void 0
809
+ };
810
+ if (envEndpoint) return {
811
+ endpoint: _resolveUriEndpoint(envEndpoint, input.endpointToken, _getEnvValue(env, LINKRPC_TOKEN_VAR, LEGACY_TOKEN_VAR), "--endpoint-token", `${LINKRPC_ENDPOINT_VAR}/${LEGACY_ENDPOINT_VAR}`),
812
+ error: void 0
813
+ };
814
+ } catch (e) {
815
+ return {
816
+ endpoint: void 0,
817
+ error: e.message
818
+ };
819
+ }
820
+ return {
821
+ endpoint: void 0,
822
+ error: void 0
823
+ };
824
+ }
825
+ /**
826
+ * Resolve a *target* endpoint from `--target-*` flags. Mirrors
827
+ * {@link resolveEndpoint} for tunnel-style commands that have both a source
828
+ * (the hub the CLI claims on) and a target (the implementation that handles
829
+ * inbound requests). Returns `{ endpoint: undefined }` when no `--target-*`
830
+ * flag is set so the caller can error with a command-specific message.
831
+ *
832
+ * Unlike {@link resolveEndpoint}, no env-var fallback is consulted \u2014 the
833
+ * target is always explicit.
834
+ */
835
+ function resolveTargetEndpoint(input) {
836
+ if ([
837
+ input.targetEndpoint,
838
+ input.targetEndpointCmd,
839
+ input.targetEndpointCmdStdio
840
+ ].filter((v) => v !== void 0).length > 1) return {
841
+ endpoint: void 0,
842
+ error: "specify at most one of --target-endpoint, --target-endpoint-cmd, --target-endpoint-cmd-stdio"
843
+ };
844
+ if ((input.provisionIdentity === true || input.provisionIdentitySlot !== void 0) && input.targetEndpointCmd === void 0) return {
845
+ endpoint: void 0,
846
+ error: "--provision-identity / --provision-identity-slot require --target-endpoint-cmd"
847
+ };
848
+ if (input.targetEndpointToken !== void 0 && input.targetEndpoint === void 0) return {
849
+ endpoint: void 0,
850
+ error: "--target-endpoint-token requires --target-endpoint containing token=%"
851
+ };
852
+ const cmdEnv = input.targetEndpointCmdEnv;
853
+ const cmdCwd = input.targetEndpointCmdCwd;
854
+ if (cmdEnv !== void 0 && Object.keys(cmdEnv).length > 0 && input.targetEndpointCmd === void 0 && input.targetEndpointCmdStdio === void 0) return {
855
+ endpoint: void 0,
856
+ error: "--target-endpoint-cmd-env requires --target-endpoint-cmd or --target-endpoint-cmd-stdio"
857
+ };
858
+ if (cmdCwd !== void 0 && input.targetEndpointCmd === void 0 && input.targetEndpointCmdStdio === void 0) return {
859
+ endpoint: void 0,
860
+ error: "--target-endpoint-cmd-cwd requires --target-endpoint-cmd or --target-endpoint-cmd-stdio"
861
+ };
862
+ try {
863
+ if (input.targetEndpointCmd !== void 0) {
864
+ const provisionSlot = resolveProvisionSlot(input.provisionIdentitySlot, input.provisionIdentity === true, input.targetEndpointCmd);
865
+ return {
866
+ endpoint: {
867
+ kind: "cmd-env",
868
+ command: { command: input.targetEndpointCmd },
869
+ ...provisionSlot !== void 0 ? { provisionSlot } : {},
870
+ ...cmdEnv !== void 0 ? { env: cmdEnv } : {},
871
+ ...cmdCwd !== void 0 ? { cwd: cmdCwd } : {}
872
+ },
873
+ error: void 0
874
+ };
875
+ }
876
+ if (input.targetEndpointCmdStdio !== void 0) return {
877
+ endpoint: {
878
+ kind: "cmd-stdio",
879
+ command: { command: input.targetEndpointCmdStdio },
880
+ ...cmdEnv !== void 0 ? { env: cmdEnv } : {},
881
+ ...cmdCwd !== void 0 ? { cwd: cmdCwd } : {}
882
+ },
883
+ error: void 0
884
+ };
885
+ if (input.targetEndpoint !== void 0) return {
886
+ endpoint: _resolveUriEndpoint(input.targetEndpoint, input.targetEndpointToken, void 0, "--target-endpoint-token", "--target-endpoint"),
887
+ error: void 0
888
+ };
889
+ } catch (e) {
890
+ return {
891
+ endpoint: void 0,
892
+ error: e.message
893
+ };
894
+ }
895
+ return {
896
+ endpoint: void 0,
897
+ error: void 0
898
+ };
899
+ }
900
+ /**
901
+ * Lower a resolved endpoint into a declarative {@link EndpointConfig} entry,
902
+ * applying the given routing fields. Used by `tunnel -c, --config` to append
903
+ * the source hub to a loaded config as a claiming endpoint, so the in-process
904
+ * hub registers the service id on the source and routes its inbound calls
905
+ * through the config-described target.
906
+ *
907
+ * A `cmd-env` endpoint's `provisionSlot` is carried over as a slotted managed
908
+ * identity so the claim signs with a persistent identity.
909
+ */
910
+ function resolvedEndpointToConfig(spec, routing) {
911
+ const claimServiceIds = [...routing.claimServiceIds];
912
+ switch (spec.kind) {
913
+ case "socket": return {
914
+ kind: "socket",
915
+ path: spec.path,
916
+ ...spec.token !== void 0 ? { token: spec.token } : {},
917
+ routeServiceIds: [],
918
+ claimServiceIds,
919
+ defaultRoute: false
920
+ };
921
+ case "ws": return {
922
+ kind: "ws",
923
+ url: spec.url,
924
+ ...spec.token !== void 0 ? { token: spec.token } : {},
925
+ routeServiceIds: [],
926
+ claimServiceIds,
927
+ defaultRoute: false
928
+ };
929
+ case "ws-no-init": throw new Error("ws-no-init endpoints cannot be used as declarative hub routes");
930
+ case "cmd-env": return {
931
+ kind: "cmd-env",
932
+ ..._commandFields(spec.command),
933
+ ...spec.env !== void 0 ? { env: { ...spec.env } } : {},
934
+ ...spec.cwd !== void 0 ? { cwd: spec.cwd } : {},
935
+ routeServiceIds: [],
936
+ claimServiceIds,
937
+ defaultRoute: false,
938
+ ...spec.provisionSlot !== void 0 ? { managedIdentity: { slot: spec.provisionSlot } } : {}
939
+ };
940
+ case "cmd-stdio": return {
941
+ kind: "cmd-stdio",
942
+ ..._commandFields(spec.command),
943
+ ...spec.env !== void 0 ? { env: { ...spec.env } } : {},
944
+ ...spec.cwd !== void 0 ? { cwd: spec.cwd } : {},
945
+ routeServiceIds: [],
946
+ claimServiceIds,
947
+ defaultRoute: false
948
+ };
949
+ }
950
+ }
951
+ /** Lower an {@link EndpointCommand} into the config's `cmd` / `argv` fields. */
952
+ function _commandFields(command) {
953
+ return "command" in command ? { cmd: command.command } : { argv: [...command.argv] };
954
+ }
955
+ //#endregion
956
+ //#region ../../packages-private/linkrpc-client/src/connect.ts
957
+ async function connect(endpoint, log) {
958
+ switch (endpoint.kind) {
959
+ case "cmd-stdio": return _connectCmdStdio(endpoint.command, endpoint.env, endpoint.cwd, log);
960
+ case "cmd-env": return _connectCmdEnv(endpoint.command, endpoint.provisionSlot, endpoint.env, endpoint.cwd, log);
961
+ case "ws": return _connectWs(endpoint, log);
962
+ case "ws-no-init": return _connectWs(endpoint, log);
963
+ case "socket": return _connectSocket(endpoint.path, endpoint.token, log);
964
+ }
965
+ }
966
+ /**
967
+ * Spawn a child from a command spec. `{ command }` is run through the OS shell
968
+ * (so quoting / splitting follows the shell's rules); `{ argv }` is run
969
+ * directly (no shell), except on Windows where `.cmd` shims need one.
970
+ */
971
+ async function _connectCmdStdio(command, env, cwd, log) {
972
+ const child = spawnCommand$1(command, {
973
+ stdio: [
974
+ "pipe",
975
+ "pipe",
976
+ "inherit"
977
+ ],
978
+ ...env !== void 0 ? { env: {
979
+ ...process.env,
980
+ ...env
981
+ } } : {},
982
+ ...cwd !== void 0 ? { cwd } : {}
983
+ });
984
+ if (!child.stdin || !child.stdout) throw new Error("connect: child process exposes no stdio");
985
+ const { transport } = await connectNdjson({
986
+ input: child.stdout,
987
+ output: child.stdin,
988
+ onClose: () => {
989
+ if (!child.killed) child.kill();
990
+ },
991
+ trace: log?.trace
992
+ });
993
+ return _makeCliConnection(transport, () => {
994
+ if (!child.killed) child.kill();
995
+ }, log);
996
+ }
997
+ /**
998
+ * Start a private in-process hub, spawn the child as a participant, then
999
+ * connect to the hub's socket. The child registers its services against the
1000
+ * hub exactly as it would against a remote one; we tear the hub + child down
1001
+ * when the connection closes.
1002
+ */
1003
+ async function _connectCmdEnv(command, provisionSlot, env, cwd, log) {
1004
+ return connectViaLocalHub({
1005
+ command,
1006
+ provisionSlot,
1007
+ env,
1008
+ cwd,
1009
+ log
1010
+ });
1011
+ }
1012
+ /**
1013
+ * Spawn a child under an in-process local hub (`startLocalHub`) and connect
1014
+ * to that hub over a socket. Backs the standard `cmd-env` endpoint path; the
1015
+ * hub + child are torn down when the connection closes.
1016
+ */
1017
+ async function connectViaLocalHub(opts) {
1018
+ const hub = await startLocalHub({
1019
+ command: opts.command,
1020
+ provisionSlot: opts.provisionSlot,
1021
+ env: opts.env,
1022
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
1023
+ });
1024
+ try {
1025
+ const conn = await _connectSocket(hub.socketPath, hub.token, opts.log);
1026
+ return {
1027
+ ...conn,
1028
+ close: () => {
1029
+ conn.close();
1030
+ hub.dispose();
1031
+ }
1032
+ };
1033
+ } catch (e) {
1034
+ hub.dispose();
1035
+ throw e;
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Connect to a spawned `cmd-env` child through a {@link startLocalOverlay}
1040
+ * RootOverlay instead of a full local hub. Root-form calls (`identity::*`,
1041
+ * `hubGrantedServiceId::*`, `hubrpc.directory`, `hubAccess`) are served locally;
1042
+ * every prefixed request/response is relayed over the returned connection's
1043
+ * channel. Used by `tunnel --target-endpoint-cmd`: the tunnel forwards each
1044
+ * claimed request onto this channel and the overlay delivers it to the child —
1045
+ * no hub, no `hub` service id, no claim-wait, but `identity::*` still served so
1046
+ * `--provision-identity` targets work.
1047
+ */
1048
+ async function connectViaRootOverlay(opts) {
1049
+ const overlay = await startLocalOverlay({
1050
+ command: opts.command,
1051
+ provisionSlot: opts.provisionSlot,
1052
+ ...opts.env !== void 0 ? { env: opts.env } : {},
1053
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
1054
+ grantedNamespace: opts.grantedNamespace
1055
+ });
1056
+ return _makeCliConnection(overlay.uplink, overlay.dispose, opts.log);
1057
+ }
1058
+ function _connectWs(endpoint, log) {
1059
+ return openWebSocket(endpoint.url).then(async (ws) => {
1060
+ const closeWs = () => {
1061
+ try {
1062
+ ws.close();
1063
+ } catch {}
1064
+ };
1065
+ const baseTransport = new WebSocketTransport(ws, closeWs);
1066
+ const transport = log?.trace === void 0 ? baseTransport : traceMessageTransport(baseTransport, log.trace);
1067
+ if (endpoint.kind === "ws") try {
1068
+ await runInitializeHandshake(transport, {
1069
+ kind: "client",
1070
+ token: endpoint.token ?? ""
1071
+ });
1072
+ } catch (err) {
1073
+ transport.dispose();
1074
+ closeWs();
1075
+ throw err;
1076
+ }
1077
+ return _makeCliConnection(transport, closeWs, log);
1078
+ });
1079
+ }
1080
+ async function _connectSocket(socketPath, token, log) {
1081
+ const socket = net.createConnection(socketPath);
1082
+ await new Promise((resolve, reject) => {
1083
+ const onConnect = () => {
1084
+ socket.removeListener("error", onError);
1085
+ resolve();
1086
+ };
1087
+ const onError = (error) => {
1088
+ socket.removeListener("connect", onConnect);
1089
+ socket.destroy();
1090
+ reject(error);
1091
+ };
1092
+ socket.once("connect", onConnect);
1093
+ socket.once("error", onError);
1094
+ });
1095
+ socket.on("error", () => socket.destroy());
1096
+ const { transport } = await connectNdjson({
1097
+ input: socket,
1098
+ output: socket,
1099
+ onClose: () => socket.destroy(),
1100
+ initialize: {
1101
+ kind: "client",
1102
+ token: token ?? ""
1103
+ },
1104
+ trace: log?.trace
1105
+ });
1106
+ return _makeCliConnection(transport, () => socket.destroy(), log);
1107
+ }
1108
+ /**
1109
+ * In-memory connection — for tests. The caller hands us a transport already
1110
+ * wired to a server-side channel (typically via `TransportPair`).
1111
+ */
1112
+ function connectViaTransport(transport) {
1113
+ return _makeCliConnection(transport, () => {});
1114
+ }
1115
+ function _makeCliConnection(transport, onClose, log) {
1116
+ const tapped = log?.log === void 0 ? transport : tapTransport(transport, {
1117
+ log: log.log,
1118
+ localLabel: "cli",
1119
+ remoteLabel: log.remoteLabel ?? "peer",
1120
+ ...log.maxPayload !== void 0 ? { maxPayload: log.maxPayload } : {}
1121
+ });
1122
+ const signing = {};
1123
+ const wrapped = SigningSender.wrapChannel(JsonRpcChannel.create(tapped), signing);
1124
+ const channel = wrapped.sender;
1125
+ return {
1126
+ channel,
1127
+ rpcChannel: wrapped,
1128
+ signing,
1129
+ setRequestHandler: (handler) => wrapped.setRequestHandler(handler),
1130
+ close: () => {
1131
+ channel.close();
1132
+ onClose();
1133
+ }
1134
+ };
1135
+ }
1136
+ //#endregion
1137
+ //#region ../../packages-private/linkrpc-client/src/principal.ts
1138
+ /**
1139
+ * Identity slot the managed-with-fallback default falls back to when the peer
1140
+ * does not offer a managed identity overlay (e.g. a plain stdio server). Maps
1141
+ * to the same on-disk slot `logout` clears.
1142
+ */
1143
+ const MANAGED_FALLBACK_USER_ID = "hubrpc-cli";
1144
+ /**
1145
+ * Parse a `--principal` value. `undefined` and `managed` both yield the
1146
+ * managed-with-fallback default. Throws on malformed input.
1147
+ */
1148
+ function parsePrincipalSpec(raw) {
1149
+ if (raw === void 0 || raw === "managed") return { kind: "managed" };
1150
+ if (raw.startsWith("user:")) {
1151
+ const id = raw.slice(5);
1152
+ if (id === "") throw new Error("--principal \"user:<id>\" requires a non-empty id");
1153
+ return {
1154
+ kind: "user",
1155
+ id
1156
+ };
1157
+ }
1158
+ if (raw.startsWith("file:")) {
1159
+ const path = raw.slice(5);
1160
+ if (path === "") throw new Error("--principal \"file:<path>\" requires a non-empty path");
1161
+ return {
1162
+ kind: "file",
1163
+ path
1164
+ };
1165
+ }
1166
+ throw new Error(`invalid --principal "${raw}" (expected "managed", "user:<id>", or "file:<path>")`);
1167
+ }
1168
+ /**
1169
+ * Resolve a {@link PrincipalSpec} into a concrete {@link Principal}, given the
1170
+ * (signed) sender used to bootstrap a managed identity. Cheap/idempotent, so
1171
+ * it can be re-derived on each reconnect. Also reports the {@link PrincipalSource}
1172
+ * that was actually used, including whether `managed` fell back to a local key.
1173
+ */
1174
+ async function resolvePrincipal(spec, sender) {
1175
+ switch (spec.kind) {
1176
+ case "managed": try {
1177
+ return {
1178
+ principal: await createManagedPrincipal(sender),
1179
+ source: { kind: "managed" }
1180
+ };
1181
+ } catch {
1182
+ return {
1183
+ principal: await createSelfManagedPrincipal(MANAGED_FALLBACK_USER_ID),
1184
+ source: {
1185
+ kind: "managed-fallback",
1186
+ userId: MANAGED_FALLBACK_USER_ID
1187
+ }
1188
+ };
1189
+ }
1190
+ case "user": return {
1191
+ principal: await createSelfManagedPrincipal(spec.id),
1192
+ source: {
1193
+ kind: "user",
1194
+ id: spec.id
1195
+ }
1196
+ };
1197
+ case "file": return {
1198
+ principal: await createSelfManagedPrincipalFromFile(spec.path),
1199
+ source: {
1200
+ kind: "file",
1201
+ path: spec.path
1202
+ }
1203
+ };
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Render a one-line, human-readable description of the identity used to sign
1208
+ * calls, e.g. `managed (node abcd012345…)` or
1209
+ * `local user:hubrpc-cli (node abcd012345…)`. `nodeId` is truncated to its
1210
+ * first 10 characters.
1211
+ */
1212
+ function formatPrincipalSource(source, nodeId) {
1213
+ const node = `node ${nodeId.slice(0, 10)}…`;
1214
+ switch (source.kind) {
1215
+ case "managed": return `managed (${node})`;
1216
+ case "managed-fallback": return `local user:${source.userId} (managed unavailable) (${node})`;
1217
+ case "user": return `local user:${source.id} (${node})`;
1218
+ case "file": return `local file:${source.path} (${node})`;
1219
+ }
1220
+ }
1221
+ //#endregion
1222
+ //#region ../../packages-private/linkrpc-client/src/identity.ts
1223
+ /**
1224
+ * Stable slot id for the on-disk keypair used by the managed-with-fallback
1225
+ * default (and `--principal user:hubrpc-cli`). The same slot always loads the
1226
+ * same keypair so persistent caps issued by the hub keep working across CLI
1227
+ * invocations.
1228
+ */
1229
+ /**
1230
+ * Delete the CLI's persistent identity keypair and its cached caps. The
1231
+ * next CLI command will mint a fresh keypair and trigger a new consent
1232
+ * modal. Returns the files that were removed (empty when there was no
1233
+ * stored state).
1234
+ */
1235
+ async function logoutCliIdentity() {
1236
+ const identity = await loadOrCreateIdentity({ id: MANAGED_FALLBACK_USER_ID });
1237
+ const capsFile = _capsFile(identity.file);
1238
+ const removed = [];
1239
+ for (const f of [identity.file, capsFile]) try {
1240
+ await fs.unlink(f);
1241
+ removed.push(f);
1242
+ } catch (e) {
1243
+ if (e.code !== "ENOENT") throw e;
1244
+ }
1245
+ return removed;
1246
+ }
1247
+ function _capsFile(identityFile) {
1248
+ return identityFile.replace(/\.json$/, ".caps.json");
1249
+ }
1250
+ //#endregion
1251
+ //#region ../../packages-private/linkrpc-client/src/reflection.ts
1252
+ async function fetchDefaults(channel) {
1253
+ const raw = await channel.sendRequest("hubrpc.defaults::get", {});
1254
+ if (!raw) return {};
1255
+ return {
1256
+ serviceId: raw.serviceId,
1257
+ interfaceId: raw.interfaceId,
1258
+ hash: raw.interfaceHash
1259
+ };
1260
+ }
1261
+ //#endregion
1262
+ //#region ../../packages-private/linkrpc-client/src/hubSigning.ts
1263
+ /** How long before expiry to refresh a capability (2 seconds = transit + skew). */
1264
+ const CAP_FRESHNESS_MARGIN_MS = 2e3;
1265
+ /**
1266
+ * The reflection interfaces the CLI walks for `ls` / `schema` / `defaults`
1267
+ * / the TUI bus walk. We request all of them, across every service, in a
1268
+ * single up-front consent prompt so exploration doesn't re-prompt per
1269
+ * service id. See {@link requestReflectionAccess}.
1270
+ */
1271
+ const REFLECTION_INTERFACE_IDS = [
1272
+ "hubrpc.directory",
1273
+ "hubrpc.schemas",
1274
+ "hubrpc.defaults"
1275
+ ];
1276
+ const TOPOLOGY_INTERFACE_ID = "hubrpc.topology";
1277
+ /**
1278
+ * Install signing on `signing` for any endpoint. Resolves the
1279
+ * {@link PrincipalSpec} into a concrete {@link Principal} (managed-with-
1280
+ * fallback, a user slot, or a file-backed keypair) and points the channel's
1281
+ * `SigningSender` at it so every outbound call is signed.
1282
+ *
1283
+ * When `negotiateHubCaps` is set (hub endpoints), it also ensures a persistent
1284
+ * `hubAccess` capability is cached on the principal's {@link CapBag} — requesting
1285
+ * one with a single signed round-trip on first run / cache miss. For hub
1286
+ * endpoints, it also installs a sign-time capability provider.
1287
+ *
1288
+ * By default (`autoNegotiatePerCall !== false`) that provider negotiates
1289
+ * per-call authority lazily using `callIntent` (method + params + nonce +
1290
+ * signedAtMs + optional interfaceHash), so one-shot grants can be pinned to the
1291
+ * exact call bytes being signed. Set `autoNegotiatePerCall: false` to disable
1292
+ * that: the provider then only presents caps already in the bag, and the caller
1293
+ * is expected to request access explicitly via
1294
+ * {@link SigningSession.requestAccess}.
1295
+ */
1296
+ async function setupSigning(channel, signing, principalSpec, opts) {
1297
+ const { principal, source: principalSource } = await resolvePrincipal(principalSpec, channel);
1298
+ signing.principal = principal;
1299
+ signing.oneShotCaps = void 0;
1300
+ signing.capProvider = void 0;
1301
+ const hubAccessMethod = `${hubAccessInterface.info.id}::requestAccess`;
1302
+ const consumerPrincipalId = principal.id;
1303
+ if (opts.negotiateHubCaps) {
1304
+ let inAccessNegotiation = false;
1305
+ const provider = async ({ method, params, nonce, signedAtMs, interfaceHash }) => {
1306
+ let presentCaps = principal.capBag.capabilities;
1307
+ presentCaps = presentCaps.filter((c) => capabilityFreshAt(c, signedAtMs, CAP_FRESHNESS_MARGIN_MS));
1308
+ const present = presentCaps.length > 0 ? { capabilities: presentCaps } : {};
1309
+ if (inAccessNegotiation) return {};
1310
+ const call = _wireMethodToCall(method);
1311
+ if (!call) return present;
1312
+ if (call.serviceId === "" || call.interfaceId === hubAccessInterface.info.id) return present;
1313
+ if (_capBagCovers(presentCaps, call)) return present;
1314
+ if (opts.autoNegotiatePerCall === false) return present;
1315
+ inAccessNegotiation = true;
1316
+ try {
1317
+ const granted = await _requestAccessForCall(channel, hubAccessMethod, consumerPrincipalId, {
1318
+ call,
1319
+ method,
1320
+ params,
1321
+ nonce,
1322
+ signedAtMs,
1323
+ interfaceHash
1324
+ });
1325
+ if (granted.length === 0) return present;
1326
+ const oneShot = granted.filter(_isOneShotCap);
1327
+ const persistent = granted.filter((c) => !_isOneShotCap(c));
1328
+ if (persistent.length > 0) await principal.capBag.add(...persistent);
1329
+ const durable = principal.capBag.capabilities.filter((c) => capabilityFreshAt(c, signedAtMs, CAP_FRESHNESS_MARGIN_MS));
1330
+ if (oneShot.length > 0) return { capabilities: [...durable, ...oneShot] };
1331
+ return durable.length > 0 ? { capabilities: durable } : {};
1332
+ } catch (err) {
1333
+ process.stderr.write(`linkrpc: capability negotiation skipped (${err.message})\n`);
1334
+ return present;
1335
+ } finally {
1336
+ inAccessNegotiation = false;
1337
+ }
1338
+ };
1339
+ signing.capProvider = provider;
1340
+ }
1341
+ return {
1342
+ principal,
1343
+ principalSource,
1344
+ hubAccessMethod,
1345
+ listGrants: () => principal.capBag.capabilities,
1346
+ requestAccess: (req) => _sessionRequestAccess(channel, hubAccessMethod, principal, req)
1347
+ };
1348
+ }
1349
+ /**
1350
+ * Up-front, explicit batched request for reflection access across *every*
1351
+ * service: `hubrpc.directory` / `hubrpc.schemas` / `hubrpc.defaults` with a
1352
+ * wildcard `serviceId` (`{ prefix: '' }`). One consent prompt covers the whole
1353
+ * bus, so `ls` / `schema` / `defaults` / the TUI walk stop re-prompting per
1354
+ * service id.
1355
+ *
1356
+ * Best-effort and fail-soft: a `denied` decision (or an open hub with no access
1357
+ * handler that rejects the call) is swallowed. Per-call auto-cap negotiation in
1358
+ * {@link setupSigning} then remains the fallback for individual gated calls.
1359
+ *
1360
+ * Returns the granted status so callers can log it; never throws.
1361
+ */
1362
+ async function requestReflectionAccess(session, opts = {}) {
1363
+ if (REFLECTION_INTERFACE_IDS.every((interfaceId) => _hasInterfaceAccess(session, interfaceId, { prefix: "" }))) return "granted";
1364
+ try {
1365
+ return (await session.requestAccess({
1366
+ consumer: {
1367
+ name: "linkrpc-cli",
1368
+ purpose: "Reflect on every service exposed by the hub (ls / schema / defaults)."
1369
+ },
1370
+ permissions: REFLECTION_INTERFACE_IDS.map((id) => ({
1371
+ target: {
1372
+ serviceId: { prefix: "" },
1373
+ interfaceId: { exact: id },
1374
+ members: [{ prefix: "" }]
1375
+ },
1376
+ canInvoke: true
1377
+ })),
1378
+ duration: opts.duration ?? "persistent"
1379
+ })).status === "granted" ? "granted" : "denied";
1380
+ } catch {
1381
+ return "skipped";
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Request topology access in one consent operation before fan-out begins.
1386
+ * Discovery mode needs both directory traversal and topology calls across the
1387
+ * bus; fixed-source mode requests topology only for those exact service ids.
1388
+ */
1389
+ async function requestTopologyAccess(session, opts = {}) {
1390
+ const sourceServiceIds = opts.sourceServiceIds === void 0 ? void 0 : [...new Set(opts.sourceServiceIds)].sort();
1391
+ const targets = sourceServiceIds === void 0 ? [{ prefix: "" }] : sourceServiceIds.map((serviceId) => ({ exact: serviceId }));
1392
+ const needsDirectory = sourceServiceIds === void 0;
1393
+ if ((!needsDirectory || _hasInterfaceAccess(session, "hubrpc.directory", { prefix: "" })) && targets.every((target) => _hasInterfaceAccess(session, TOPOLOGY_INTERFACE_ID, target))) return "granted";
1394
+ const permissions = [];
1395
+ if (needsDirectory) permissions.push({
1396
+ target: {
1397
+ serviceId: { prefix: "" },
1398
+ interfaceId: { exact: "hubrpc.directory" },
1399
+ members: [{ prefix: "" }]
1400
+ },
1401
+ canInvoke: true
1402
+ });
1403
+ for (const serviceId of targets) permissions.push({
1404
+ target: {
1405
+ serviceId,
1406
+ interfaceId: { exact: TOPOLOGY_INTERFACE_ID },
1407
+ members: [{ prefix: "" }]
1408
+ },
1409
+ canInvoke: true
1410
+ });
1411
+ try {
1412
+ return (await session.requestAccess({
1413
+ consumer: {
1414
+ name: "linkrpc-cli",
1415
+ purpose: sourceServiceIds === void 0 ? "Discover and inspect the topology exposed by every service on the hub." : "Inspect topology for the selected services."
1416
+ },
1417
+ permissions,
1418
+ duration: opts.duration ?? "persistent"
1419
+ })).status === "granted" ? "granted" : "denied";
1420
+ } catch {
1421
+ return "skipped";
1422
+ }
1423
+ }
1424
+ function _hasInterfaceAccess(session, interfaceId, requestedServiceId) {
1425
+ const now = Date.now();
1426
+ return session.listGrants().some((capability) => capability.audience === session.principal.id && capabilityFreshAt(capability, now, CAP_FRESHNESS_MARGIN_MS) && capability.permissions.some((permission) => {
1427
+ if (permission.canInvoke !== true || permission.callBind !== void 0 || permission.params !== void 0 || permission.target.interfaceHash !== void 0 || !permission.target.members.some((member) => "prefix" in member && member.prefix === "")) return false;
1428
+ if ("prefix" in requestedServiceId) {
1429
+ const grantedServiceId = permission.target.serviceId;
1430
+ return "prefix" in grantedServiceId && grantedServiceId.prefix === "" && permissionMatchesTarget({
1431
+ serviceId: "",
1432
+ interfaceId,
1433
+ member: ""
1434
+ }, permission);
1435
+ }
1436
+ return permissionMatchesTarget({
1437
+ serviceId: requestedServiceId.exact,
1438
+ interfaceId,
1439
+ member: ""
1440
+ }, permission);
1441
+ }));
1442
+ }
1443
+ /** Parse a wire method into a {@link CallTarget}, or `undefined` for form-1/2. */
1444
+ function _wireMethodToCall(wireMethod) {
1445
+ const parts = wireMethod.split("::");
1446
+ if (parts.length !== 3) return void 0;
1447
+ const [serviceId, interfaceId, member] = parts;
1448
+ return {
1449
+ serviceId,
1450
+ interfaceId,
1451
+ member
1452
+ };
1453
+ }
1454
+ /** A cap is one-shot when any permission is pinned to a single call via `callBind`. */
1455
+ function _isOneShotCap(sc) {
1456
+ return sc.permissions.some((p) => p.callBind !== void 0);
1457
+ }
1458
+ /** True when a durable (non-one-shot) cap in the bag authorises `target`. */
1459
+ function _capBagCovers(caps, target) {
1460
+ return caps.some((sc) => !_isOneShotCap(sc) && sc.permissions.some((p) => permissionMatchesTarget(target, p)));
1461
+ }
1462
+ async function _requestAccessForCall(channel, hubAccessMethod, consumerPrincipalId, req) {
1463
+ const result = await _sendRequestAccess(channel, hubAccessMethod, {
1464
+ consumer: {
1465
+ name: "linkrpc-cli",
1466
+ principal: consumerPrincipalId,
1467
+ purpose: `Invoke ${req.method}.`
1468
+ },
1469
+ permissions: [{
1470
+ target: {
1471
+ serviceId: { exact: req.call.serviceId },
1472
+ interfaceId: { exact: req.call.interfaceId },
1473
+ members: [{ exact: req.call.member }]
1474
+ },
1475
+ canInvoke: true,
1476
+ callIntent: {
1477
+ method: req.method,
1478
+ params: req.params,
1479
+ nonce: req.nonce,
1480
+ signedAtMs: req.signedAtMs,
1481
+ ...req.interfaceHash !== void 0 ? { interfaceHash: req.interfaceHash } : {},
1482
+ suggestion: "once"
1483
+ }
1484
+ }],
1485
+ duration: "once"
1486
+ });
1487
+ if (result.status !== "granted") return [];
1488
+ return result.capabilities ?? [];
1489
+ }
1490
+ async function _sendRequestAccess(channel, hubAccessMethod, params) {
1491
+ return await _awaitWithApprovalNotice(channel.sendRequest(hubAccessMethod, params));
1492
+ }
1493
+ /** Delay before we tell the user an access request is parked awaiting approval. */
1494
+ const APPROVAL_NOTICE_DELAY_MS = 750;
1495
+ /**
1496
+ * Await a `hubAccess::requestAccess` round-trip, printing a one-line hint to
1497
+ * stderr if it doesn't resolve quickly. Access requests park at the hub until a
1498
+ * human approver (admin) decides them, so without this notice the CLI looks
1499
+ * hung — it blocks with no output until approval. Fast requests (auto-approved
1500
+ * / open hub) stay silent because the notice only fires after
1501
+ * {@link APPROVAL_NOTICE_DELAY_MS}.
1502
+ */
1503
+ async function _awaitWithApprovalNotice(pending) {
1504
+ const timer = setTimeout(() => {
1505
+ process.stderr.write("linkrpc: access request sent — waiting for the hub admin to approve it...\n");
1506
+ }, APPROVAL_NOTICE_DELAY_MS);
1507
+ timer.unref?.();
1508
+ try {
1509
+ return await pending;
1510
+ } finally {
1511
+ clearTimeout(timer);
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Backs {@link SigningSession.requestAccess}. Sends a batched
1516
+ * `hubAccess::requestAccess`, then adds any durable (non-one-shot) caps the hub
1517
+ * minted to the principal's cap bag so later calls present them automatically.
1518
+ */
1519
+ async function _sessionRequestAccess(channel, hubAccessMethod, principal, req) {
1520
+ const result = await _sendRequestAccess(channel, hubAccessMethod, {
1521
+ consumer: {
1522
+ ...req.consumer,
1523
+ principal: principal.id
1524
+ },
1525
+ permissions: req.permissions,
1526
+ ...req.duration !== void 0 ? { duration: req.duration } : {}
1527
+ });
1528
+ if (result.status === "granted") {
1529
+ const capabilities = result.capabilities ?? [];
1530
+ const durable = capabilities.filter((c) => !_isOneShotCap(c));
1531
+ if (durable.length > 0) await principal.capBag.add(...durable);
1532
+ return {
1533
+ status: "granted",
1534
+ capabilities,
1535
+ addedDurable: durable.length
1536
+ };
1537
+ }
1538
+ return {
1539
+ status: result.status,
1540
+ reason: result.reason
1541
+ };
1542
+ }
1543
+ //#endregion
1544
+ //#region src/completions/directorySource.ts
1545
+ /**
1546
+ * Hub-backed source of completion candidates. Abstracted so the orchestrator
1547
+ * in {@link ./complete.ts} stays unit-testable: tests inject a hand-written
1548
+ * source, production wires {@link ChannelDirectorySource} which talks to a
1549
+ * live hub via the standard reflection helpers (with an optional file cache
1550
+ * provided by {@link ./cache}).
1551
+ *
1552
+ * The interface intentionally has three primitive operations:
1553
+ * - {@link entries} — the bus snapshot (serviceId, interfaceId) pairs
1554
+ * - {@link methodsOnInterface} — method names per (serviceId, interfaceId)
1555
+ * - {@link paramNamesForMethod} — param property names per method
1556
+ *
1557
+ * Everything else (distinct serviceIds, interfaces-on-service) is derived
1558
+ * by the orchestrator. This keeps cache coordination trivial: one snapshot,
1559
+ * one per-interface schema fetch per (sid, iid).
1560
+ */
1561
+ /** Distinct, sorted serviceIds (drops the empty `''` root). */
1562
+ function distinctServiceIds(entries) {
1563
+ return [...new Set(entries.map((e) => e.serviceId).filter((s) => s.length > 0))].sort();
1564
+ }
1565
+ /** Distinct, sorted interface ids across all services. */
1566
+ function distinctInterfaceIds(entries) {
1567
+ return [...new Set(entries.map((e) => e.interfaceId))].sort();
1568
+ }
1569
+ /** Distinct, sorted interface ids registered on a particular serviceId. */
1570
+ function interfacesOnService(entries, serviceId) {
1571
+ return [...new Set(entries.filter((e) => e.serviceId === serviceId).map((e) => e.interfaceId))].sort();
1572
+ }
1573
+ /**
1574
+ * Live source: walks the hub once (memoized for the instance lifetime),
1575
+ * then derives every entry / methods lookup from that single snapshot +
1576
+ * lazy `fetchSchema` calls. Each (sid, iid) schema is fetched at most once
1577
+ * per instance regardless of how many downstream queries reference it.
1578
+ */
1579
+ var ChannelDirectorySource = class {
1580
+ _channel;
1581
+ _walkPromise;
1582
+ _schemaCache = /* @__PURE__ */ new Map();
1583
+ constructor(_channel) {
1584
+ this._channel = _channel;
1585
+ }
1586
+ entries() {
1587
+ if (!this._walkPromise) this._walkPromise = walkHub(this._channel).then((listings) => listings.map((l) => ({
1588
+ serviceId: l.serviceId,
1589
+ interfaceId: l.interfaceId,
1590
+ hash: l.hash
1591
+ })));
1592
+ return this._walkPromise;
1593
+ }
1594
+ async methodsOnInterface(serviceId, interfaceId) {
1595
+ return (await this._fetchSchema(serviceId, interfaceId)).methods;
1596
+ }
1597
+ async paramNamesForMethod(serviceId, interfaceId, methodName) {
1598
+ return (await this._fetchSchema(serviceId, interfaceId)).paramsByMethod.get(methodName) ?? [];
1599
+ }
1600
+ _fetchSchema(serviceId, interfaceId) {
1601
+ const key = `${serviceId ?? ""}::${interfaceId}`;
1602
+ let cached = this._schemaCache.get(key);
1603
+ if (!cached) {
1604
+ cached = (async () => {
1605
+ const schema = await fetchSchema$1(this._channel, interfaceId, void 0, serviceId);
1606
+ const methods = Object.keys(schema.methods).sort();
1607
+ const paramsByMethod = /* @__PURE__ */ new Map();
1608
+ for (const [name, method] of Object.entries(schema.methods)) {
1609
+ const params = method.params;
1610
+ if (typeof params === "object" && params !== null && params.type === "object") {
1611
+ const props = params.properties ?? {};
1612
+ paramsByMethod.set(name, Object.keys(props));
1613
+ } else paramsByMethod.set(name, []);
1614
+ }
1615
+ return {
1616
+ methods,
1617
+ paramsByMethod
1618
+ };
1619
+ })();
1620
+ this._schemaCache.set(key, cached);
1621
+ }
1622
+ return cached;
1623
+ }
1624
+ };
1625
+ //#endregion
1626
+ //#region src/completions/parse.ts
1627
+ /** Whitespace per the simple POSIX rule (space, tab, newline). */
1628
+ function _isWs(ch) {
1629
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
1630
+ }
1631
+ function tokenize(line) {
1632
+ const tokens = [];
1633
+ let i = 0;
1634
+ while (i < line.length) {
1635
+ while (i < line.length && _isWs(line[i])) i++;
1636
+ if (i >= line.length) break;
1637
+ const start = i;
1638
+ const ch = line[i];
1639
+ let text = "";
1640
+ let quoted = false;
1641
+ if (ch === "\"" || ch === "'") {
1642
+ quoted = true;
1643
+ const q = ch;
1644
+ i++;
1645
+ while (i < line.length && line[i] !== q) text += line[i++];
1646
+ if (i < line.length) i++;
1647
+ } else while (i < line.length && !_isWs(line[i])) text += line[i++];
1648
+ tokens.push({
1649
+ text,
1650
+ start,
1651
+ end: i,
1652
+ quoted
1653
+ });
1654
+ }
1655
+ return tokens;
1656
+ }
1657
+ /**
1658
+ * Split `line` at `point` (0-indexed cursor position) into a previous-tokens
1659
+ * list + a possibly in-progress current word. Cursor inside or at the end of
1660
+ * a token = that token is the current word; cursor in whitespace = no current
1661
+ * token, new empty word at this position.
1662
+ */
1663
+ function parseLine(line, point) {
1664
+ const tokens = tokenize(line);
1665
+ const safePoint = Math.max(0, Math.min(line.length, point));
1666
+ if (_isWs(safePoint > 0 ? line[safePoint - 1] : " ") || safePoint === 0) return {
1667
+ tokens,
1668
+ tokensBefore: tokens.filter((t) => t.end <= safePoint),
1669
+ currentToken: void 0,
1670
+ currentWordPrefix: ""
1671
+ };
1672
+ const cur = tokens.find((t) => t.start <= safePoint - 1 && safePoint - 1 < t.end);
1673
+ if (!cur) return {
1674
+ tokens,
1675
+ tokensBefore: tokens.filter((t) => t.end <= safePoint),
1676
+ currentToken: void 0,
1677
+ currentWordPrefix: ""
1678
+ };
1679
+ return {
1680
+ tokens,
1681
+ tokensBefore: tokens.filter((t) => t.end <= cur.start),
1682
+ currentToken: cur,
1683
+ currentWordPrefix: cur.text.slice(0, safePoint - cur.start)
1684
+ };
1685
+ }
1686
+ //#endregion
1687
+ //#region src/completions/tree.ts
1688
+ const ENDPOINT_GLOBAL_OPTIONS = [
1689
+ {
1690
+ name: "--endpoint",
1691
+ takesValue: true,
1692
+ valueType: "free",
1693
+ description: "endpoint URI"
1694
+ },
1695
+ {
1696
+ name: "--endpoint-cmd",
1697
+ takesValue: true,
1698
+ valueType: "free"
1699
+ },
1700
+ {
1701
+ name: "--endpoint-cmd-stdio",
1702
+ takesValue: true,
1703
+ valueType: "free"
1704
+ },
1705
+ {
1706
+ name: "--endpoint-cmd-env",
1707
+ takesValue: true,
1708
+ valueType: "free"
1709
+ },
1710
+ {
1711
+ name: "--endpoint-cmd-cwd",
1712
+ takesValue: true,
1713
+ valueType: "free"
1714
+ },
1715
+ {
1716
+ name: "--endpoint-token",
1717
+ takesValue: true,
1718
+ valueType: "free"
1719
+ },
1720
+ {
1721
+ name: "--context",
1722
+ takesValue: true,
1723
+ valueType: "free"
1724
+ },
1725
+ {
1726
+ name: "--context-set",
1727
+ takesValue: false
1728
+ },
1729
+ {
1730
+ name: "--new-context",
1731
+ takesValue: true,
1732
+ valueType: "free"
1733
+ },
1734
+ {
1735
+ name: "--schema",
1736
+ takesValue: true,
1737
+ valueType: "free"
1738
+ },
1739
+ {
1740
+ name: "--validation",
1741
+ takesValue: true,
1742
+ valueType: "free"
1743
+ },
1744
+ {
1745
+ name: "--use-env",
1746
+ takesValue: false
1747
+ },
1748
+ {
1749
+ name: "--no-use-env",
1750
+ takesValue: false
1751
+ },
1752
+ {
1753
+ name: "--provision-identity",
1754
+ takesValue: false
1755
+ },
1756
+ {
1757
+ name: "--provision-identity-slot",
1758
+ takesValue: true,
1759
+ valueType: "free"
1760
+ },
1761
+ {
1762
+ name: "--principal",
1763
+ takesValue: true,
1764
+ valueType: "free",
1765
+ description: "\"managed\", \"user:<id>\", or \"file:<path>\""
1766
+ }
1767
+ ];
1768
+ const HUB_GLOBAL_OPTIONS = [...ENDPOINT_GLOBAL_OPTIONS, {
1769
+ name: "--config",
1770
+ takesValue: true,
1771
+ valueType: "free"
1772
+ }];
1773
+ const SHARED_COMMANDS = [
1774
+ {
1775
+ name: "ls",
1776
+ description: "List services and interfaces.",
1777
+ positionals: [],
1778
+ options: [
1779
+ {
1780
+ name: "--interface",
1781
+ takesValue: true,
1782
+ valueType: "interfaceId"
1783
+ },
1784
+ {
1785
+ name: "--service",
1786
+ takesValue: true,
1787
+ valueType: "serviceId"
1788
+ },
1789
+ {
1790
+ name: "--depth",
1791
+ takesValue: true,
1792
+ valueType: "free"
1793
+ },
1794
+ {
1795
+ name: "--with-members",
1796
+ takesValue: false
1797
+ },
1798
+ {
1799
+ name: "--json",
1800
+ takesValue: false
1801
+ },
1802
+ {
1803
+ name: "--dump",
1804
+ takesValue: true,
1805
+ valueType: "free"
1806
+ },
1807
+ {
1808
+ name: "--dump-patches",
1809
+ takesValue: true,
1810
+ valueType: "free"
1811
+ },
1812
+ {
1813
+ name: "--stream",
1814
+ takesValue: false
1815
+ },
1816
+ {
1817
+ name: "--watch",
1818
+ takesValue: false
1819
+ }
1820
+ ]
1821
+ },
1822
+ {
1823
+ name: "defaults",
1824
+ description: "Print the preset service / interface.",
1825
+ positionals: [],
1826
+ options: [{
1827
+ name: "--json",
1828
+ takesValue: false
1829
+ }]
1830
+ },
1831
+ {
1832
+ name: "schema",
1833
+ description: "Inspect, hash, and compare interface schemas.",
1834
+ positionals: [{
1835
+ name: "interfaceRef",
1836
+ type: "interfaceRef"
1837
+ }],
1838
+ options: [],
1839
+ subcommands: [
1840
+ {
1841
+ name: "show",
1842
+ description: "Print an interface schema.",
1843
+ positionals: [{
1844
+ name: "interfaceRef",
1845
+ type: "interfaceRef"
1846
+ }],
1847
+ options: [
1848
+ {
1849
+ name: "--method",
1850
+ takesValue: true,
1851
+ valueType: "free"
1852
+ },
1853
+ {
1854
+ name: "--service",
1855
+ takesValue: true,
1856
+ valueType: "serviceId"
1857
+ },
1858
+ {
1859
+ name: "--json",
1860
+ takesValue: false
1861
+ }
1862
+ ]
1863
+ },
1864
+ {
1865
+ name: "hash",
1866
+ description: "Hash a local schema.",
1867
+ positionals: [{
1868
+ name: "schema",
1869
+ type: "free"
1870
+ }],
1871
+ options: []
1872
+ },
1873
+ {
1874
+ name: "check-compat",
1875
+ description: "Compare a local schema against the live one.",
1876
+ positionals: [{
1877
+ name: "interfaceId",
1878
+ type: "interfaceId"
1879
+ }, {
1880
+ name: "local",
1881
+ type: "free"
1882
+ }],
1883
+ options: []
1884
+ }
1885
+ ]
1886
+ },
1887
+ {
1888
+ name: "call",
1889
+ description: "Invoke a request.",
1890
+ positionals: [{
1891
+ name: "methodRef",
1892
+ type: "methodRef"
1893
+ }],
1894
+ options: [
1895
+ {
1896
+ name: "--params",
1897
+ takesValue: true,
1898
+ valueType: "free"
1899
+ },
1900
+ {
1901
+ name: "--param",
1902
+ takesValue: true,
1903
+ valueType: "free"
1904
+ },
1905
+ {
1906
+ name: "--no-validate",
1907
+ takesValue: false
1908
+ }
1909
+ ]
1910
+ },
1911
+ {
1912
+ name: "notify",
1913
+ description: "Fire a notification (no response).",
1914
+ positionals: [{
1915
+ name: "methodRef",
1916
+ type: "methodRef"
1917
+ }],
1918
+ options: [
1919
+ {
1920
+ name: "--params",
1921
+ takesValue: true,
1922
+ valueType: "free"
1923
+ },
1924
+ {
1925
+ name: "--param",
1926
+ takesValue: true,
1927
+ valueType: "free"
1928
+ },
1929
+ {
1930
+ name: "--no-validate",
1931
+ takesValue: false
1932
+ }
1933
+ ]
1934
+ },
1935
+ {
1936
+ name: "batch",
1937
+ description: "Run sequential calls and notifications.",
1938
+ positionals: [],
1939
+ variadic: "free",
1940
+ options: []
1941
+ },
1942
+ {
1943
+ name: "connection",
1944
+ description: "Manage persistent RPC connections.",
1945
+ positionals: [],
1946
+ options: [],
1947
+ subcommands: [
1948
+ {
1949
+ name: "create",
1950
+ description: "Create a persistent connection.",
1951
+ positionals: [],
1952
+ options: [
1953
+ {
1954
+ name: "--timeout",
1955
+ takesValue: true,
1956
+ valueType: "free"
1957
+ },
1958
+ {
1959
+ name: "--ttl",
1960
+ takesValue: true,
1961
+ valueType: "free"
1962
+ },
1963
+ {
1964
+ name: "--notification-limit",
1965
+ takesValue: true,
1966
+ valueType: "free"
1967
+ }
1968
+ ]
1969
+ },
1970
+ {
1971
+ name: "status",
1972
+ description: "Show persistent connection status.",
1973
+ positionals: [],
1974
+ options: []
1975
+ },
1976
+ {
1977
+ name: "notifications",
1978
+ description: "Read buffered server notifications.",
1979
+ positionals: [],
1980
+ options: [
1981
+ {
1982
+ name: "--after",
1983
+ takesValue: true,
1984
+ valueType: "free"
1985
+ },
1986
+ {
1987
+ name: "--wait",
1988
+ takesValue: true,
1989
+ valueType: "free"
1990
+ },
1991
+ {
1992
+ name: "--follow",
1993
+ takesValue: false
1994
+ }
1995
+ ]
1996
+ },
1997
+ {
1998
+ name: "destroy",
1999
+ description: "Destroy a persistent connection.",
2000
+ positionals: [],
2001
+ options: []
2002
+ }
2003
+ ]
2004
+ },
2005
+ {
2006
+ name: "context",
2007
+ description: "Show how the active context resolves, or modify its defaults.",
2008
+ positionals: [],
2009
+ options: [],
2010
+ subcommands: [
2011
+ {
2012
+ name: "show",
2013
+ description: "Show stored context defaults only.",
2014
+ positionals: [],
2015
+ options: []
2016
+ },
2017
+ {
2018
+ name: "list",
2019
+ description: "List stored contexts.",
2020
+ positionals: [],
2021
+ options: []
2022
+ },
2023
+ {
2024
+ name: "set",
2025
+ description: "Set values on the selected context.",
2026
+ positionals: [],
2027
+ options: [{
2028
+ name: "--unset",
2029
+ takesValue: true,
2030
+ valueType: "free"
2031
+ }]
2032
+ },
2033
+ {
2034
+ name: "remove",
2035
+ description: "Remove the selected context.",
2036
+ positionals: [],
2037
+ options: []
2038
+ }
2039
+ ]
2040
+ },
2041
+ {
2042
+ name: "ping",
2043
+ description: "One reflection round-trip; prints latency.",
2044
+ positionals: [],
2045
+ options: []
2046
+ },
2047
+ {
2048
+ name: "ui",
2049
+ description: "Launch the terminal UI.",
2050
+ positionals: [],
2051
+ options: []
2052
+ },
2053
+ {
2054
+ name: "completions",
2055
+ description: "Print a shell completion script for the requested shell.",
2056
+ positionals: [{
2057
+ name: "shell",
2058
+ type: "shell"
2059
+ }],
2060
+ options: []
2061
+ },
2062
+ {
2063
+ name: "_complete",
2064
+ description: "(internal) Emit completion candidates for a partial command line.",
2065
+ positionals: [],
2066
+ options: [{
2067
+ name: "--line",
2068
+ takesValue: true,
2069
+ valueType: "free"
2070
+ }, {
2071
+ name: "--point",
2072
+ takesValue: true,
2073
+ valueType: "free"
2074
+ }],
2075
+ hidden: true
2076
+ },
2077
+ {
2078
+ name: "connect",
2079
+ description: "Legacy alias for connection create.",
2080
+ positionals: [],
2081
+ options: [
2082
+ {
2083
+ name: "--timeout",
2084
+ takesValue: true,
2085
+ valueType: "free"
2086
+ },
2087
+ {
2088
+ name: "--ttl",
2089
+ takesValue: true,
2090
+ valueType: "free"
2091
+ },
2092
+ {
2093
+ name: "--notification-limit",
2094
+ takesValue: true,
2095
+ valueType: "free"
2096
+ }
2097
+ ]
2098
+ },
2099
+ {
2100
+ name: "connection-status",
2101
+ description: "Legacy alias for connection status.",
2102
+ positionals: [],
2103
+ options: []
2104
+ },
2105
+ {
2106
+ name: "notifications",
2107
+ description: "Legacy alias for connection notifications.",
2108
+ positionals: [],
2109
+ options: [
2110
+ {
2111
+ name: "--after",
2112
+ takesValue: true,
2113
+ valueType: "free"
2114
+ },
2115
+ {
2116
+ name: "--wait",
2117
+ takesValue: true,
2118
+ valueType: "free"
2119
+ },
2120
+ {
2121
+ name: "--follow",
2122
+ takesValue: false
2123
+ }
2124
+ ]
2125
+ },
2126
+ {
2127
+ name: "disconnect",
2128
+ description: "Legacy alias for connection destroy.",
2129
+ positionals: [],
2130
+ options: []
2131
+ },
2132
+ {
2133
+ name: "hash",
2134
+ description: "Legacy alias for schema hash.",
2135
+ positionals: [{
2136
+ name: "schema",
2137
+ type: "free"
2138
+ }],
2139
+ options: []
2140
+ },
2141
+ {
2142
+ name: "check-compat",
2143
+ description: "Legacy alias for schema check-compat.",
2144
+ positionals: [{
2145
+ name: "interfaceId",
2146
+ type: "interfaceId"
2147
+ }, {
2148
+ name: "local",
2149
+ type: "free"
2150
+ }],
2151
+ options: []
2152
+ }
2153
+ ];
2154
+ const HUB_ONLY_COMMANDS = [
2155
+ {
2156
+ name: "topology",
2157
+ description: "Inspect the merged transport topology.",
2158
+ positionals: [],
2159
+ options: [
2160
+ {
2161
+ name: "--source",
2162
+ takesValue: true,
2163
+ valueType: "serviceId"
2164
+ },
2165
+ {
2166
+ name: "--depth",
2167
+ takesValue: true,
2168
+ valueType: "free"
2169
+ },
2170
+ {
2171
+ name: "--node",
2172
+ takesValue: true,
2173
+ valueType: "free"
2174
+ },
2175
+ {
2176
+ name: "--service",
2177
+ takesValue: true,
2178
+ valueType: "serviceId"
2179
+ },
2180
+ {
2181
+ name: "--kind",
2182
+ takesValue: true,
2183
+ valueType: "free"
2184
+ },
2185
+ {
2186
+ name: "--search",
2187
+ takesValue: true,
2188
+ valueType: "free"
2189
+ },
2190
+ {
2191
+ name: "--format",
2192
+ takesValue: true,
2193
+ valueType: "free"
2194
+ },
2195
+ {
2196
+ name: "--json",
2197
+ takesValue: false
2198
+ },
2199
+ {
2200
+ name: "--stream",
2201
+ takesValue: false
2202
+ },
2203
+ {
2204
+ name: "--watch",
2205
+ takesValue: false
2206
+ }
2207
+ ],
2208
+ subcommands: [{
2209
+ name: "participants",
2210
+ description: "List participant nodes.",
2211
+ positionals: [],
2212
+ options: [{
2213
+ name: "--search",
2214
+ takesValue: true,
2215
+ valueType: "free"
2216
+ }, {
2217
+ name: "--json",
2218
+ takesValue: false
2219
+ }]
2220
+ }]
2221
+ },
2222
+ {
2223
+ name: "traffic",
2224
+ description: "Observe participant traffic.",
2225
+ positionals: [],
2226
+ options: [],
2227
+ subcommands: [{
2228
+ name: "watch",
2229
+ description: "Watch node-wide traffic.",
2230
+ positionals: [],
2231
+ options: [
2232
+ {
2233
+ name: "--search",
2234
+ takesValue: true,
2235
+ valueType: "free"
2236
+ },
2237
+ {
2238
+ name: "--node",
2239
+ takesValue: true,
2240
+ valueType: "free"
2241
+ },
2242
+ {
2243
+ name: "--method",
2244
+ takesValue: true,
2245
+ valueType: "free"
2246
+ },
2247
+ {
2248
+ name: "--payloads",
2249
+ takesValue: true,
2250
+ valueType: "free"
2251
+ },
2252
+ {
2253
+ name: "--format",
2254
+ takesValue: true,
2255
+ valueType: "free"
2256
+ },
2257
+ {
2258
+ name: "--resume",
2259
+ takesValue: true,
2260
+ valueType: "free"
2261
+ }
2262
+ ]
2263
+ }]
2264
+ },
2265
+ {
2266
+ name: "identity",
2267
+ description: "Inspect the persistent principal used by connected CLI commands.",
2268
+ positionals: [],
2269
+ options: [],
2270
+ subcommands: [{
2271
+ name: "show",
2272
+ description: "Show the resolved principal.",
2273
+ positionals: [],
2274
+ options: [{
2275
+ name: "--json",
2276
+ takesValue: false
2277
+ }]
2278
+ }]
2279
+ },
2280
+ {
2281
+ name: "approval",
2282
+ description: "Inspect and decide pending Hub access approval requests.",
2283
+ positionals: [],
2284
+ options: [],
2285
+ subcommands: [
2286
+ {
2287
+ name: "requests",
2288
+ description: "List pending requests.",
2289
+ positionals: [],
2290
+ options: [{
2291
+ name: "--json",
2292
+ takesValue: false
2293
+ }]
2294
+ },
2295
+ {
2296
+ name: "approve",
2297
+ description: "Approve a pending request.",
2298
+ positionals: [{
2299
+ name: "request-id",
2300
+ type: "free"
2301
+ }],
2302
+ options: [{
2303
+ name: "--json",
2304
+ takesValue: false
2305
+ }]
2306
+ },
2307
+ {
2308
+ name: "deny",
2309
+ description: "Deny a pending request.",
2310
+ positionals: [{
2311
+ name: "request-id",
2312
+ type: "free"
2313
+ }],
2314
+ options: [{
2315
+ name: "--reason",
2316
+ takesValue: true,
2317
+ valueType: "free"
2318
+ }, {
2319
+ name: "--json",
2320
+ takesValue: false
2321
+ }]
2322
+ },
2323
+ {
2324
+ name: "ui",
2325
+ description: "Open the approval UI.",
2326
+ positionals: [],
2327
+ options: []
2328
+ }
2329
+ ]
2330
+ },
2331
+ {
2332
+ name: "serve",
2333
+ description: "Run a configured hub.",
2334
+ positionals: [{
2335
+ name: "config",
2336
+ type: "free"
2337
+ }],
2338
+ options: [{
2339
+ name: "--print-schema",
2340
+ takesValue: false
2341
+ }, {
2342
+ name: "--cmd-interactive",
2343
+ takesValue: false
2344
+ }]
2345
+ },
2346
+ {
2347
+ name: "tunnel",
2348
+ description: "Claim a serviceId on the source hub and forward to a target.",
2349
+ positionals: [{
2350
+ name: "serviceId",
2351
+ type: "serviceId"
2352
+ }],
2353
+ options: [
2354
+ {
2355
+ name: "--target-endpoint",
2356
+ takesValue: true,
2357
+ valueType: "free"
2358
+ },
2359
+ {
2360
+ name: "--target-endpoint-cmd",
2361
+ takesValue: true,
2362
+ valueType: "free"
2363
+ },
2364
+ {
2365
+ name: "--target-endpoint-cmd-stdio",
2366
+ takesValue: true,
2367
+ valueType: "free"
2368
+ },
2369
+ {
2370
+ name: "--target-endpoint-cmd-env",
2371
+ takesValue: true,
2372
+ valueType: "free"
2373
+ },
2374
+ {
2375
+ name: "--target-endpoint-cmd-cwd",
2376
+ takesValue: true,
2377
+ valueType: "free"
2378
+ },
2379
+ {
2380
+ name: "--target-endpoint-token",
2381
+ takesValue: true,
2382
+ valueType: "free"
2383
+ }
2384
+ ]
2385
+ },
2386
+ {
2387
+ name: "connect-as",
2388
+ description: "Mint a slot identity from a connectionTokenBinder service and splice a target onto the hub as that slot.",
2389
+ positionals: [{
2390
+ name: "binderServiceId",
2391
+ type: "serviceId"
2392
+ }, {
2393
+ name: "slot",
2394
+ type: "free"
2395
+ }],
2396
+ options: [
2397
+ {
2398
+ name: "--hub-endpoint",
2399
+ takesValue: true,
2400
+ valueType: "free"
2401
+ },
2402
+ {
2403
+ name: "--target-endpoint",
2404
+ takesValue: true,
2405
+ valueType: "free"
2406
+ },
2407
+ {
2408
+ name: "--target-endpoint-cmd",
2409
+ takesValue: true,
2410
+ valueType: "free"
2411
+ },
2412
+ {
2413
+ name: "--target-endpoint-cmd-stdio",
2414
+ takesValue: true,
2415
+ valueType: "free"
2416
+ },
2417
+ {
2418
+ name: "--target-endpoint-cmd-env",
2419
+ takesValue: true,
2420
+ valueType: "free"
2421
+ },
2422
+ {
2423
+ name: "--target-endpoint-cmd-cwd",
2424
+ takesValue: true,
2425
+ valueType: "free"
2426
+ },
2427
+ {
2428
+ name: "--target-endpoint-token",
2429
+ takesValue: true,
2430
+ valueType: "free"
2431
+ },
2432
+ {
2433
+ name: "--granted-service-id",
2434
+ takesValue: true,
2435
+ valueType: "serviceId"
2436
+ }
2437
+ ]
2438
+ },
2439
+ {
2440
+ name: "mcp-forward",
2441
+ description: "Expose an MCP server through the hub.",
2442
+ positionals: [],
2443
+ variadic: "free",
2444
+ options: [{
2445
+ name: "--serviceId",
2446
+ takesValue: true,
2447
+ valueType: "serviceId"
2448
+ }, {
2449
+ name: "--env",
2450
+ takesValue: true,
2451
+ valueType: "free"
2452
+ }]
2453
+ },
2454
+ {
2455
+ name: "logout",
2456
+ description: "Delete the stored CLI identity.",
2457
+ positionals: [],
2458
+ options: []
2459
+ }
2460
+ ];
2461
+ const HUB_COMMAND_TREE = {
2462
+ globalOptions: HUB_GLOBAL_OPTIONS,
2463
+ subcommands: [...SHARED_COMMANDS, ...HUB_ONLY_COMMANDS]
2464
+ };
2465
+ const RPC_COMMAND_TREE = {
2466
+ globalOptions: ENDPOINT_GLOBAL_OPTIONS,
2467
+ subcommands: [...SHARED_COMMANDS, {
2468
+ name: "hub",
2469
+ description: "Use the hub profile.",
2470
+ positionals: [],
2471
+ options: [{
2472
+ name: "--config",
2473
+ takesValue: true,
2474
+ valueType: "free"
2475
+ }],
2476
+ subcommands: [...SHARED_COMMANDS, ...HUB_ONLY_COMMANDS]
2477
+ }]
2478
+ };
2479
+ /** Hub-profile tree retained as the default for direct resolver consumers. */
2480
+ const COMMAND_TREE = HUB_COMMAND_TREE;
2481
+ /**
2482
+ * Look up a flag by its long-form name from the leaf command through its
2483
+ * ancestors, then from the global options.
2484
+ */
2485
+ function findFlag(name, commandPath, tree) {
2486
+ const eq = name.indexOf("=");
2487
+ const flagName = eq >= 0 ? name.slice(0, eq) : name;
2488
+ for (let index = commandPath.length - 1; index >= 0; index--) {
2489
+ const flag = commandPath[index].options.find((option) => option.name === flagName);
2490
+ if (flag !== void 0) return flag;
2491
+ }
2492
+ return tree.globalOptions.find((option) => option.name === flagName);
2493
+ }
2494
+ //#endregion
2495
+ //#region src/completions/resolve.ts
2496
+ /**
2497
+ * Walk the tokens before the cursor, tracking subcommand selection, current
2498
+ * positional index, and whether the next token is the value for a flag.
2499
+ * Returns the slot the cursor itself is in plus the surrounding context.
2500
+ */
2501
+ function resolveSlot(parsed, tree) {
2502
+ const tokens = parsed.tokensBefore;
2503
+ let subcommand;
2504
+ const commandPath = [];
2505
+ let positionalIndex = 0;
2506
+ let expectingValueFor;
2507
+ const seenFlagValues = /* @__PURE__ */ new Map();
2508
+ const seenPositionals = [];
2509
+ for (let i = 1; i < tokens.length; i++) {
2510
+ const t = tokens[i].text;
2511
+ if (expectingValueFor !== void 0) {
2512
+ seenFlagValues.set(expectingValueFor.name, t);
2513
+ expectingValueFor = void 0;
2514
+ continue;
2515
+ }
2516
+ if (t.startsWith("-") && t.length > 1) {
2517
+ const eq = t.indexOf("=");
2518
+ const flag = findFlag(eq >= 0 ? t.slice(0, eq) : t, commandPath, tree);
2519
+ if (flag?.takesValue) {
2520
+ if (eq >= 0) seenFlagValues.set(flag.name, t.slice(eq + 1));
2521
+ else expectingValueFor = flag;
2522
+ } else if (flag) seenFlagValues.set(flag.name, void 0);
2523
+ continue;
2524
+ }
2525
+ if (subcommand === void 0) {
2526
+ const sub = tree.subcommands.find((s) => s.name === t);
2527
+ if (!sub) return {
2528
+ slot: { kind: "none" },
2529
+ subcommand: void 0,
2530
+ commandPath,
2531
+ seenFlagValues,
2532
+ seenPositionals
2533
+ };
2534
+ subcommand = sub;
2535
+ commandPath.push(sub);
2536
+ positionalIndex = 0;
2537
+ } else if (subcommand.subcommands !== void 0 && seenPositionals.length === 0) {
2538
+ const child = subcommand.subcommands.find((candidate) => candidate.name === t);
2539
+ if (child === void 0) {
2540
+ if (subcommand.positionals.length === 0 && subcommand.variadic === void 0) return {
2541
+ slot: { kind: "none" },
2542
+ subcommand,
2543
+ commandPath,
2544
+ seenFlagValues,
2545
+ seenPositionals
2546
+ };
2547
+ seenPositionals.push(t);
2548
+ positionalIndex++;
2549
+ continue;
2550
+ }
2551
+ subcommand = child;
2552
+ commandPath.push(child);
2553
+ positionalIndex = 0;
2554
+ seenPositionals.length = 0;
2555
+ } else {
2556
+ seenPositionals.push(t);
2557
+ positionalIndex++;
2558
+ }
2559
+ }
2560
+ const word = parsed.currentWordPrefix;
2561
+ if (expectingValueFor !== void 0) return {
2562
+ slot: {
2563
+ kind: "flag-value",
2564
+ flag: expectingValueFor,
2565
+ subcommand
2566
+ },
2567
+ subcommand,
2568
+ commandPath,
2569
+ seenFlagValues,
2570
+ seenPositionals
2571
+ };
2572
+ if (word.startsWith("-")) {
2573
+ const eq = word.indexOf("=");
2574
+ if (eq >= 0) {
2575
+ const flag = findFlag(word.slice(0, eq), commandPath, tree);
2576
+ if (flag?.takesValue) return {
2577
+ slot: {
2578
+ kind: "flag-value",
2579
+ flag,
2580
+ subcommand
2581
+ },
2582
+ subcommand,
2583
+ commandPath,
2584
+ seenFlagValues,
2585
+ seenPositionals
2586
+ };
2587
+ }
2588
+ return {
2589
+ slot: {
2590
+ kind: "flag-name",
2591
+ subcommand
2592
+ },
2593
+ subcommand,
2594
+ commandPath,
2595
+ seenFlagValues,
2596
+ seenPositionals
2597
+ };
2598
+ }
2599
+ if (subcommand === void 0) return {
2600
+ slot: {
2601
+ kind: "subcommand",
2602
+ parent: void 0
2603
+ },
2604
+ subcommand: void 0,
2605
+ commandPath,
2606
+ seenFlagValues,
2607
+ seenPositionals
2608
+ };
2609
+ if (subcommand.subcommands !== void 0 && seenPositionals.length === 0) {
2610
+ const childMatches = subcommand.subcommands.some((child) => child.name.startsWith(word));
2611
+ if (word.length > 0 && !childMatches && subcommand.positionals[positionalIndex] !== void 0) return {
2612
+ slot: {
2613
+ kind: "positional",
2614
+ type: subcommand.positionals[positionalIndex].type,
2615
+ subcommand,
2616
+ index: positionalIndex
2617
+ },
2618
+ subcommand,
2619
+ commandPath,
2620
+ seenFlagValues,
2621
+ seenPositionals
2622
+ };
2623
+ return {
2624
+ slot: {
2625
+ kind: "subcommand",
2626
+ parent: subcommand
2627
+ },
2628
+ subcommand,
2629
+ commandPath,
2630
+ seenFlagValues,
2631
+ seenPositionals
2632
+ };
2633
+ }
2634
+ const positional = subcommand.positionals[positionalIndex];
2635
+ if (positional !== void 0) return {
2636
+ slot: {
2637
+ kind: "positional",
2638
+ type: positional.type,
2639
+ subcommand,
2640
+ index: positionalIndex
2641
+ },
2642
+ subcommand,
2643
+ commandPath,
2644
+ seenFlagValues,
2645
+ seenPositionals
2646
+ };
2647
+ if (subcommand.variadic !== void 0) return {
2648
+ slot: {
2649
+ kind: "positional",
2650
+ type: subcommand.variadic,
2651
+ subcommand,
2652
+ index: positionalIndex
2653
+ },
2654
+ subcommand,
2655
+ commandPath,
2656
+ seenFlagValues,
2657
+ seenPositionals
2658
+ };
2659
+ return {
2660
+ slot: { kind: "none" },
2661
+ subcommand,
2662
+ commandPath,
2663
+ seenFlagValues,
2664
+ seenPositionals
2665
+ };
2666
+ }
2667
+ //#endregion
2668
+ //#region src/cliInvocation.ts
2669
+ const INHERITED_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
2670
+ "--endpoint",
2671
+ "--endpoint-cmd",
2672
+ "--endpoint-cmd-stdio",
2673
+ "--endpoint-cmd-env",
2674
+ "--endpoint-token",
2675
+ "--endpoint-cmd-cwd",
2676
+ "--config",
2677
+ "-c",
2678
+ "--provision-identity-slot",
2679
+ "--principal",
2680
+ "--context",
2681
+ "--new-context",
2682
+ "--schema",
2683
+ "--validation"
2684
+ ]);
2685
+ const LEGACY_COMMANDS = {
2686
+ connect: ["connection", "create"],
2687
+ "connection-status": ["connection", "status"],
2688
+ notifications: ["connection", "notifications"],
2689
+ disconnect: ["connection", "destroy"],
2690
+ hash: ["schema", "hash"],
2691
+ "check-compat": ["schema", "check-compat"]
2692
+ };
2693
+ const SCHEMA_SUBCOMMANDS = /* @__PURE__ */ new Set([
2694
+ "show",
2695
+ "hash",
2696
+ "check-compat",
2697
+ "help"
2698
+ ]);
2699
+ function resolveCliInvocation(rawArgv, executablePath) {
2700
+ const executable = normalizeCliExecutable(executablePath);
2701
+ let profile = executable === "hub" ? "hub" : "rpc";
2702
+ let argv = [...rawArgv];
2703
+ let commandIndex = findCommandIndex(argv);
2704
+ if (profile === "rpc" && commandIndex !== void 0 && argv[commandIndex] === "hub") {
2705
+ argv.splice(commandIndex, 1);
2706
+ profile = "hub";
2707
+ commandIndex = findCommandIndex(argv);
2708
+ }
2709
+ if (commandIndex !== void 0) {
2710
+ const command = argv[commandIndex];
2711
+ const replacement = LEGACY_COMMANDS[command];
2712
+ if (replacement !== void 0) argv.splice(commandIndex, 1, ...replacement);
2713
+ else if (command === "schema") {
2714
+ const schemaMemberIndex = findCommandIndex(argv, commandIndex + 1);
2715
+ const schemaMember = schemaMemberIndex === void 0 ? void 0 : argv[schemaMemberIndex];
2716
+ if (schemaMember !== void 0 && !SCHEMA_SUBCOMMANDS.has(schemaMember)) argv.splice(commandIndex + 1, 0, "show");
2717
+ }
2718
+ }
2719
+ const baseName = executable === "hub" ? "hub" : executable === "rpc" ? "rpc" : "linkrpc";
2720
+ return {
2721
+ argv,
2722
+ profile,
2723
+ programName: profile === "hub" && executable !== "hub" ? `${baseName} hub` : baseName
2724
+ };
2725
+ }
2726
+ function normalizeCliExecutable(executablePath) {
2727
+ if (executablePath === void 0) return "linkrpc";
2728
+ return (executablePath.split(/[\\/]/).pop() ?? executablePath).replace(/\.(?:cmd|exe|js|mjs|cjs)$/i, "");
2729
+ }
2730
+ function findCommandIndex(argv, start = 0) {
2731
+ for (let index = start; index < argv.length; index++) {
2732
+ const arg = argv[index];
2733
+ if (arg === "--") return void 0;
2734
+ if (arg.startsWith("--") && arg.includes("=")) continue;
2735
+ if (INHERITED_OPTIONS_WITH_VALUE.has(arg)) {
2736
+ index++;
2737
+ continue;
2738
+ }
2739
+ if (arg.startsWith("-")) continue;
2740
+ return index;
2741
+ }
2742
+ }
2743
+ //#endregion
2744
+ //#region src/completions/complete.ts
2745
+ /**
2746
+ * Top-level completion orchestrator. Takes a command line + cursor position,
2747
+ * resolves what's being completed, and returns the candidate list. The
2748
+ * resolver is the only thing that needs to be wire-aware (subcommand /
2749
+ * positional / flag / flag-value); everything past that is either a static
2750
+ * filter against the {@link CommandTree} or a query against a
2751
+ * {@link DirectorySource}.
2752
+ */
2753
+ const SHELLS = [
2754
+ "powershell",
2755
+ "bash",
2756
+ "zsh",
2757
+ "fish"
2758
+ ];
2759
+ /**
2760
+ * Resolve completion candidates for the cursor at `point` in `line`.
2761
+ *
2762
+ * The slot kinds returned by {@link resolveSlot} map to:
2763
+ * - `subcommand` / `flag-name` / static-typed `flag-value` / static-typed
2764
+ * `positional` → filter against the {@link CommandTree}
2765
+ * - `flag-value` / `positional` with a dynamic slot type → call into
2766
+ * `directory` (skipped when `directory` is `undefined`)
2767
+ * - `flag-name` under `call`/`notify` with a methodRef typed → adds
2768
+ * `--p:<name>` shortcuts for the live params
2769
+ * - `none` → empty
2770
+ *
2771
+ * Candidates are always prefix-filtered so the caller (PowerShell) can
2772
+ * enumerate the result as-is. The output is sorted and de-duplicated.
2773
+ */
2774
+ async function complete(opts) {
2775
+ const parsed = parseLine(opts.line, opts.point);
2776
+ const executable = normalizeCliExecutable(parsed.tokensBefore[0]?.text);
2777
+ const tree = opts.tree ?? (executable === "hub" ? HUB_COMMAND_TREE : RPC_COMMAND_TREE);
2778
+ const ctx = resolveSlot(parsed, tree);
2779
+ const prefix = parsed.currentWordPrefix;
2780
+ const stat = _staticCandidates(ctx, prefix, tree);
2781
+ const dyn = opts.directory ? await _dynamicCandidates(ctx, prefix, opts.directory) : [];
2782
+ const seen = /* @__PURE__ */ new Set();
2783
+ const merged = [];
2784
+ for (const c of [...stat, ...dyn]) {
2785
+ if (seen.has(c.text)) continue;
2786
+ seen.add(c.text);
2787
+ merged.push(c);
2788
+ }
2789
+ return merged.sort((a, b) => a.text < b.text ? -1 : a.text > b.text ? 1 : 0);
2790
+ }
2791
+ function _staticCandidates(ctx, prefix, tree) {
2792
+ const slot = ctx.slot;
2793
+ if (slot.kind === "subcommand") return (slot.parent?.subcommands ?? tree.subcommands).filter((s) => !s.hidden && s.name.startsWith(prefix)).map((s) => ({
2794
+ text: s.name,
2795
+ tooltip: s.description
2796
+ }));
2797
+ if (slot.kind === "flag-name") {
2798
+ const flags = [...ctx.commandPath.flatMap((command) => command.options), ...tree.globalOptions];
2799
+ const eq = prefix.indexOf("=");
2800
+ const lookupPrefix = eq >= 0 ? prefix.slice(0, eq) : prefix;
2801
+ return flags.filter((f) => f.name.startsWith(lookupPrefix)).map((f) => ({
2802
+ text: f.name,
2803
+ tooltip: f.description
2804
+ }));
2805
+ }
2806
+ if (slot.kind === "flag-value" || slot.kind === "positional") {
2807
+ if ((slot.kind === "flag-value" ? slot.flag.valueType ?? "free" : slot.type) === "shell") return SHELLS.filter((s) => s.startsWith(prefix)).map((s) => ({ text: s }));
2808
+ }
2809
+ return [];
2810
+ }
2811
+ async function _dynamicCandidates(ctx, prefix, dir) {
2812
+ const slot = ctx.slot;
2813
+ if ((slot.kind === "flag-name" || slot.kind === "none" && (prefix === "" || prefix.startsWith("-"))) && (ctx.subcommand?.name === "call" || ctx.subcommand?.name === "notify") && ctx.seenPositionals.length >= 1) {
2814
+ const methodRef = ctx.seenPositionals[0];
2815
+ const ref = _parseMethodRefForCompletion(methodRef);
2816
+ if (ref !== void 0 && ref.interfaceId !== void 0) return (await _safeParams(dir, ref.serviceId, ref.interfaceId, ref.methodName)).map((n) => `--p:${n}`).filter((c) => c.startsWith(prefix)).map((c) => ({
2817
+ text: c,
2818
+ tooltip: `method param: ${c.slice(4)}`
2819
+ }));
2820
+ }
2821
+ let type;
2822
+ if (slot.kind === "flag-value") type = slot.flag.valueType;
2823
+ else if (slot.kind === "positional") type = slot.type;
2824
+ if (!type) return [];
2825
+ if (type === "serviceId") return distinctServiceIds(await dir.entries()).filter((s) => s.startsWith(prefix)).map((s) => ({ text: s }));
2826
+ if (type === "interfaceId" || type === "interfaceRef") return distinctInterfaceIds(await dir.entries()).filter((i) => i.startsWith(prefix)).map((i) => ({ text: i }));
2827
+ if (type === "methodRef") return _completeMethodRef(prefix, dir);
2828
+ return [];
2829
+ }
2830
+ /**
2831
+ * Complete a `[serviceId::][interfaceId::]methodName[@hash]` reference. The
2832
+ * three forms are disambiguated by how many `::` the prefix contains so far.
2833
+ * Each TAB-cycle is a complete word (no trailing `::`) — the user adds the
2834
+ * next separator themselves to drill in. That keeps PowerShell's TAB-cycle
2835
+ * advancing through peer candidates instead of re-stalling on a separator.
2836
+ *
2837
+ * 0 sep: `vscode.window` → serviceIds + root-hosted interfaceIds
2838
+ * (NOT service-bound interfaces, since
2839
+ * calling them bare won't route)
2840
+ * 1 sep: `azure-cli::Runner` → if `azure-cli` is a serviceId, suggest
2841
+ * `azure-cli::<iface>`; if it's also a
2842
+ * root interfaceId, suggest its methods
2843
+ * (form-2). Skip form-2 entirely when
2844
+ * `azure-cli` isn't root-hosted — fetching
2845
+ * a schema for any typed string would
2846
+ * spam the hub on every keystroke.
2847
+ * 2 sep: `azure-cli::Runner::g` → method names on (serviceId, interfaceId)
2848
+ */
2849
+ async function _completeMethodRef(prefix, dir) {
2850
+ const parts = prefix.split("::");
2851
+ const seps = parts.length - 1;
2852
+ const entries = await dir.entries();
2853
+ const sids = distinctServiceIds(entries);
2854
+ const rootInterfaceIds = new Set(entries.filter((e) => e.serviceId === "").map((e) => e.interfaceId));
2855
+ if (seps === 0) {
2856
+ const out = [];
2857
+ for (const sid of sids) if (sid.startsWith(prefix)) out.push({
2858
+ text: sid,
2859
+ tooltip: `service ${sid}`
2860
+ });
2861
+ for (const iid of [...rootInterfaceIds].sort()) if (iid.startsWith(prefix)) out.push({
2862
+ text: iid,
2863
+ tooltip: `interface ${iid}`
2864
+ });
2865
+ return out;
2866
+ }
2867
+ if (seps === 1) {
2868
+ const [first] = parts;
2869
+ const out = [];
2870
+ if (sids.includes(first)) for (const iid of interfacesOnService(entries, first)) {
2871
+ const c = `${first}::${iid}`;
2872
+ if (c.startsWith(prefix)) out.push({
2873
+ text: c,
2874
+ tooltip: `${first} :: ${iid}`
2875
+ });
2876
+ }
2877
+ if (rootInterfaceIds.has(first)) {
2878
+ const formTwoMethods = await _safeMethods(dir, void 0, first);
2879
+ for (const m of formTwoMethods) {
2880
+ const c = `${first}::${m}`;
2881
+ if (c.startsWith(prefix)) out.push({
2882
+ text: c,
2883
+ tooltip: `${first} :: ${m}`
2884
+ });
2885
+ }
2886
+ }
2887
+ return out;
2888
+ }
2889
+ if (seps === 2) {
2890
+ const [sid, iid] = parts;
2891
+ return (await _safeMethods(dir, sid, iid)).map((m) => `${sid}::${iid}::${m}`).filter((c) => c.startsWith(prefix)).map((c) => ({ text: c }));
2892
+ }
2893
+ return [];
2894
+ }
2895
+ async function _safeMethods(dir, serviceId, interfaceId) {
2896
+ try {
2897
+ return await dir.methodsOnInterface(serviceId, interfaceId);
2898
+ } catch {
2899
+ return [];
2900
+ }
2901
+ }
2902
+ async function _safeParams(dir, serviceId, interfaceId, methodName) {
2903
+ try {
2904
+ return await dir.paramNamesForMethod(serviceId, interfaceId, methodName);
2905
+ } catch {
2906
+ return [];
2907
+ }
2908
+ }
2909
+ /**
2910
+ * Parse `[serviceId::][interfaceId::]methodName[@hash]` enough to look up
2911
+ * the method's params. Returns `undefined` for unrecognized shapes so the
2912
+ * orchestrator can skip the dynamic call. Independent of
2913
+ * `MethodRefWithOptHash` to keep the completions module dependency-free.
2914
+ */
2915
+ function _parseMethodRefForCompletion(raw) {
2916
+ if (raw.length === 0) return void 0;
2917
+ const at = raw.lastIndexOf("@");
2918
+ const parts = (at >= 0 && !raw.slice(at + 1).includes("::") ? raw.slice(0, at) : raw).split("::");
2919
+ if (parts.some((p) => p.length === 0)) return void 0;
2920
+ if (parts.length === 1) return {
2921
+ serviceId: void 0,
2922
+ interfaceId: void 0,
2923
+ methodName: parts[0]
2924
+ };
2925
+ if (parts.length === 2) return {
2926
+ serviceId: void 0,
2927
+ interfaceId: parts[0],
2928
+ methodName: parts[1]
2929
+ };
2930
+ if (parts.length === 3) return {
2931
+ serviceId: parts[0],
2932
+ interfaceId: parts[1],
2933
+ methodName: parts[2]
2934
+ };
2935
+ }
2936
+ //#endregion
2937
+ //#region src/completions/cache.ts
2938
+ /**
2939
+ * File-backed JSON cache for the directory snapshot + per-interface method
2940
+ * lists. Keyed by a stable identifier of the target hub (typically the
2941
+ * resolved endpoint URI sha) so TAB completion against different endpoints
2942
+ * doesn't share state.
2943
+ *
2944
+ * Cache lives under `os.tmpdir()/linkrpc-completions/`. I/O failures are
2945
+ * silently absorbed — the wrapper falls through to the inner source rather
2946
+ * than failing the completion request.
2947
+ */
2948
+ /** TTL for the bus snapshot (services + interfaces). */
2949
+ const SNAPSHOT_TTL_MS = 6e4;
2950
+ /** TTL for per-interface method lists (schemas change rarely). */
2951
+ const METHODS_TTL_MS = 3e5;
2952
+ const CACHE_DIR = path$1.join(os.tmpdir(), "linkrpc-completions");
2953
+ /**
2954
+ * Wrap `inner` with a JSON-file cache scoped to `endpointKey`. Reads the
2955
+ * cache file once at construction; refills the bus snapshot lazily on the
2956
+ * first `entries()` call when the cache is missing or stale.
2957
+ */
2958
+ function withFileCache(inner, endpointKey) {
2959
+ const file = _cacheFile(endpointKey);
2960
+ let cached = _loadFresh(file);
2961
+ let entriesPromise;
2962
+ const ensureEntries = () => {
2963
+ if (cached !== void 0) return Promise.resolve(cached.entries);
2964
+ if (!entriesPromise) entriesPromise = inner.entries().then((entries) => {
2965
+ cached = {
2966
+ ts: Date.now(),
2967
+ entries,
2968
+ methods: {}
2969
+ };
2970
+ _save(file, cached);
2971
+ return entries;
2972
+ });
2973
+ return entriesPromise;
2974
+ };
2975
+ return {
2976
+ entries: ensureEntries,
2977
+ async methodsOnInterface(serviceId, interfaceId) {
2978
+ const key = `${serviceId ?? ""}::${interfaceId}`;
2979
+ if (cached && cached.methods[key]) {
2980
+ const entry = cached.methods[key];
2981
+ if (Date.now() - entry.ts <= METHODS_TTL_MS) return entry.names;
2982
+ }
2983
+ const names = await inner.methodsOnInterface(serviceId, interfaceId);
2984
+ if (cached) {
2985
+ cached = {
2986
+ ...cached,
2987
+ methods: {
2988
+ ...cached.methods,
2989
+ [key]: {
2990
+ ts: Date.now(),
2991
+ names: [...names]
2992
+ }
2993
+ }
2994
+ };
2995
+ _save(file, cached);
2996
+ }
2997
+ return names;
2998
+ },
2999
+ async paramNamesForMethod(serviceId, interfaceId, methodName) {
3000
+ const key = `${serviceId ?? ""}::${interfaceId}::${methodName}`;
3001
+ if (cached && cached.params && cached.params[key]) {
3002
+ const entry = cached.params[key];
3003
+ if (Date.now() - entry.ts <= METHODS_TTL_MS) return entry.names;
3004
+ }
3005
+ const names = await inner.paramNamesForMethod(serviceId, interfaceId, methodName);
3006
+ if (cached) {
3007
+ const params = {
3008
+ ...cached.params ?? {},
3009
+ [key]: {
3010
+ ts: Date.now(),
3011
+ names: [...names]
3012
+ }
3013
+ };
3014
+ cached = {
3015
+ ...cached,
3016
+ params
3017
+ };
3018
+ _save(file, cached);
3019
+ }
3020
+ return names;
3021
+ }
3022
+ };
3023
+ }
3024
+ function _loadFresh(file) {
3025
+ try {
3026
+ const raw = fs$1.readFileSync(file, "utf8");
3027
+ const parsed = JSON.parse(raw);
3028
+ if (!parsed || typeof parsed.ts !== "number" || !Array.isArray(parsed.entries)) return;
3029
+ if (Date.now() - parsed.ts > SNAPSHOT_TTL_MS) return void 0;
3030
+ return parsed;
3031
+ } catch {
3032
+ return;
3033
+ }
3034
+ }
3035
+ function _save(file, snap) {
3036
+ try {
3037
+ fs$1.mkdirSync(CACHE_DIR, { recursive: true });
3038
+ fs$1.writeFileSync(file, JSON.stringify(snap));
3039
+ } catch {}
3040
+ }
3041
+ function _cacheFile(endpointKey) {
3042
+ const hash = createHash("sha256").update(endpointKey).digest("hex").slice(0, 16);
3043
+ return path$1.join(CACHE_DIR, `${hash}.json`);
3044
+ }
3045
+ //#endregion
3046
+ //#region src/contexts.ts
3047
+ var ContextStore = class {
3048
+ _file;
3049
+ _cwd;
3050
+ constructor(options = {}) {
3051
+ this._file = options.file ?? defaultContextStoreFile();
3052
+ this._cwd = path$1.resolve(options.cwd ?? process.cwd());
3053
+ }
3054
+ get file() {
3055
+ return this._file;
3056
+ }
3057
+ async select(options = {}) {
3058
+ const contexts = await this._read();
3059
+ if (options.selector !== void 0) {
3060
+ const reference = await this.resolveReference(options.selector);
3061
+ return this._selectedReference(contexts, reference, "argument", true, options.allowMissing === true);
3062
+ }
3063
+ if (options.environmentSelector !== void 0 && options.environmentSelector !== "") {
3064
+ const reference = await this.resolveReference(options.environmentSelector);
3065
+ return this._selectedReference(contexts, reference, "environment", false, options.allowMissing === true);
3066
+ }
3067
+ let cursor = await canonicalDirectory(this._cwd);
3068
+ while (true) {
3069
+ const reference = normalizeContextReference({
3070
+ kind: "path",
3071
+ path: cursor
3072
+ });
3073
+ const context = contexts.get(contextKey(reference));
3074
+ if (context !== void 0) return {
3075
+ reference,
3076
+ context,
3077
+ selectedBy: reference.kind === "root" ? "root" : "cwd",
3078
+ explicitlySelected: false
3079
+ };
3080
+ const parent = path$1.dirname(cursor);
3081
+ if (parent === cursor) break;
3082
+ cursor = parent;
3083
+ }
3084
+ const rootReference = { kind: "root" };
3085
+ const root = contexts.get(contextKey(rootReference));
3086
+ if (root !== void 0) return {
3087
+ reference: rootReference,
3088
+ context: root,
3089
+ selectedBy: "root",
3090
+ explicitlySelected: false
3091
+ };
3092
+ return {
3093
+ reference: { kind: "empty" },
3094
+ context: void 0,
3095
+ selectedBy: "empty",
3096
+ explicitlySelected: false
3097
+ };
3098
+ }
3099
+ async resolveReference(selector) {
3100
+ if (selector === ":empty") return { kind: "empty" };
3101
+ if (selector === ":root") return { kind: "root" };
3102
+ if (selector.startsWith("id:")) {
3103
+ const id = selector.slice(3);
3104
+ if (id.length === 0) throw new Error("context selector \"id:\" requires a non-empty id");
3105
+ return {
3106
+ kind: "id",
3107
+ id
3108
+ };
3109
+ }
3110
+ if (selector.startsWith(":")) throw new Error(`unknown context selector "${selector}" (expected a folder, id:<name>, :root, or :empty)`);
3111
+ return normalizeContextReference({
3112
+ kind: "path",
3113
+ path: await canonicalDirectory(path$1.resolve(this._cwd, selector))
3114
+ });
3115
+ }
3116
+ async set(reference, values, options = {}) {
3117
+ reference = normalizeContextReference(reference);
3118
+ return this._mutate((contexts) => {
3119
+ const key = contextKey(reference);
3120
+ const existing = contexts.get(key);
3121
+ if (options.createOnly === true && existing !== void 0) throw new Error(`context ${formatContextReference(reference)} already exists`);
3122
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3123
+ const context = {
3124
+ key,
3125
+ reference,
3126
+ values: existing === void 0 ? normalizeContextValues(values) : mergeContextValues(existing.values, values),
3127
+ createdAt: existing?.createdAt ?? now,
3128
+ updatedAt: now
3129
+ };
3130
+ contexts.set(key, context);
3131
+ return {
3132
+ value: context,
3133
+ changed: true
3134
+ };
3135
+ });
3136
+ }
3137
+ async assertCanCreate(reference) {
3138
+ reference = normalizeContextReference(reference);
3139
+ if ((await this._read()).has(contextKey(reference))) throw new Error(`context ${formatContextReference(reference)} already exists`);
3140
+ }
3141
+ async replace(reference, values, options = {}) {
3142
+ reference = normalizeContextReference(reference);
3143
+ return this._mutate((contexts) => {
3144
+ const key = contextKey(reference);
3145
+ const existing = contexts.get(key);
3146
+ if (options.createOnly === true && existing !== void 0) throw new Error(`context ${formatContextReference(reference)} already exists`);
3147
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3148
+ const context = {
3149
+ key,
3150
+ reference,
3151
+ values: normalizeContextValues(values),
3152
+ createdAt: existing?.createdAt ?? now,
3153
+ updatedAt: now
3154
+ };
3155
+ contexts.set(key, context);
3156
+ return {
3157
+ value: context,
3158
+ changed: true
3159
+ };
3160
+ });
3161
+ }
3162
+ async unset(reference, keys) {
3163
+ reference = normalizeContextReference(reference);
3164
+ return this._mutate((contexts) => {
3165
+ const key = contextKey(reference);
3166
+ const existing = contexts.get(key);
3167
+ if (existing === void 0) throw new Error(`context ${formatContextReference(reference)} does not exist`);
3168
+ const values = { ...existing.values };
3169
+ for (const item of keys) delete values[item];
3170
+ const context = {
3171
+ ...existing,
3172
+ values: normalizeContextValues(values),
3173
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3174
+ };
3175
+ contexts.set(key, context);
3176
+ return {
3177
+ value: context,
3178
+ changed: true
3179
+ };
3180
+ });
3181
+ }
3182
+ async remove(reference) {
3183
+ if (reference.kind === "empty") throw new Error(":empty is immutable and cannot be removed");
3184
+ reference = normalizeContextReference(reference);
3185
+ return this._mutate((contexts) => {
3186
+ const removed = contexts.delete(contextKey(reference));
3187
+ return {
3188
+ value: removed,
3189
+ changed: removed
3190
+ };
3191
+ });
3192
+ }
3193
+ async list() {
3194
+ return [...(await this._read()).values()].sort((a, b) => formatContextReference(a.reference).localeCompare(formatContextReference(b.reference)));
3195
+ }
3196
+ _selectedReference(contexts, reference, selectedBy, explicitlySelected, allowMissing) {
3197
+ if (reference.kind === "empty") return {
3198
+ reference,
3199
+ context: void 0,
3200
+ selectedBy,
3201
+ explicitlySelected
3202
+ };
3203
+ const context = contexts.get(contextKey(reference));
3204
+ if (context === void 0 && !allowMissing) throw new Error(`context ${formatContextReference(reference)} does not exist`);
3205
+ return {
3206
+ reference,
3207
+ context,
3208
+ selectedBy,
3209
+ explicitlySelected
3210
+ };
3211
+ }
3212
+ async _read() {
3213
+ let text;
3214
+ try {
3215
+ text = await readFile(this._file, "utf8");
3216
+ } catch (error) {
3217
+ if (error.code === "ENOENT") return /* @__PURE__ */ new Map();
3218
+ throw error;
3219
+ }
3220
+ const value = JSON.parse(text);
3221
+ if (value.version !== 1 || !Array.isArray(value.contexts)) throw new Error(`invalid context store ${this._file}`);
3222
+ const contexts = /* @__PURE__ */ new Map();
3223
+ for (const raw of value.contexts) {
3224
+ const context = parseStoredContext(raw);
3225
+ if (contexts.has(context.key)) throw new Error(`duplicate context key "${context.key}" in ${this._file}`);
3226
+ contexts.set(context.key, context);
3227
+ }
3228
+ return contexts;
3229
+ }
3230
+ async _write(contexts) {
3231
+ await mkdir(path$1.dirname(this._file), { recursive: true });
3232
+ const document = {
3233
+ version: 1,
3234
+ contexts: [...contexts.values()]
3235
+ };
3236
+ const temporary = `${this._file}.${process.pid}.${randomUUID()}.tmp`;
3237
+ await writeFile(temporary, JSON.stringify(document, void 0, 2) + "\n", { mode: 384 });
3238
+ await rename(temporary, this._file);
3239
+ if (process.platform !== "win32") await chmod(this._file, 384);
3240
+ }
3241
+ async _mutate(update) {
3242
+ const release = await acquireContextStoreLock(this._file);
3243
+ try {
3244
+ const contexts = await this._read();
3245
+ const result = update(contexts);
3246
+ if (result.changed) await this._write(contexts);
3247
+ return result.value;
3248
+ } finally {
3249
+ await release();
3250
+ }
3251
+ }
3252
+ };
3253
+ function mergeContextValues(base, overrides) {
3254
+ const result = { ...base };
3255
+ const endpointSelectors = [
3256
+ ["endpoint", overrides.endpoint],
3257
+ ["endpointCmd", overrides.endpointCmd],
3258
+ ["endpointCmdStdio", overrides.endpointCmdStdio]
3259
+ ].filter((entry) => entry[1] !== void 0);
3260
+ if (endpointSelectors.length === 1) {
3261
+ delete result.endpoint;
3262
+ delete result.endpointCmd;
3263
+ delete result.endpointCmdStdio;
3264
+ const selector = endpointSelectors[0][0];
3265
+ if (selector === "endpoint") {
3266
+ delete result.endpointCmdEnv;
3267
+ delete result.endpointCmdCwd;
3268
+ delete result.provisionIdentity;
3269
+ delete result.provisionIdentitySlot;
3270
+ const endpoint = overrides.endpoint;
3271
+ if (overrides.endpointToken === void 0 && endpoint !== void 0 && !/(?:[?&])token=%(?:[&#]|$)/.test(endpoint)) delete result.endpointToken;
3272
+ } else {
3273
+ delete result.endpointToken;
3274
+ if (selector === "endpointCmdStdio") {
3275
+ delete result.provisionIdentity;
3276
+ delete result.provisionIdentitySlot;
3277
+ }
3278
+ }
3279
+ }
3280
+ for (const [key, value] of Object.entries(overrides)) if (value !== void 0) result[key] = value;
3281
+ return normalizeContextValues(result);
3282
+ }
3283
+ async function acquireContextStoreLock(file) {
3284
+ const lockFile = `${file}.lock`;
3285
+ const deadline = Date.now() + 5e3;
3286
+ await mkdir(path$1.dirname(file), { recursive: true });
3287
+ while (true) {
3288
+ try {
3289
+ const handle = await open(lockFile, "wx", 384);
3290
+ try {
3291
+ await handle.writeFile(`${process.pid}\n${(/* @__PURE__ */ new Date()).toISOString()}\n`);
3292
+ } catch (error) {
3293
+ await handle.close();
3294
+ await unlink(lockFile);
3295
+ throw error;
3296
+ }
3297
+ return async () => {
3298
+ await handle.close();
3299
+ try {
3300
+ await unlink(lockFile);
3301
+ } catch (error) {
3302
+ if (error.code !== "ENOENT") throw error;
3303
+ }
3304
+ };
3305
+ } catch (error) {
3306
+ if (error.code !== "EEXIST") throw error;
3307
+ }
3308
+ try {
3309
+ const lockInfo = await stat(lockFile);
3310
+ if (Date.now() - lockInfo.mtimeMs > 3e4) {
3311
+ await unlink(lockFile);
3312
+ continue;
3313
+ }
3314
+ } catch (error) {
3315
+ if (error.code === "ENOENT") continue;
3316
+ throw error;
3317
+ }
3318
+ if (Date.now() >= deadline) throw new Error(`timed out waiting for context store lock ${lockFile}`);
3319
+ await new Promise((resolve) => setTimeout(resolve, 25));
3320
+ }
3321
+ }
3322
+ function contextKey(reference) {
3323
+ reference = normalizeContextReference(reference);
3324
+ switch (reference.kind) {
3325
+ case "empty": return ":empty";
3326
+ case "root": return ":root";
3327
+ case "id": return `id:${reference.id}`;
3328
+ case "path": {
3329
+ const normalized = path$1.normalize(reference.path);
3330
+ return `path:${process.platform === "win32" ? normalized.toLowerCase() : normalized}`;
3331
+ }
3332
+ }
3333
+ }
3334
+ function formatContextReference(reference) {
3335
+ reference = normalizeContextReference(reference);
3336
+ switch (reference.kind) {
3337
+ case "empty": return ":empty";
3338
+ case "root": return ":root";
3339
+ case "id": return `id:${reference.id}`;
3340
+ case "path": return reference.path;
3341
+ }
3342
+ }
3343
+ function defaultContextStoreFile() {
3344
+ const home = os.homedir();
3345
+ if (process.platform === "win32") return path$1.join(process.env.APPDATA ?? path$1.join(home, "AppData", "Roaming"), "linkrpc", "contexts.json");
3346
+ if (process.platform === "darwin") return path$1.join(home, "Library", "Application Support", "linkrpc", "contexts.json");
3347
+ return path$1.join(process.env.XDG_CONFIG_HOME ?? path$1.join(home, ".config"), "linkrpc", "contexts.json");
3348
+ }
3349
+ async function canonicalDirectory(folder) {
3350
+ let info;
3351
+ try {
3352
+ info = await stat(folder);
3353
+ } catch (error) {
3354
+ if (error.code === "ENOENT") throw new Error(`context folder does not exist: ${folder}`);
3355
+ throw error;
3356
+ }
3357
+ if (!info.isDirectory()) throw new Error(`context path is not a directory: ${folder}`);
3358
+ return path$1.normalize(await realpath(folder));
3359
+ }
3360
+ function normalizeContextReference(reference) {
3361
+ if (process.platform !== "win32" && reference.kind === "path" && path$1.parse(path$1.normalize(reference.path)).root === path$1.normalize(reference.path)) return { kind: "root" };
3362
+ return reference;
3363
+ }
3364
+ function normalizeContextValues(values) {
3365
+ return JSON.parse(JSON.stringify(values));
3366
+ }
3367
+ function parseStoredContext(value) {
3368
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("invalid context entry");
3369
+ const raw = value;
3370
+ if (typeof raw.key !== "string" || typeof raw.createdAt !== "string" || typeof raw.updatedAt !== "string" || typeof raw.reference !== "object" || raw.reference === null || typeof raw.values !== "object" || raw.values === null) throw new Error("invalid context entry");
3371
+ const reference = raw.reference;
3372
+ if (reference.kind === "empty" || ![
3373
+ "root",
3374
+ "id",
3375
+ "path"
3376
+ ].includes(reference.kind) || reference.kind === "id" && typeof reference.id !== "string" || reference.kind === "path" && typeof reference.path !== "string") throw new Error("invalid context reference");
3377
+ if (raw.key !== contextKey(reference)) throw new Error(`context key mismatch for ${formatContextReference(reference)}`);
3378
+ return {
3379
+ key: raw.key,
3380
+ reference,
3381
+ values: normalizeContextValues(raw.values),
3382
+ createdAt: raw.createdAt,
3383
+ updatedAt: raw.updatedAt
3384
+ };
3385
+ }
3386
+ //#endregion
3387
+ //#region src/invocationContext.ts
3388
+ async function resolveInvocationContext(options) {
3389
+ const env = options.env ?? process.env;
3390
+ const environmentSelector = firstDefined(env.LINKRPC_CONTEXT, env.HUBRPC_CONTEXT);
3391
+ const selected = await options.store.select({
3392
+ ...options.selector !== void 0 ? { selector: options.selector } : {},
3393
+ ...options.selector === void 0 && environmentSelector !== void 0 ? { environmentSelector } : {},
3394
+ ...options.allowMissingContext === true ? { allowMissing: true } : {}
3395
+ });
3396
+ const environmentApplied = options.useEnvironment ?? (options.profile === "hub" && options.selector === void 0);
3397
+ const contextValues = selected.context?.values ?? {};
3398
+ const environmentValues = environmentApplied ? contextValuesFromEnvironment(env) : {};
3399
+ const cliOverrides = options.cliOverrides ?? {};
3400
+ return {
3401
+ profile: options.profile,
3402
+ selected,
3403
+ contextValues,
3404
+ environmentValues,
3405
+ values: mergeContextValues(mergeContextValues(contextValues, environmentValues), cliOverrides),
3406
+ cliOverrides,
3407
+ environmentApplied
3408
+ };
3409
+ }
3410
+ function contextValuesFromEnvironment(env) {
3411
+ const endpoint = firstDefined(env.LINKRPC_ENDPOINT, env.HUBRPC_ENDPOINT);
3412
+ const endpointToken = firstDefined(env.LINKRPC_TOKEN, env.HUBRPC_TOKEN);
3413
+ return {
3414
+ ...endpoint !== void 0 ? { endpoint } : {},
3415
+ ...endpointToken !== void 0 ? { endpointToken } : {}
3416
+ };
3417
+ }
3418
+ async function mutationReference(store, selected) {
3419
+ if (selected.reference.kind !== "empty" && selected.reference.kind !== "root") return selected.reference;
3420
+ const cwd = await store.resolveReference(".");
3421
+ if (cwd.kind === "empty") throw new Error("current directory cannot be used as a context");
3422
+ return cwd;
3423
+ }
3424
+ function validationDefault(profile) {
3425
+ return profile === "hub" ? "required" : "auto";
3426
+ }
3427
+ function resolveInvocationEndpoint(invocation, provisioningHandledElsewhere = false) {
3428
+ const cli = invocation.cliOverrides;
3429
+ const context = invocation.contextValues;
3430
+ const environment = invocation.environmentValues;
3431
+ const cliHasEndpoint = hasEndpointSelector(cli);
3432
+ const environmentHasEndpoint = environment.endpoint !== void 0;
3433
+ let input;
3434
+ if (cliHasEndpoint) {
3435
+ const inheritedToken = cli.endpoint !== void 0 && hasTokenPlaceholder(cli.endpoint) ? environment.endpointToken ?? context.endpointToken : void 0;
3436
+ const token = cli.endpointToken ?? inheritedToken;
3437
+ input = {
3438
+ ...endpointInput(cli),
3439
+ ...token !== void 0 ? { endpointToken: token } : {},
3440
+ provisioningHandledElsewhere,
3441
+ env: {}
3442
+ };
3443
+ } else if (environmentHasEndpoint) input = {
3444
+ ...endpointInput(cli),
3445
+ ...cli.endpointToken !== void 0 ? { endpointToken: cli.endpointToken } : {},
3446
+ provisioningHandledElsewhere,
3447
+ env: {
3448
+ LINKRPC_ENDPOINT: environment.endpoint,
3449
+ ...environment.endpointToken !== void 0 ? { LINKRPC_TOKEN: environment.endpointToken } : {}
3450
+ }
3451
+ };
3452
+ else {
3453
+ const values = {
3454
+ ...context,
3455
+ ...cli
3456
+ };
3457
+ const token = cli.endpointToken ?? environment.endpointToken ?? context.endpointToken;
3458
+ input = {
3459
+ ...endpointInput(values),
3460
+ ...token !== void 0 ? { endpointToken: token } : {},
3461
+ provisioningHandledElsewhere,
3462
+ env: {}
3463
+ };
3464
+ }
3465
+ const result = resolveEndpoint(input);
3466
+ if (result.error !== void 0) throw new Error(result.error);
3467
+ return result.endpoint;
3468
+ }
3469
+ function endpointInput(values) {
3470
+ return {
3471
+ ...values.endpoint !== void 0 ? { endpoint: values.endpoint } : {},
3472
+ ...values.endpointCmd !== void 0 ? { endpointCmd: values.endpointCmd } : {},
3473
+ ...values.endpointCmdStdio !== void 0 ? { endpointCmdStdio: values.endpointCmdStdio } : {},
3474
+ ...values.endpointCmdEnv !== void 0 ? { endpointCmdEnv: values.endpointCmdEnv } : {},
3475
+ ...values.endpointCmdCwd !== void 0 ? { endpointCmdCwd: values.endpointCmdCwd } : {},
3476
+ ...values.provisionIdentity !== void 0 ? { provisionIdentity: values.provisionIdentity } : {},
3477
+ ...values.provisionIdentitySlot !== void 0 ? { provisionIdentitySlot: values.provisionIdentitySlot } : {}
3478
+ };
3479
+ }
3480
+ function hasEndpointSelector(values) {
3481
+ return values.endpoint !== void 0 || values.endpointCmd !== void 0 || values.endpointCmdStdio !== void 0;
3482
+ }
3483
+ function hasTokenPlaceholder(endpoint) {
3484
+ return /(?:[?&])token=%(?:[&#]|$)/.test(endpoint);
3485
+ }
3486
+ function firstDefined(...values) {
3487
+ return values.find((value) => value !== void 0);
3488
+ }
3489
+ //#endregion
3490
+ //#region src/staticHubSchema.ts
3491
+ function resolveStaticHubSchemaSource(source) {
3492
+ return isHttpUrl(source) ? source : resolve(source);
3493
+ }
3494
+ async function loadStaticHubSchema(source) {
3495
+ let raw;
3496
+ try {
3497
+ let text;
3498
+ if (isHttpUrl(source)) {
3499
+ const response = await fetch(source);
3500
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`.trimEnd());
3501
+ text = await response.text();
3502
+ } else text = await readFile(source, "utf8");
3503
+ raw = JSON.parse(text);
3504
+ } catch (error) {
3505
+ throw new Error(`Failed to load hub schema '${source}': ${error.message}`);
3506
+ }
3507
+ try {
3508
+ return parseStaticHubSchema(raw);
3509
+ } catch (error) {
3510
+ throw new Error(`Invalid hub schema '${source}': ${error.message}`);
3511
+ }
3512
+ }
3513
+ function isHttpUrl(source) {
3514
+ return /^https?:\/\//i.test(source);
3515
+ }
3516
+ function parseStaticHubSchema(value) {
3517
+ const root = expectRecord(value, "hub schema");
3518
+ const rawServices = expectArray(root.services, "services");
3519
+ const interfaceSchemas = expectArray(root.interfaceSchemas, "interfaceSchemas").map((schema, index) => parseInterfaceSchema(schema, `interfaceSchemas[${index}]`));
3520
+ const schemasByKey = /* @__PURE__ */ new Map();
3521
+ for (const schema of interfaceSchemas) {
3522
+ const key = interfaceKey$1(schema.id, schema.hash);
3523
+ if (schemasByKey.has(key)) throw new Error(`interfaceSchemas contains duplicate ${schema.id}@${schema.hash}`);
3524
+ schemasByKey.set(key, schema);
3525
+ }
3526
+ const services = rawServices.map((service, index) => parseService(service, `services[${index}]`));
3527
+ for (const [serviceIndex, service] of services.entries()) for (const [interfaceIndex, ref] of service.interfaces.entries()) requireResolvedReference(ref, schemasByKey, `services[${serviceIndex}].interfaces[${interfaceIndex}]`);
3528
+ const defaultInterface = root.defaultInterface === void 0 ? void 0 : parseInterfaceReference(root.defaultInterface, "defaultInterface");
3529
+ if (defaultInterface !== void 0) requireResolvedReference(defaultInterface, schemasByKey, "defaultInterface");
3530
+ return {
3531
+ services,
3532
+ ...defaultInterface === void 0 ? {} : { defaultInterface },
3533
+ interfaceSchemas
3534
+ };
3535
+ }
3536
+ function parseService(value, path) {
3537
+ const service = expectRecord(value, path);
3538
+ if (typeof service.serviceId !== "string") throw new Error(`${path}.serviceId must be a string`);
3539
+ return {
3540
+ serviceId: service.serviceId,
3541
+ interfaces: expectArray(service.interfaces, `${path}.interfaces`).map((ref, index) => parseInterfaceReference(ref, `${path}.interfaces[${index}]`))
3542
+ };
3543
+ }
3544
+ function parseInterfaceReference(value, path) {
3545
+ const ref = expectRecord(value, path);
3546
+ if (typeof ref.interfaceId !== "string" || ref.interfaceId.length === 0) throw new Error(`${path}.interfaceId must be a non-empty string`);
3547
+ if (typeof ref.interfaceHash !== "string" || ref.interfaceHash.length === 0) throw new Error(`${path}.interfaceHash must be a non-empty string`);
3548
+ return {
3549
+ interfaceId: ref.interfaceId,
3550
+ interfaceHash: ref.interfaceHash
3551
+ };
3552
+ }
3553
+ function parseInterfaceSchema(value, path) {
3554
+ const schema = expectRecord(value, path);
3555
+ if (typeof schema.id !== "string" || schema.id.length === 0) throw new Error(`${path}.id must be a non-empty string`);
3556
+ if (typeof schema.hash !== "string" || schema.hash.length === 0) throw new Error(`${path}.hash must be a non-empty string`);
3557
+ if (typeof schema.methods !== "object" || schema.methods === null || Array.isArray(schema.methods)) throw new Error(`${path}.methods must be an object`);
3558
+ for (const [name, method] of Object.entries(schema.methods)) {
3559
+ if (name.length === 0) throw new Error(`${path}.methods keys must be non-empty strings`);
3560
+ const parsed = expectRecord(method, `${path}.methods.${name}`);
3561
+ if (!Object.hasOwn(parsed, "params")) throw new Error(`${path}.methods.${name}.params is required`);
3562
+ }
3563
+ const typed = schema;
3564
+ const computedHash = computeInterfaceHash(typed);
3565
+ if (computedHash !== typed.hash) throw new Error(`${path} hash mismatch for ${typed.id}: declared ${typed.hash}, computed ${computedHash}`);
3566
+ return typed;
3567
+ }
3568
+ function requireResolvedReference(ref, schemasByKey, path) {
3569
+ if (!schemasByKey.has(interfaceKey$1(ref.interfaceId, ref.interfaceHash))) throw new Error(`${path} ${formatReference(ref)} does not resolve to an interface schema`);
3570
+ }
3571
+ function formatReference(ref) {
3572
+ return `${ref.interfaceId}@${ref.interfaceHash}`;
3573
+ }
3574
+ function interfaceKey$1(interfaceId, interfaceHash) {
3575
+ return `${interfaceId}\0${interfaceHash}`;
3576
+ }
3577
+ function expectRecord(value, path) {
3578
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
3579
+ return value;
3580
+ }
3581
+ function expectArray(value, path) {
3582
+ if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
3583
+ return value;
3584
+ }
3585
+ //#endregion
3586
+ //#region src/commands/staticHubReflection.ts
3587
+ const DIRECTORY_INTERFACE_ID = "hubrpc.directory";
3588
+ const SCHEMAS_INTERFACE_ID = "hubrpc.schemas";
3589
+ const DEFAULTS_INTERFACE_ID = "hubrpc.defaults";
3590
+ const DIRECTORY_LIST_METHOD = `${DIRECTORY_INTERFACE_ID}::list`;
3591
+ const DIRECTORY_WATCH_METHOD = `${DIRECTORY_INTERFACE_ID}::watch`;
3592
+ const SCHEMAS_GET_METHOD = `${SCHEMAS_INTERFACE_ID}::get`;
3593
+ const DEFAULTS_GET_METHOD = `${DEFAULTS_INTERFACE_ID}::get`;
3594
+ var StaticHubReflection = class {
3595
+ _schema;
3596
+ _directory;
3597
+ _schemasByKey = /* @__PURE__ */ new Map();
3598
+ _activeHashesById = /* @__PURE__ */ new Map();
3599
+ _hashesByServiceInterface = /* @__PURE__ */ new Map();
3600
+ constructor(_schema) {
3601
+ this._schema = _schema;
3602
+ this._directory = _schema.services.flatMap((service) => service.interfaces.map((ref) => ({
3603
+ serviceId: service.serviceId,
3604
+ interfaceId: ref.interfaceId,
3605
+ interfaceHash: ref.interfaceHash
3606
+ })));
3607
+ for (const schema of _schema.interfaceSchemas) this._schemasByKey.set(interfaceKey(schema.id, schema.hash), schema);
3608
+ for (const item of this._directory) {
3609
+ const hashes = this._activeHashesById.get(item.interfaceId) ?? /* @__PURE__ */ new Set();
3610
+ hashes.add(item.interfaceHash);
3611
+ this._activeHashesById.set(item.interfaceId, hashes);
3612
+ const serviceKey = interfaceKey(item.serviceId, item.interfaceId);
3613
+ const serviceHashes = this._hashesByServiceInterface.get(serviceKey) ?? /* @__PURE__ */ new Set();
3614
+ serviceHashes.add(item.interfaceHash);
3615
+ this._hashesByServiceInterface.set(serviceKey, serviceHashes);
3616
+ }
3617
+ if (_schema.defaultInterface !== void 0) {
3618
+ const ref = _schema.defaultInterface;
3619
+ const hashes = this._activeHashesById.get(ref.interfaceId) ?? /* @__PURE__ */ new Set();
3620
+ hashes.add(ref.interfaceHash);
3621
+ this._activeHashesById.set(ref.interfaceId, hashes);
3622
+ }
3623
+ }
3624
+ tryHandleRequest(call) {
3625
+ return this.tryHandle(call.method, call.params, call.signal);
3626
+ }
3627
+ tryHandle(method, params, signal) {
3628
+ const reflectionCall = parseReflectionMethod(method);
3629
+ if (reflectionCall === void 0) return;
3630
+ switch (reflectionCall.method) {
3631
+ case DIRECTORY_LIST_METHOD: return Promise.resolve(this._listDirectory(params));
3632
+ case DIRECTORY_WATCH_METHOD: return this._watchDirectory(params, signal);
3633
+ case SCHEMAS_GET_METHOD: return Promise.resolve(this._getSchema(params, reflectionCall.serviceId));
3634
+ case DEFAULTS_GET_METHOD: return Promise.resolve({ result: this._schema.defaultInterface === void 0 ? {} : {
3635
+ interfaceId: this._schema.defaultInterface.interfaceId,
3636
+ interfaceHash: this._schema.defaultInterface.interfaceHash
3637
+ } });
3638
+ default: return Promise.resolve(methodNotFound(method));
3639
+ }
3640
+ }
3641
+ _listDirectory(params) {
3642
+ const parsed = parseDirectoryParams(params);
3643
+ if ("error" in parsed) return parsed;
3644
+ const filtered = this._directory.filter((item) => parsed.interfaceId === void 0 || item.interfaceId === parsed.interfaceId).filter((item) => parsed.serviceId === void 0 || item.serviceId === parsed.serviceId);
3645
+ const start = parsed.cursor ?? 0;
3646
+ if (start > filtered.length) return invalidParams("directory cursor is outside the result set");
3647
+ const end = parsed.limit === void 0 ? filtered.length : Math.min(filtered.length, start + parsed.limit);
3648
+ return { result: {
3649
+ items: filtered.slice(start, end),
3650
+ ...end < filtered.length ? { nextCursor: String(end) } : {}
3651
+ } };
3652
+ }
3653
+ _watchDirectory(params, signal) {
3654
+ const parsed = parseDirectoryParams(params, false);
3655
+ if ("error" in parsed) return Promise.resolve(parsed);
3656
+ return new Promise((resolve) => {
3657
+ const done = () => resolve({ result: {} });
3658
+ if (signal.aborted) {
3659
+ done();
3660
+ return;
3661
+ }
3662
+ signal.addEventListener("abort", done, { once: true });
3663
+ });
3664
+ }
3665
+ _getSchema(params, serviceId) {
3666
+ if (!isRecord(params)) return invalidParams("schemas.get params must be an object");
3667
+ const interfaceId = params.interfaceId;
3668
+ const hash = params.hash;
3669
+ if (typeof interfaceId !== "string" || interfaceId.length === 0) return invalidParams("schemas.get interfaceId must be a non-empty string");
3670
+ if (hash !== void 0 && typeof hash !== "string") return invalidParams("schemas.get hash must be a string");
3671
+ let resolvedHash = hash;
3672
+ if (resolvedHash === void 0) {
3673
+ const active = serviceId === void 0 ? [...this._activeHashesById.get(interfaceId) ?? []] : [...this._hashesByServiceInterface.get(interfaceKey(serviceId, interfaceId)) ?? []];
3674
+ if (active.length !== 1) return { error: {
3675
+ code: ErrorCode.methodNotFound,
3676
+ message: `Interface not found: ${interfaceId}`,
3677
+ data: {
3678
+ reason: "unknown-interface",
3679
+ interfaceId
3680
+ }
3681
+ } };
3682
+ resolvedHash = active[0];
3683
+ }
3684
+ const schema = this._schemasByKey.get(interfaceKey(interfaceId, resolvedHash));
3685
+ if (schema === void 0) return { error: {
3686
+ code: ErrorCode.methodNotFound,
3687
+ message: `Interface not found: ${interfaceId}@${resolvedHash}`,
3688
+ data: {
3689
+ reason: "unknown-interface",
3690
+ interfaceId,
3691
+ hash: resolvedHash
3692
+ }
3693
+ } };
3694
+ return { result: { schema } };
3695
+ }
3696
+ };
3697
+ function withStaticHubReflection(sender, schema) {
3698
+ const reflection = new StaticHubReflection(schema);
3699
+ const handle = (method, params, signal) => {
3700
+ return reflection.tryHandle(method, params, signal)?.then(unwrapResult);
3701
+ };
3702
+ return {
3703
+ sendRequest: (method, params, opts) => handle(method, params, new AbortController().signal) ?? sender.sendRequest(method, params, opts),
3704
+ sendNotification: (method, params, opts) => sender.sendNotification(method, params, opts),
3705
+ sendRequestWithStream: (method, params, opts) => {
3706
+ const controller = new AbortController();
3707
+ const result = handle(method, params, controller.signal);
3708
+ if (result === void 0) return sender.sendRequestWithStream(method, params, opts);
3709
+ return {
3710
+ result,
3711
+ send: () => {},
3712
+ cancel: () => controller.abort(),
3713
+ dispose: () => controller.abort(),
3714
+ ping: () => Promise.resolve()
3715
+ };
3716
+ },
3717
+ close: () => sender.close()
3718
+ };
3719
+ }
3720
+ function unwrapResult(result) {
3721
+ if ("error" in result) throw new RpcError(result.error.message, result.error.code, result.error.data);
3722
+ return result.result;
3723
+ }
3724
+ function parseDirectoryParams(params, allowPagination = true) {
3725
+ if (params === void 0) return {};
3726
+ if (!isRecord(params)) return invalidParams("directory params must be an object");
3727
+ const interfaceId = optionalString(params.interfaceId);
3728
+ if (interfaceId === false) return invalidParams("directory interfaceId must be a string");
3729
+ const serviceId = optionalString(params.serviceId);
3730
+ if (serviceId === false) return invalidParams("directory serviceId must be a string");
3731
+ const result = {
3732
+ ...interfaceId === void 0 ? {} : { interfaceId },
3733
+ ...serviceId === void 0 ? {} : { serviceId }
3734
+ };
3735
+ if (!allowPagination) return result;
3736
+ if (params.cursor !== void 0) {
3737
+ if (typeof params.cursor !== "string" || !/^(0|[1-9]\d*)$/.test(params.cursor)) return invalidParams("directory cursor must be a non-negative integer string");
3738
+ result.cursor = Number(params.cursor);
3739
+ }
3740
+ if (params.limit !== void 0) {
3741
+ if (typeof params.limit !== "number" || !Number.isSafeInteger(params.limit) || params.limit <= 0) return invalidParams("directory limit must be a positive integer");
3742
+ result.limit = params.limit;
3743
+ }
3744
+ return result;
3745
+ }
3746
+ function parseReflectionMethod(method) {
3747
+ const parts = method.split("::");
3748
+ const candidate = parts.length === 2 ? method : parts.length === 3 ? `${parts[1]}::${parts[2]}` : void 0;
3749
+ if (candidate?.startsWith(`${DIRECTORY_INTERFACE_ID}::`) || candidate?.startsWith(`${SCHEMAS_INTERFACE_ID}::`) || candidate?.startsWith(`${DEFAULTS_INTERFACE_ID}::`)) return {
3750
+ method: candidate,
3751
+ serviceId: parts.length === 3 ? parts[0] : void 0
3752
+ };
3753
+ }
3754
+ function methodNotFound(method) {
3755
+ return { error: {
3756
+ code: ErrorCode.methodNotFound,
3757
+ message: `Unknown static reflection method: ${method}`
3758
+ } };
3759
+ }
3760
+ function invalidParams(message) {
3761
+ return { error: {
3762
+ code: ErrorCode.invalidParams,
3763
+ message
3764
+ } };
3765
+ }
3766
+ function optionalString(value) {
3767
+ if (value === void 0) return void 0;
3768
+ return typeof value === "string" ? value : false;
3769
+ }
3770
+ function interfaceKey(interfaceId, interfaceHash) {
3771
+ return `${interfaceId}\0${interfaceHash}`;
3772
+ }
3773
+ function isRecord(value) {
3774
+ return value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value);
3775
+ }
3776
+ //#endregion
3777
+ //#region src/completions/runComplete.ts
3778
+ /**
3779
+ * Shared completion core: resolve a command line + cursor into a structured
3780
+ * candidate list, opening a best-effort hub connection when the slot needs
3781
+ * dynamic data.
3782
+ *
3783
+ * This is the reusable engine behind two front-ends:
3784
+ * - the CLI's `_complete` command (see `../commands/internalComplete.ts`),
3785
+ * which formats the result as `text\ttooltip` lines for shell scripts;
3786
+ * - the VS Code extension's terminal completion provider, which maps the
3787
+ * structured result onto `TerminalCompletionItem`s.
3788
+ *
3789
+ * Endpoint selection is caller-controllable: `endpointOverride` wins over the
3790
+ * line's `--endpoint`, and `env` controls (or disables, via `{}`) the
3791
+ * `LINKRPC_ENDPOINT` fallback — the extension passes `{}` so completion targets
3792
+ * the endpoint named on the line rather than the extension host's environment.
3793
+ */
3794
+ /** Slot types that require talking to a hub. Anything else is static-only. */
3795
+ const DYNAMIC_SLOT_TYPES = /* @__PURE__ */ new Set([
3796
+ "serviceId",
3797
+ "interfaceId",
3798
+ "interfaceRef",
3799
+ "methodRef"
3800
+ ]);
3801
+ /**
3802
+ * Resolve completion candidates for the cursor at `point` in `line`, opening a
3803
+ * hub connection only when the slot needs dynamic data.
3804
+ */
3805
+ async function completeForLine(opts) {
3806
+ const parsed = parseLine(opts.line, opts.point);
3807
+ const executable = normalizeCliExecutable(parsed.tokensBefore[0]?.text);
3808
+ const tree = executable === "hub" ? HUB_COMMAND_TREE : RPC_COMMAND_TREE;
3809
+ const ctx = resolveSlot(parsed, tree);
3810
+ const profile = executable === "hub" || ctx.commandPath[0]?.name === "hub" ? "hub" : "rpc";
3811
+ const prefix = parsed.currentWordPrefix;
3812
+ let directory = opts.directoryOverride;
3813
+ let closeConnection;
3814
+ if (!directory && !opts.skipConnect && _slotWantsDynamic(ctx.slot)) {
3815
+ const opened = await _openDirectoryFromLine(ctx.seenFlagValues, {
3816
+ endpointOverride: opts.endpointOverride,
3817
+ env: opts.env,
3818
+ profile
3819
+ });
3820
+ directory = opened?.source;
3821
+ closeConnection = opened?.close;
3822
+ }
3823
+ try {
3824
+ return {
3825
+ candidates: await complete({
3826
+ line: opts.line,
3827
+ point: opts.point,
3828
+ tree,
3829
+ ...directory !== void 0 ? { directory } : {}
3830
+ }),
3831
+ slot: ctx.slot,
3832
+ replacementIndex: opts.point - prefix.length,
3833
+ replacementLength: prefix.length
3834
+ };
3835
+ } finally {
3836
+ closeConnection?.();
3837
+ }
3838
+ }
3839
+ function _slotWantsDynamic(slot) {
3840
+ if (slot.kind === "flag-value") return slot.flag.valueType !== void 0 && DYNAMIC_SLOT_TYPES.has(slot.flag.valueType);
3841
+ if (slot.kind === "positional") return DYNAMIC_SLOT_TYPES.has(slot.type);
3842
+ return false;
3843
+ }
3844
+ /**
3845
+ * Try to open a `DirectorySource` against the hub the partial command line
3846
+ * points at (or `endpointOverride`). The returned `close()` MUST be called:
3847
+ * the underlying socket otherwise keeps the process alive past action return,
3848
+ * hanging the user's prompt.
3849
+ *
3850
+ * Any failure → `undefined` (the orchestrator falls back to static-only).
3851
+ */
3852
+ async function _openDirectoryFromLine(seenFlagValues, opts) {
3853
+ try {
3854
+ return await _openDirectoryFromLineCore(seenFlagValues, opts);
3855
+ } catch {
3856
+ return;
3857
+ }
3858
+ }
3859
+ async function _openDirectoryFromLineCore(seenFlagValues, opts) {
3860
+ const invocation = await resolveInvocationContext({
3861
+ profile: opts.profile,
3862
+ store: new ContextStore(),
3863
+ selector: seenFlagValues.get("--context"),
3864
+ cliOverrides: completionContextOverrides(seenFlagValues, opts.endpointOverride),
3865
+ env: opts.env,
3866
+ useEnvironment: seenFlagValues.has("--use-env") ? true : seenFlagValues.has("--no-use-env") ? false : void 0
3867
+ });
3868
+ let endpoint;
3869
+ try {
3870
+ endpoint = resolveInvocationEndpoint(invocation);
3871
+ } catch {
3872
+ return;
3873
+ }
3874
+ if (endpoint === void 0) return void 0;
3875
+ if (endpoint.kind !== "socket" && endpoint.kind !== "ws" && endpoint.kind !== "ws-no-init") return void 0;
3876
+ const opened = await _connectWithDeadline(endpoint, 800);
3877
+ if (!opened) return void 0;
3878
+ const isRaw = endpoint.kind === "ws-no-init" || endpoint.kind === "socket" && endpoint.brokerMode === "raw";
3879
+ if (opts.profile === "hub" && !isRaw) try {
3880
+ const principalSpec = parsePrincipalSpec(invocation.values.principal);
3881
+ await _withDeadline(setupSigning(opened.channel, opened.signing, principalSpec, { negotiateHubCaps: isHubEndpoint(endpoint) }), 800);
3882
+ } catch {
3883
+ opened.close();
3884
+ return;
3885
+ }
3886
+ const staticSchema = invocation.values.schema === void 0 ? void 0 : await loadStaticHubSchema(invocation.values.schema);
3887
+ return {
3888
+ source: withFileCache(new ChannelDirectorySource(staticSchema === void 0 ? opened.channel : withStaticHubReflection(opened.channel, staticSchema)), _endpointCacheKey(endpoint, staticSchema)),
3889
+ close: () => opened.close()
3890
+ };
3891
+ }
3892
+ function completionContextOverrides(seen, endpointOverride) {
3893
+ const endpoint = endpointOverride ?? seen.get("--endpoint");
3894
+ return {
3895
+ ...endpoint !== void 0 ? { endpoint } : {},
3896
+ ...seen.get("--endpoint-cmd") !== void 0 ? { endpointCmd: seen.get("--endpoint-cmd") } : {},
3897
+ ...seen.get("--endpoint-cmd-stdio") !== void 0 ? { endpointCmdStdio: seen.get("--endpoint-cmd-stdio") } : {},
3898
+ ...endpointOverride === void 0 && seen.get("--endpoint-token") !== void 0 ? { endpointToken: seen.get("--endpoint-token") } : {},
3899
+ ...seen.has("--provision-identity") ? { provisionIdentity: true } : {},
3900
+ ...seen.get("--provision-identity-slot") !== void 0 ? { provisionIdentitySlot: seen.get("--provision-identity-slot") } : {},
3901
+ ...seen.get("--principal") !== void 0 ? { principal: seen.get("--principal") } : {},
3902
+ ...seen.get("--schema") !== void 0 ? { schema: seen.get("--schema") } : {}
3903
+ };
3904
+ }
3905
+ /**
3906
+ * Race `connect` against a soft deadline. On timeout, the connect promise's
3907
+ * resolved connection (if any) is closed so it doesn't leak.
3908
+ */
3909
+ async function _connectWithDeadline(endpoint, deadlineMs) {
3910
+ let timer;
3911
+ let timedOut = false;
3912
+ const timeout = new Promise((resolve) => {
3913
+ timer = setTimeout(() => {
3914
+ timedOut = true;
3915
+ resolve(void 0);
3916
+ }, deadlineMs);
3917
+ });
3918
+ try {
3919
+ const connectPromise = connect(endpoint).catch(() => void 0);
3920
+ const conn = await Promise.race([connectPromise, timeout]);
3921
+ if (timedOut) {
3922
+ connectPromise.then((late) => late?.close());
3923
+ return;
3924
+ }
3925
+ return conn ?? void 0;
3926
+ } finally {
3927
+ if (timer) clearTimeout(timer);
3928
+ }
3929
+ }
3930
+ /** Reject `p` with a timeout error after `deadlineMs`, without leaking the timer. */
3931
+ async function _withDeadline(p, deadlineMs) {
3932
+ let timer;
3933
+ const timeout = new Promise((_, reject) => {
3934
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("deadline exceeded")), deadlineMs);
3935
+ });
3936
+ try {
3937
+ return await Promise.race([p, timeout]);
3938
+ } finally {
3939
+ if (timer) clearTimeout(timer);
3940
+ }
3941
+ }
3942
+ function _endpointCacheKey(endpoint, staticSchema) {
3943
+ const endpointKey = endpoint.kind === "socket" ? `socket:${endpoint.path}` : endpoint.kind === "ws" ? `ws:${endpoint.url}` : endpoint.kind === "ws-no-init" ? `ws-no-init:${endpoint.url}` : `${endpoint.kind}:?`;
3944
+ return staticSchema === void 0 ? endpointKey : `${endpointKey}|schema:${JSON.stringify(staticSchema)}`;
3945
+ }
3946
+ //#endregion
3947
+ export { connectViaRootOverlay as A, parseParamOverride as B, findMethodInSchema as C, formatPrincipalSource as D, logoutCliIdentity as E, describeSchema as F, mcpForwardInterface as H, explainValidation as I, validateValueAgainstSchema as L, resolveTargetEndpoint as M, resolvedEndpointToConfig as N, parsePrincipalSpec as O, describeObjectParams as P, valueToConstSchema as R, fetchSchema$1 as S, walkHubDetailed as T, MethodRefWithOptHash as V, ChannelDirectorySource as _, resolveStaticHubSchemaSource as a, setupSigning as b, resolveInvocationEndpoint as c, formatContextReference as d, complete as f, parseLine as g, COMMAND_TREE as h, loadStaticHubSchema as i, connectViaTransport as j, connect as k, validationDefault as l, resolveSlot as m, StaticHubReflection as n, mutationReference as o, resolveCliInvocation as p, withStaticHubReflection as r, resolveInvocationContext as s, completeForLine as t, ContextStore as u, requestReflectionAccess as v, walkHub as w, fetchDefaults as x, requestTopologyAccess as y, mergeParams as z };
3948
+
3949
+ //# sourceMappingURL=runComplete-BkQwzPbF.js.map