@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.
- package/README.md +370 -0
- package/dist/chunks/cli-D_-vlAa2.js +6073 -0
- package/dist/chunks/cli-D_-vlAa2.js.map +1 -0
- package/dist/chunks/runApprovalUi-BvJZdjKm.js +715 -0
- package/dist/chunks/runApprovalUi-BvJZdjKm.js.map +1 -0
- package/dist/chunks/runComplete-BkQwzPbF.js +3949 -0
- package/dist/chunks/runComplete-BkQwzPbF.js.map +1 -0
- package/dist/chunks/runUi-D-8LMU4F.js +1143 -0
- package/dist/chunks/runUi-D-8LMU4F.js.map +1 -0
- package/dist/chunks/scroll-BTzAYh4N.js +93 -0
- package/dist/chunks/scroll-BTzAYh4N.js.map +1 -0
- package/dist/hub.d.ts +1 -0
- package/dist/hub.js +8 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +381 -0
- package/dist/index.js +2 -0
- package/dist/linkrpc.d.ts +1 -0
- package/dist/linkrpc.js +8 -0
- package/dist/linkrpc.js.map +1 -0
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +8 -0
- package/dist/rpc.js.map +1 -0
- package/package.json +45 -0
|
@@ -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
|