@hediet/linkrpc-cli 0.0.3-next.20261006.1 → 0.0.3-next.20261006.2

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 CHANGED
@@ -267,6 +267,17 @@ Every endpoint is a valid `new URL()` — safe in env vars and logs:
267
267
  | `cmd:?command=…` / `cmd:?argv=…&argv=…` | spawn-as-server (`--endpoint-cmd`) |
268
268
  | `cmd-stdio:?command=…` / `?argv=…&argv=…` | spawn over stdio (`--endpoint-cmd-stdio`) |
269
269
 
270
+ `cmd:` children can use `startEndpoint({ onConnection })` without parsing any
271
+ transport flags: the parent injects `LINKRPC_ENDPOINT` and `LINKRPC_TOKEN`.
272
+ The CLI accepts one authenticated callback connection and exposes the child's
273
+ interfaces directly. It does not wait for a hub service-namespace claim.
274
+ The existing root overlay still supplies namespace (`local`) and optional
275
+ provisioned-identity services for hub-aware children. Full routing-hub startup
276
+ remains a separate `connectViaLocalHub` helper.
277
+ Child stdout/stderr are diagnostics and appear on the CLI's stderr; parent stdout
278
+ remains reserved for command results. Startup fails if the child exits, cannot
279
+ spawn, or does not connect within 10 seconds.
280
+
270
281
  The command payload is either a single verbatim string (`?command=node%20server.js`,
271
282
  split by the OS shell) or repeated, structure-preserving `argv` params
272
283
  (`?argv=node&argv=server.js`).
@@ -411,7 +422,7 @@ initial document:
411
422
  | `call <method> [--param k=v]... [--params <json>\|-]` | Invoke a request. Method is form 1/2/3 (`send`, `iface::send`, `svc::iface::send`). Validates params against the live schema by default; pass `--no-validate` to skip. Use `@hash` on the method ref (e.g. `iface::send@a3f2b1`) to look up a pinned schema version for local validation. Reads stdin for `--params -`. |
412
423
  | `notify <method> ...` | Fire-and-forget notification. Same flags as `call`. |
413
424
  | `batch --call <method> ... [--notify <method> ...]` | Run multiple calls and notifications sequentially on one connection. `--params`, repeatable `--param`, and `--no-validate` apply to the preceding operation. Fails fast unless `--continue-on-error` is set. Results are one JSON array; stream chunks are labeled on stderr. |
414
- | `connection create [--timeout 30s] [--ttl 5min] [--schema source]` | Open the configured remote once in a detached broker and print its authenticated local `unix:` / `npipe:` endpoint. `--timeout` is transport inactivity; `--ttl` is an absolute lifetime. `--schema` overlays static LinkRPC reflection from a local JSON file or an HTTP(S) URL. |
425
+ | `connection create --timeout <duration\|inf> [--ttl <duration\|inf>] [--schema source]` | Open the configured remote once in a detached broker and print its authenticated local `unix:` / `npipe:` endpoint. Explicitly supply at least one of `--timeout` (transport inactivity) or `--ttl` (absolute lifetime). Omitted limits are `inf`. `--schema` overlays static LinkRPC reflection from a local JSON file or an HTTP(S) URL. |
415
426
  | `connection status` | Read lifecycle and buffer status from the broker configured by the effective endpoint. |
416
427
  | `connection notifications [--after n] [--wait 25s] [--follow]` | Read incoming remote notifications from the broker's bounded buffer. `--follow` emits one JSON object per line and can run alongside calls. |
417
428
  | `connection destroy` | Stop the configured connection broker through its local overlay. |
@@ -490,6 +501,45 @@ hub batch \
490
501
 
491
502
  ### Persistent connections
492
503
 
504
+ Every `connection create` (including the legacy `connect` alias and context-owned
505
+ connections) requires an explicit `--timeout` or `--ttl`. Stored context limits
506
+ do not satisfy this requirement or supply omitted limits: each omitted limit is
507
+ `inf`. Both flags accept durations such as `250ms`, `30s`, `5min`, or `2h`, or the
508
+ literal `inf`. Finite limits must fit a Node timer (1 through 2147483647 ms).
509
+ Unlimited limits schedule no timer; status reports their `timeoutMs`/`ttlMs` as
510
+ the JSON string `"inf"`, while finite values remain millisecond numbers.
511
+ Broker startup has a separate 60-second readiness deadline; `inf` does not
512
+ disable startup error detection.
513
+ Command-environment endpoints also retain their existing 10-second deadline for
514
+ the spawned application to connect back. This is distinct from the transport
515
+ initialize handshake and the broker's lifetime limits. A slow application must
516
+ establish its endpoint before that deadline. Startup failures report the broker's
517
+ underlying error, including the last 16384 characters of command-environment child
518
+ stdout/stderr, rather than only the broker process's exit code.
519
+
520
+ Use a folder context to own the broker without environment overrides:
521
+
522
+ ```sh
523
+ rpc --endpoint-cmd "node ./server.js" connection create --timeout inf --new-context .
524
+ rpc call example::ping --no-validate
525
+ rpc context remove --context .
526
+ ```
527
+
528
+ Only `connection create --new-context <selector>` assigns ownership. Removing
529
+ that context, replacing its endpoint/token/config, or unsetting the owned endpoint
530
+ or token stops the owned broker before saving the mutation. This also applies to
531
+ persisted `--context-set` overrides. Unrelated edits and temporary overrides do
532
+ not stop it. Saving an existing endpoint or deriving another context with
533
+ `--new-context` does not transfer or duplicate ownership.
534
+
535
+ Already-stopped local brokers permit cleanup. Authentication, RPC, permission,
536
+ and other shutdown errors fail explicitly and preserve the context for retry.
537
+ The shutdown RPC has a five-second response deadline.
538
+ If saving fails after shutdown, the old context remains available for cleanup.
539
+ Contexts created by older versions have no ownership metadata and are not
540
+ automatically stopped. An unlimited broker without an owning context must be
541
+ stopped explicitly with `connection destroy`.
542
+
493
543
  `connection create` keeps one remote transport open in a detached process. It prints the
494
544
  actual local socket endpoint; store that endpoint in `LINKRPC_ENDPOINT` for later
495
545
  commands:
@@ -1,4 +1,4 @@
1
- import { A as connectViaRootOverlay, C as findMethodInSchema, D as formatPrincipalSource, E as logoutCliIdentity, F as describeSchema, H as mcpForwardInterface, I as explainValidation, L as validateValueAgainstSchema, M as resolveTargetEndpoint, N as resolvedEndpointToConfig, O as parsePrincipalSpec, P as describeObjectParams, S as fetchSchema$1, V as MethodRefWithOptHash, a as resolveStaticHubSchemaSource, b as setupSigning, c as resolveInvocationEndpoint, d as formatContextReference, i as loadStaticHubSchema, j as connectViaTransport, k as connect, l as validationDefault, n as StaticHubReflection, o as mutationReference, p as resolveCliInvocation, r as withStaticHubReflection, s as resolveInvocationContext, t as completeForLine, u as ContextStore, v as requestReflectionAccess, w as walkHub, x as fetchDefaults, y as requestTopologyAccess, z as mergeParams } from "./runComplete-CM1sMWnI.js";
1
+ import { A as fetchSchema$1, B as resolveTargetEndpoint, D as requestTopologyAccess, E as requestReflectionAccess, F as formatPrincipalSource, G as validateValueAgainstSchema, H as describeObjectParams, I as parsePrincipalSpec, L as connect, M as walkHub, O as setupSigning, P as logoutCliIdentity, R as connectViaRootOverlay, U as describeSchema, V as resolvedEndpointToConfig, W as explainValidation, X as mcpForwardInterface, Y as MethodRefWithOptHash, _ as parseConnectionLimit, a as resolveInvocationContext, c as ContextStore, d as getBrokerStatus, f as readBrokerNotifications, g as withStaticHubReflection, h as tunnelCommand, i as mutationReference, j as findMethodInSchema, k as fetchDefaults, l as formatContextReference, m as ConnectionBroker, n as loadStaticHubSchema, o as resolveInvocationEndpoint, p as stopConnectionBroker, q as mergeParams, r as resolveStaticHubSchemaSource, s as validationDefault, t as completeForLine, u as disconnectBroker, v as parseConnectionLimitMilliseconds, x as resolveCliInvocation, y as parseDuration, z as connectViaTransport } from "./runComplete-DvUneaDU.js";
2
2
  import { a as derived, i as autorun, r as observableValue } from "./dist-BOcSU4-O.js";
3
3
  import { ErrorCode, INTERFACE_TEMPLATES_EXTENSION, LinkRpcConnection, RpcError, computeInterfaceHash, connectTransports, createSchemaToZod, exportStaticHubSchema, generateTsContract, generateTsInterface, interfaceTemplateArgumentsEquivalent, interfaceTemplateMemberName, isAssignable, jcsCanonicalize } from "@hediet/linkrpc";
4
4
  import { WebSocketTransport, connectNdjson, formatEndpointUri, isHubEndpoint, openWebSocket, parseEndpointUri, runInitializeHandshake, traceMessageTransport as traceMessageTransport$1 } from "@hediet/linkrpc/node";
@@ -11,7 +11,7 @@ import { spawnCommand } from "@hediet/linkrpc-hub/spawn";
11
11
  import { tapTransport } from "@hediet/linkrpc-hub/hub/server/transit";
12
12
  import * as net from "node:net";
13
13
  import { mkdir, open, readFile, writeFile } from "node:fs/promises";
14
- import { DEFAULT_WALK_DEPTH, HubDirectoryExplorer, fetchSchema, hubGrantedServiceIdInterface, hubServiceIdRegistryInterface } from "@hediet/linkrpc/hub/common";
14
+ import { DEFAULT_WALK_DEPTH, HubDirectoryExplorer, fetchSchema, hubGrantedServiceIdInterface } from "@hediet/linkrpc/hub/common";
15
15
  import { Command, InvalidArgumentError } from "commander";
16
16
  import { bootstrapApprovalCapability, createHubApprovalClient } from "@hediet/linkrpc-infra/approval";
17
17
  import { connectionTokenBinderInterface } from "@hediet/linkrpc-hub/hub/server/connection-token-binder";
@@ -717,340 +717,6 @@ function getErrorMessage(error) {
717
717
  return error instanceof Error ? error.message : String(error);
718
718
  }
719
719
  //#endregion
720
- //#region src/commands/tunnel.ts
721
- /**
722
- * The wire method for the provenance claim front door. Mirrors
723
- * `Hub.registerServiceIdNamespace`, which calls
724
- * `connection.get(hubGrantedServiceIdInterface).register({ serviceId })`
725
- * with no serviceId prefix — i.e. interface-form
726
- * `hubGrantedServiceId::register`. The claimed serviceId must be
727
- * within the connection's granted namespace.
728
- */
729
- const CLAIM_METHOD$1 = `${hubGrantedServiceIdInterface.info.id}::register`;
730
- const GET_CLAIM_NAMESPACE_METHOD = `${hubGrantedServiceIdInterface.info.id}::get`;
731
- const GET_HUB_SERVICE_ID_METHOD = `${hubGrantedServiceIdInterface.info.id}::getHubServiceId`;
732
- /**
733
- * Run a transparent service tunnel until stopped.
734
- *
735
- * Claims `serviceId` on {@link TunnelOptions.local} via the hub claim
736
- * interface, then forwards every inbound request/notification verbatim onto
737
- * {@link TunnelOptions.remote} and pipes the response back. Forwarding is
738
- * signature-preserving: the original caller's `$hubrpc` envelope rides inside
739
- * `params`, so the remote send is issued unsigned (`signerOverride: null`) to
740
- * avoid re-wrapping it. The streaming sub-protocol is bridged both ways, so
741
- * long-running / streaming calls forward their in-flight stream messages and
742
- * cancellation, not just the final response.
743
- *
744
- * When {@link TunnelOptions.bidi} is set, the mirror forwarder is also bound on
745
- * {@link TunnelOptions.remote}, so every request/notification the endpoint
746
- * emits is forwarded verbatim back to the hub.
747
- *
748
- * Resolves when {@link TunnelOptions.stop} resolves (or on `SIGINT`/`SIGTERM`).
749
- */
750
- async function tunnelCommand(opts) {
751
- const log = opts.log ?? ((line) => process.stderr.write(line + "\n"));
752
- opts.local.setRequestHandler(createForwardingHandler(opts.remote));
753
- if (opts.bidi) opts.remote.setRequestHandler(createForwardingHandler(opts.local));
754
- await _claimServiceId(opts.local, opts.serviceId);
755
- log(opts.bidi ? `tunnel: claimed '${opts.serviceId}' — forwarding requests to remote (bidirectional)` : `tunnel: claimed '${opts.serviceId}' — forwarding requests to remote`);
756
- await (opts.stop ?? _untilSignalled$2());
757
- log("tunnel: stopping");
758
- }
759
- /**
760
- * Build the inbound handler that forwards every request/notification received
761
- * on one connection onto `to`, bridging the streaming sub-protocol both ways
762
- * (see {@link _forwardRequest}). Used for the source→target direction and, in
763
- * bidirectional mode, the mirror.
764
- */
765
- function createForwardingHandler(to) {
766
- return {
767
- handleRequest: (call) => _forwardRequest(to, call),
768
- handleNotification: (call) => _forwardNotification(to, call)
769
- };
770
- }
771
- async function _claimServiceId(local, serviceId) {
772
- const namespaceRes = await local.channel.sendRequest(GET_CLAIM_NAMESPACE_METHOD, {});
773
- if (_isServiceIdUnder(serviceId, typeof namespaceRes === "object" && namespaceRes !== null && "grantedServiceIdNamespace" in namespaceRes && typeof namespaceRes.grantedServiceIdNamespace === "string" ? namespaceRes.grantedServiceIdNamespace : "")) {
774
- await local.channel.sendRequest(CLAIM_METHOD$1, { serviceId });
775
- return;
776
- }
777
- const hubIdRes = await local.channel.sendRequest(GET_HUB_SERVICE_ID_METHOD, {});
778
- const hubServiceId = typeof hubIdRes === "object" && hubIdRes !== null && "hubServiceId" in hubIdRes && typeof hubIdRes.hubServiceId === "string" ? hubIdRes.hubServiceId : void 0;
779
- if (!hubServiceId) throw new Error("tunnel: failed to resolve hub service id for out-of-namespace claim");
780
- const registerMethod = `${hubServiceId}::${hubServiceIdRegistryInterface.info.id}::registerServiceId`;
781
- await local.channel.sendRequest(registerMethod, { requestedPrefix: serviceId });
782
- }
783
- function _isServiceIdUnder(serviceId, namespace) {
784
- if (namespace === "") return false;
785
- return serviceId === namespace || serviceId.startsWith(`${namespace}/`);
786
- }
787
- /** Resolve on the next `SIGINT` / `SIGTERM`. */
788
- function _untilSignalled$2() {
789
- return new Promise((resolve) => {
790
- const done = () => {
791
- process.removeListener("SIGINT", done);
792
- process.removeListener("SIGTERM", done);
793
- resolve();
794
- };
795
- process.once("SIGINT", done);
796
- process.once("SIGTERM", done);
797
- });
798
- }
799
- async function _forwardRequest(remote, call) {
800
- const rc = remote.channel.sendRequestWithStream(call.method, call.params, {
801
- ctx: { signerOverride: null },
802
- onStreamMessage: (payload) => {
803
- call.stream.send(payload);
804
- }
805
- });
806
- call.stream.onMessage((payload) => rc.send(payload));
807
- const cancelRemote = () => rc.cancel(call.signal.reason?.message);
808
- if (call.signal.aborted) cancelRemote();
809
- else call.signal.addEventListener("abort", cancelRemote, { once: true });
810
- try {
811
- return { result: await rc.result };
812
- } catch (err) {
813
- if (err instanceof RpcError) return { error: {
814
- code: err.code,
815
- message: err.message,
816
- data: err.data
817
- } };
818
- return { error: {
819
- code: ErrorCode.internalError,
820
- message: err.message
821
- } };
822
- }
823
- }
824
- function _forwardNotification(remote, call) {
825
- remote.channel.sendNotification(call.method, call.params, { ctx: { signerOverride: null } }).catch(() => {});
826
- }
827
- //#endregion
828
- //#region src/commands/connectionBroker.ts
829
- const BROKER_INTERFACE_ID = "hubrpc.connectionBroker";
830
- const BROKER_STATUS_METHOD = `${BROKER_INTERFACE_ID}::status`;
831
- const BROKER_READ_NOTIFICATIONS_METHOD = `${BROKER_INTERFACE_ID}::readNotifications`;
832
- const BROKER_DISCONNECT_METHOD = `${BROKER_INTERFACE_ID}::disconnect`;
833
- var ConnectionBroker = class {
834
- _remote;
835
- _options;
836
- _now;
837
- _notificationLimit;
838
- _notifications = [];
839
- _waiters = /* @__PURE__ */ new Set();
840
- _remoteForwarder;
841
- _staticReflection;
842
- _startedAt;
843
- _nextNotificationSequence = 1;
844
- _locals = /* @__PURE__ */ new Set();
845
- _timeoutTimer;
846
- _ttlTimer;
847
- _lastActivityAt;
848
- _resolveStopped;
849
- _disposed = false;
850
- stopped = new Promise((resolve) => {
851
- this._resolveStopped = resolve;
852
- });
853
- stoppedReason;
854
- constructor(_remote, _options) {
855
- this._remote = _remote;
856
- this._options = _options;
857
- this._now = _options.now ?? Date.now;
858
- this._notificationLimit = _options.notificationLimit ?? 1e3;
859
- this._startedAt = _options.startedAt;
860
- this._lastActivityAt = _options.startedAt;
861
- this._remoteForwarder = createForwardingHandler(_remote);
862
- this._staticReflection = _options.staticHubSchema === void 0 ? void 0 : new StaticHubReflection(_options.staticHubSchema);
863
- this._remote.setRequestHandler({
864
- handleRequest: (call) => this._handleRemoteRequest(call),
865
- handleNotification: (call) => this._bufferNotification(call)
866
- });
867
- this._resetTimeout();
868
- this._ttlTimer = setTimeout(() => this._stop("ttl"), Math.max(0, this._options.ttlMs - (this._now() - this._startedAt)));
869
- this._ttlTimer.unref?.();
870
- }
871
- attach(local) {
872
- if (this._disposed) throw new Error("connection broker is stopped");
873
- this._locals.add(local);
874
- local.setRequestHandler({
875
- handleRequest: (call) => this._handleLocalRequest(call),
876
- handleNotification: (call) => this._handleLocalNotification(call)
877
- });
878
- this.recordActivity();
879
- }
880
- detach(local) {
881
- if (!this._locals.delete(local)) return;
882
- local.setRequestHandler(void 0);
883
- }
884
- recordActivity() {
885
- if (this._disposed) return;
886
- this._lastActivityAt = this._now();
887
- this._resetTimeout();
888
- }
889
- dispose() {
890
- this._stop("disposed");
891
- }
892
- async _handleLocalRequest(call) {
893
- this.recordActivity();
894
- switch (call.method) {
895
- case BROKER_STATUS_METHOD: return { result: this._status() };
896
- case BROKER_READ_NOTIFICATIONS_METHOD: return { result: await this._readNotifications(call.params, call.signal) };
897
- case BROKER_DISCONNECT_METHOD:
898
- setTimeout(() => this._stop("disconnect"), 0);
899
- return { result: { disconnected: true } };
900
- default:
901
- if (call.method.startsWith(`hubrpc.connectionBroker::`)) return { error: {
902
- code: ErrorCode.methodNotFound,
903
- message: `Unknown connection broker method: ${call.method}`
904
- } };
905
- const reflected = this._staticReflection?.tryHandleRequest(call);
906
- if (reflected !== void 0) return reflected;
907
- return this._remoteForwarder.handleRequest(call);
908
- }
909
- }
910
- _handleLocalNotification(call) {
911
- this.recordActivity();
912
- if (call.method.startsWith(`hubrpc.connectionBroker::`)) return;
913
- this._remoteForwarder.handleNotification(call);
914
- }
915
- _handleRemoteRequest(call) {
916
- this.recordActivity();
917
- if (this._locals.size !== 1) return Promise.resolve({ error: {
918
- code: ErrorCode.peerDisconnected,
919
- message: this._locals.size === 0 ? "No local broker client is attached" : "Cannot route a reverse request while multiple local broker clients are attached"
920
- } });
921
- const local = this._locals.values().next().value;
922
- return createForwardingHandler(local).handleRequest(call);
923
- }
924
- _bufferNotification(call) {
925
- this.recordActivity();
926
- const notification = {
927
- sequence: this._nextNotificationSequence++,
928
- receivedAt: this._now(),
929
- method: call.method,
930
- ...call.params === void 0 ? {} : { params: call.params }
931
- };
932
- this._notifications.push(notification);
933
- if (this._notifications.length > this._notificationLimit) this._notifications.splice(0, this._notifications.length - this._notificationLimit);
934
- this._settleReadyWaiters();
935
- }
936
- _status() {
937
- return {
938
- id: this._options.id,
939
- mode: this._options.mode,
940
- remoteEndpoint: this._options.remoteEndpoint,
941
- startedAt: this._startedAt,
942
- lastActivityAt: this._lastActivityAt,
943
- timeoutMs: this._options.timeoutMs,
944
- ttlMs: this._options.ttlMs,
945
- notificationCount: this._notifications.length,
946
- nextNotificationSequence: this._nextNotificationSequence,
947
- localClientCount: this._locals.size
948
- };
949
- }
950
- _readNotifications(params, signal) {
951
- const parsed = parseReadParams(params);
952
- const immediate = this._collectNotifications(parsed.after);
953
- if (immediate.notifications.length > 0 || parsed.waitMs === 0) return Promise.resolve(immediate);
954
- return new Promise((resolve) => {
955
- const waiter = {
956
- after: parsed.after,
957
- resolve,
958
- timer: setTimeout(() => this._settleWaiter(waiter), parsed.waitMs),
959
- signal,
960
- onAbort: () => this._settleWaiter(waiter)
961
- };
962
- waiter.timer.unref?.();
963
- signal.addEventListener("abort", waiter.onAbort, { once: true });
964
- this._waiters.add(waiter);
965
- });
966
- }
967
- _collectNotifications(after) {
968
- const notifications = this._notifications.filter((item) => item.sequence > after);
969
- return {
970
- notifications,
971
- next: notifications.at(-1)?.sequence ?? after,
972
- droppedBefore: this._notifications[0]?.sequence ?? this._nextNotificationSequence
973
- };
974
- }
975
- _settleReadyWaiters() {
976
- for (const waiter of [...this._waiters]) if (this._notifications.some((item) => item.sequence > waiter.after)) this._settleWaiter(waiter);
977
- }
978
- _settleWaiter(waiter) {
979
- if (!this._waiters.delete(waiter)) return;
980
- clearTimeout(waiter.timer);
981
- waiter.signal.removeEventListener("abort", waiter.onAbort);
982
- waiter.resolve(this._collectNotifications(waiter.after));
983
- }
984
- _resetTimeout() {
985
- if (this._timeoutTimer !== void 0) clearTimeout(this._timeoutTimer);
986
- this._timeoutTimer = setTimeout(() => this._stop("timeout"), this._options.timeoutMs);
987
- this._timeoutTimer.unref?.();
988
- }
989
- _stop(reason) {
990
- if (this._disposed) return;
991
- this._disposed = true;
992
- this.stoppedReason = reason;
993
- if (this._timeoutTimer !== void 0) clearTimeout(this._timeoutTimer);
994
- if (this._ttlTimer !== void 0) clearTimeout(this._ttlTimer);
995
- for (const waiter of [...this._waiters]) this._settleWaiter(waiter);
996
- for (const local of this._locals) local.close();
997
- this._locals.clear();
998
- this._remote.close();
999
- this._resolveStopped(reason);
1000
- this._options.onStop?.(reason);
1001
- }
1002
- };
1003
- function parseReadParams(params) {
1004
- if (params === void 0) return {
1005
- after: 0,
1006
- waitMs: 0
1007
- };
1008
- if (typeof params !== "object" || params === null || Array.isArray(params)) throw new Error("readNotifications params must be an object");
1009
- const after = "after" in params ? params.after : 0;
1010
- const waitMs = "waitMs" in params ? params.waitMs : 0;
1011
- if (typeof after !== "number" || !Number.isSafeInteger(after) || after < 0) throw new Error("readNotifications after must be a non-negative integer");
1012
- if (typeof waitMs !== "number" || !Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > 3e4) throw new Error("readNotifications waitMs must be an integer between 0 and 30000");
1013
- return {
1014
- after,
1015
- waitMs
1016
- };
1017
- }
1018
- //#endregion
1019
- //#region src/commands/connectionBrokerClient.ts
1020
- function getBrokerStatus(channel) {
1021
- return channel.sendRequest(BROKER_STATUS_METHOD, {});
1022
- }
1023
- async function disconnectBroker(channel) {
1024
- await channel.sendRequest(BROKER_DISCONNECT_METHOD, {});
1025
- }
1026
- async function readBrokerNotifications(channel, options = {}) {
1027
- const value = await channel.sendRequest(BROKER_READ_NOTIFICATIONS_METHOD, {
1028
- after: options.after ?? 0,
1029
- waitMs: options.waitMs ?? 0
1030
- });
1031
- if (!isRecord(value) || !Array.isArray(value.notifications)) throw new Error("connection broker returned an invalid notification batch");
1032
- const next = value.next;
1033
- const droppedBefore = value.droppedBefore;
1034
- if (typeof next !== "number" || !Number.isSafeInteger(next) || typeof droppedBefore !== "number" || !Number.isSafeInteger(droppedBefore)) throw new Error("connection broker returned invalid notification sequence metadata");
1035
- return {
1036
- notifications: value.notifications.map(parseNotification),
1037
- next,
1038
- droppedBefore
1039
- };
1040
- }
1041
- function parseNotification(value) {
1042
- if (!isRecord(value) || typeof value.sequence !== "number" || !Number.isSafeInteger(value.sequence) || typeof value.receivedAt !== "number" || !Number.isSafeInteger(value.receivedAt) || typeof value.method !== "string") throw new Error("connection broker returned an invalid notification");
1043
- return {
1044
- sequence: value.sequence,
1045
- receivedAt: value.receivedAt,
1046
- method: value.method,
1047
- ..."params" in value ? { params: value.params } : {}
1048
- };
1049
- }
1050
- function isRecord(value) {
1051
- return typeof value === "object" && value !== null && !Array.isArray(value);
1052
- }
1053
- //#endregion
1054
720
  //#region src/commands/connectionBrokerProcess.ts
1055
721
  async function runConnectionBroker(options) {
1056
722
  const startedAt = Date.now();
@@ -1139,7 +805,7 @@ async function spawnConnectionBroker(options) {
1139
805
  throw new Error("connection broker child exposes no stdout");
1140
806
  }
1141
807
  try {
1142
- const endpoint = await readReadyEndpoint(child, stdout, options.readyTimeoutMs ?? 15e3);
808
+ const endpoint = await readReadyEndpoint(child, stdout, options.readyTimeoutMs ?? 6e4);
1143
809
  stdout.destroy();
1144
810
  child.unref();
1145
811
  return endpoint;
@@ -1162,6 +828,11 @@ function readReadyEndpoint(child, stdout, timeoutMs) {
1162
828
  const endpoint = buffer.slice(0, newline).trim();
1163
829
  cleanup();
1164
830
  try {
831
+ if (endpoint.startsWith("{")) {
832
+ const response = JSON.parse(endpoint);
833
+ if (typeof response === "object" && response !== null && "error" in response && typeof response.error === "string") throw new Error(`connection broker failed to start: ${response.error}`);
834
+ throw new Error("connection broker returned an invalid startup response");
835
+ }
1165
836
  const parsed = new URL(endpoint);
1166
837
  if (parsed.protocol !== "unix:" && parsed.protocol !== "npipe:") throw new Error(`unexpected broker endpoint '${endpoint}'`);
1167
838
  resolve(endpoint);
@@ -1181,31 +852,14 @@ function readReadyEndpoint(child, stdout, timeoutMs) {
1181
852
  clearTimeout(timer);
1182
853
  stdout.removeListener("data", onData);
1183
854
  child.removeListener("error", onError);
1184
- child.removeListener("exit", onExit);
855
+ child.removeListener("close", onExit);
1185
856
  };
1186
857
  stdout.on("data", onData);
1187
858
  child.once("error", onError);
1188
- child.once("exit", onExit);
859
+ child.once("close", onExit);
1189
860
  });
1190
861
  }
1191
862
  //#endregion
1192
- //#region src/duration.ts
1193
- const DURATION_PATTERN = /^(\d+)(ms|s|m|min|h)$/;
1194
- const UNIT_MS = {
1195
- ms: 1,
1196
- s: 1e3,
1197
- m: 6e4,
1198
- min: 6e4,
1199
- h: 36e5
1200
- };
1201
- function parseDuration(value) {
1202
- const match = DURATION_PATTERN.exec(value.trim());
1203
- if (match === null) throw new Error(`invalid duration '${value}' (expected e.g. 250ms, 30s, 5min, or 2h)`);
1204
- const durationMs = Number.parseInt(match[1], 10) * UNIT_MS[match[2]];
1205
- if (!Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error(`invalid duration '${value}' (must be greater than zero)`);
1206
- return durationMs;
1207
- }
1208
- //#endregion
1209
863
  //#region src/commands/checkCompat.ts
1210
864
  async function checkCompatCommand(channel, opts) {
1211
865
  return diffSchemas(JSON.parse(readFileSync(opts.localPath, "utf8")), await fetchSchema$1(channel, opts.interfaceId, void 0));
@@ -6584,17 +6238,18 @@ Batch options:
6584
6238
  }, { requestReflectionAccess: effectiveValidation(profile) !== "off" && plan.operations.some((operation) => !operation.noValidate) });
6585
6239
  });
6586
6240
  const connection = program.command("connection").description("Create, inspect, consume, and destroy persistent RPC connections.");
6587
- connection.command("create").description("Create a detached persistent connection and print its local endpoint.").option("--timeout <duration>", "exit after this much transport inactivity").option("--ttl <duration>", "hard maximum broker lifetime").option("--notification-limit <count>", "maximum buffered incoming notifications", (value) => parsePositiveInteger(value, "--notification-limit")).action(async (opts) => {
6241
+ connection.command("create").description("Create a detached persistent connection and print its local endpoint.").option("--timeout <duration>", "transport inactivity limit (duration or inf; omitted: inf)").option("--ttl <duration>", "hard lifetime limit (duration or inf; omitted: inf)").option("--notification-limit <count>", "maximum buffered incoming notifications", (value) => parsePositiveInteger(value, "--notification-limit")).action(async (opts) => {
6242
+ if (opts.timeout === void 0 && opts.ttl === void 0) throw new Error("connection create requires an explicit --timeout or --ttl (use inf for unlimited)");
6588
6243
  if (g_hubConfigPath !== void 0) throw new Error("connection create does not support --config; use an endpoint URI or command endpoint");
6589
6244
  const schemaSource = g_invocation?.values.schema === void 0 ? void 0 : resolveStaticHubSchemaSource(g_invocation.values.schema);
6590
6245
  if (schemaSource !== void 0) await loadStaticHubSchema(schemaSource);
6591
- const timeout = opts.timeout ?? g_invocation?.values.connectionTimeout ?? "30s";
6592
- const ttl = opts.ttl ?? g_invocation?.values.connectionTtl ?? "5min";
6246
+ const timeout = opts.timeout ?? "inf";
6247
+ const ttl = opts.ttl ?? "inf";
6593
6248
  const notificationLimit = opts.notificationLimit ?? g_invocation?.values.notificationLimit ?? 1e3;
6594
6249
  const brokerEndpoint = await spawnConnectionBroker({
6595
6250
  remote: needEndpoint(endpoint),
6596
- timeoutMs: parseDuration(timeout),
6597
- ttlMs: parseDuration(ttl),
6251
+ timeoutMs: parseConnectionLimit(timeout),
6252
+ ttlMs: parseConnectionLimit(ttl),
6598
6253
  notificationLimit,
6599
6254
  schemaSource
6600
6255
  });
@@ -6659,14 +6314,14 @@ Batch options:
6659
6314
  process.stdout.write(`Updated context ${formatContextReference(target)}.\n`);
6660
6315
  if (result !== void 0) printContextValues(result.values, false);
6661
6316
  });
6662
- context.command("remove").description("Remove the selected context from the global store.").action(async () => {
6317
+ context.command("remove").description("Stop any owned connection and remove the selected context from the global store.").action(async () => {
6663
6318
  if (g_contextStore === void 0 || g_invocation === void 0) throw new Error("context resolver is unavailable");
6664
6319
  const reference = g_invocation.selected.reference;
6665
6320
  if (reference.kind === "empty" || reference.kind === "root" && explicitOption(program, "context") === void 0) throw new Error("select the context to remove explicitly with --context");
6666
6321
  if (!await g_contextStore.remove(reference)) throw new Error(`context ${formatContextReference(reference)} does not exist`);
6667
6322
  process.stdout.write(`Removed context ${formatContextReference(reference)}.\n`);
6668
6323
  });
6669
- program.command("_connection-broker").description("(internal) Run a persistent connection broker.").requiredOption("--remote-endpoint <uri>").requiredOption("--timeout-ms <milliseconds>", "", (value) => parsePositiveInteger(value, "--timeout-ms")).requiredOption("--ttl-ms <milliseconds>", "", (value) => parsePositiveInteger(value, "--ttl-ms")).option("--notification-limit <count>", "", (value) => parsePositiveInteger(value, "--notification-limit"), 1e3).option("--schema <path-or-url>").action(async (opts) => {
6324
+ program.command("_connection-broker").description("(internal) Run a persistent connection broker.").requiredOption("--remote-endpoint <uri>").requiredOption("--timeout-ms <milliseconds-or-inf>", "", parseConnectionLimitMilliseconds).requiredOption("--ttl-ms <milliseconds-or-inf>", "", parseConnectionLimitMilliseconds).option("--notification-limit <count>", "", (value) => parsePositiveInteger(value, "--notification-limit"), 1e3).option("--schema <path-or-url>").action(async (opts) => {
6670
6325
  const running = await runConnectionBroker({
6671
6326
  remote: parseEndpointUri(opts.remoteEndpoint),
6672
6327
  timeoutMs: opts.timeoutMs,
@@ -6919,7 +6574,7 @@ Batch options:
6919
6574
  process.once("SIGINT", onSignal);
6920
6575
  process.once("SIGTERM", onSignal);
6921
6576
  try {
6922
- const { runApprovalUi } = await import("./runApprovalUi-eLDf_5ZN.js");
6577
+ const { runApprovalUi } = await import("./runApprovalUi-BFI84eck.js");
6923
6578
  await runApprovalUi({
6924
6579
  client,
6925
6580
  signal: stop.signal
@@ -6934,7 +6589,7 @@ Batch options:
6934
6589
  registerViewCommands(program, (action) => withChannel(endpoint, getPrincipalSpec(), action));
6935
6590
  program.command("ui").description("Launch the terminal UI (form-based browser of services + methods).").action(async () => {
6936
6591
  const acquired = await acquireConnection(endpoint);
6937
- const { runUi } = await import("./runUi-DF_HTMk9.js");
6592
+ const { runUi } = await import("./runUi-BNCT8zwR.js");
6938
6593
  try {
6939
6594
  await runUi({
6940
6595
  endpoint: acquired.ep,
@@ -7011,8 +6666,11 @@ function copyExplicit(command, source, target, optionName, contextName = optionN
7011
6666
  function explicitOption(command, name) {
7012
6667
  return command.getOptionValueSource(name) === "cli" ? command.opts()[name] : void 0;
7013
6668
  }
7014
- async function createContextFromInvocation(store, reference, values) {
7015
- await store.replace(reference, values, { createOnly: true });
6669
+ async function createContextFromInvocation(store, reference, values, ownedConnection) {
6670
+ await store.replace(reference, values, {
6671
+ createOnly: true,
6672
+ ownedConnection
6673
+ });
7016
6674
  process.stderr.write(`linkrpc: created context ${formatContextReference(reference)}.\n`);
7017
6675
  }
7018
6676
  async function createConnectionContext(store, reference, values, brokerEndpoint) {
@@ -7021,7 +6679,7 @@ async function createConnectionContext(store, reference, values, brokerEndpoint)
7021
6679
  await createContextFromInvocation(store, reference, {
7022
6680
  ...rest,
7023
6681
  ...connectionEndpoint
7024
- });
6682
+ }, { endpoint: brokerEndpoint });
7025
6683
  }
7026
6684
  function splitEndpointToken(endpoint) {
7027
6685
  const parsed = parseEndpointUri(endpoint);
@@ -7033,14 +6691,6 @@ function splitEndpointToken(endpoint) {
7033
6691
  endpointToken: token
7034
6692
  };
7035
6693
  }
7036
- async function stopConnectionBroker(endpoint) {
7037
- const connection = await connect(parseEndpointUri(endpoint));
7038
- try {
7039
- await disconnectBroker(connection.channel);
7040
- } finally {
7041
- connection.close();
7042
- }
7043
- }
7044
6694
  function mutableReference(reference) {
7045
6695
  if (reference.kind === "empty") throw new Error(":empty is immutable and cannot be modified");
7046
6696
  return reference;
@@ -7344,8 +6994,15 @@ function parseInterfaceRef(raw) {
7344
6994
  };
7345
6995
  }
7346
6996
  function runCli(executableName) {
7347
- main(process.argv.slice(2), executableName).catch((e) => {
7348
- process.stderr.write(`linkrpc: ${formatCliError(e)}\n`);
6997
+ main(process.argv.slice(2), executableName).catch(async (e) => {
6998
+ const message = formatCliError(e);
6999
+ process.stderr.write(`linkrpc: ${message}\n`);
7000
+ if (process.argv[2] === "_connection-broker") await new Promise((resolve, reject) => {
7001
+ process.stdout.write(JSON.stringify({ error: message }) + "\n", (error) => {
7002
+ if (error) reject(error);
7003
+ else resolve();
7004
+ });
7005
+ });
7349
7006
  process.exit(1);
7350
7007
  });
7351
7008
  }
@@ -7367,4 +7024,4 @@ function safeJson(value) {
7367
7024
  //#endregion
7368
7025
  export { formatCommandLegend as a, safeTerminalText as c, interfaceTarget as d, matchesTarget as f, dispatchHostKey as i, resolveViewTarget as l, approvalDetailLines as m, views as n, hostCommands as o, targetId as p, ViewFrame as r, matchesKey as s, runCli as t, formatCondition as u };
7369
7026
 
7370
- //# sourceMappingURL=cli-yDtBqjCT.js.map
7027
+ //# sourceMappingURL=cli-JE4URhh3.js.map