@drisp/cli 0.5.8 → 0.5.11

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.
@@ -24,6 +24,159 @@ import {
24
24
  // src/gateway/daemon.ts
25
25
  import fs4 from "fs";
26
26
 
27
+ // src/gateway/channelManager.ts
28
+ var DEFAULT_DEDUP_WINDOW = 1024;
29
+ var DuplicateChannelError = class extends Error {
30
+ constructor(id) {
31
+ super(`channel ${id} already registered`);
32
+ this.name = "DuplicateChannelError";
33
+ }
34
+ };
35
+ var UnknownChannelError = class extends Error {
36
+ constructor(id) {
37
+ super(`channel ${id} not registered`);
38
+ this.name = "UnknownChannelError";
39
+ }
40
+ };
41
+ var ChannelManager = class {
42
+ entries = /* @__PURE__ */ new Map();
43
+ dedup = [];
44
+ dedupSet = /* @__PURE__ */ new Set();
45
+ dedupMax;
46
+ log;
47
+ inboundSink = null;
48
+ healthSink = null;
49
+ stopped = false;
50
+ constructor(opts = {}) {
51
+ this.dedupMax = opts.dedupWindow ?? DEFAULT_DEDUP_WINDOW;
52
+ this.log = opts.log;
53
+ }
54
+ /** Register the single inbound dispatch target. M5 wires the router here. */
55
+ setInboundSink(sink) {
56
+ this.inboundSink = sink;
57
+ }
58
+ /**
59
+ * Returns the attachmentId associated with `channelId` at registration
60
+ * time, or undefined if the channel is unknown or registered without one.
61
+ */
62
+ getAttachmentId(channelId) {
63
+ return this.entries.get(channelId)?.attachmentId;
64
+ }
65
+ setHealthSink(sink) {
66
+ this.healthSink = sink;
67
+ }
68
+ listChannels() {
69
+ return [...this.entries.values()].map((e) => ({
70
+ id: e.adapter.id,
71
+ health: e.lastHealth
72
+ }));
73
+ }
74
+ /** Snapshot of currently registered adapters; used by the relay coordinator. */
75
+ listAdapters() {
76
+ return [...this.entries.values()].map((e) => e.adapter);
77
+ }
78
+ async register(adapter, opts = {}) {
79
+ if (this.stopped) {
80
+ throw new Error("channel manager already stopped");
81
+ }
82
+ if (this.entries.has(adapter.id)) {
83
+ throw new DuplicateChannelError(adapter.id);
84
+ }
85
+ const abort = new AbortController();
86
+ const inboundListener = (msg) => this.handleInbound(adapter.id, msg);
87
+ const healthListener = (sample) => {
88
+ const entry2 = this.entries.get(adapter.id);
89
+ if (entry2) entry2.lastHealth = sample;
90
+ this.healthSink?.(sample);
91
+ };
92
+ const startPromise = adapter.start({
93
+ log: (level, msg) => this.log?.(level, `[${adapter.id}] ${msg}`),
94
+ signal: abort.signal,
95
+ emitInbound: inboundListener,
96
+ emitHealth: healthListener
97
+ });
98
+ const entry = {
99
+ adapter,
100
+ abort,
101
+ startPromise
102
+ };
103
+ if (opts.attachmentId !== void 0) entry.attachmentId = opts.attachmentId;
104
+ this.entries.set(adapter.id, entry);
105
+ try {
106
+ await startPromise;
107
+ } catch (err) {
108
+ this.entries.delete(adapter.id);
109
+ throw err;
110
+ }
111
+ }
112
+ async unregister(id, reason) {
113
+ const entry = this.entries.get(id);
114
+ if (!entry) {
115
+ throw new UnknownChannelError(id);
116
+ }
117
+ this.entries.delete(id);
118
+ entry.abort.abort();
119
+ await entry.adapter.stop(reason);
120
+ }
121
+ async send(channelId, msg) {
122
+ const entry = this.entries.get(channelId);
123
+ if (!entry) {
124
+ throw new UnknownChannelError(channelId);
125
+ }
126
+ return entry.adapter.send(msg);
127
+ }
128
+ async probe(channelId) {
129
+ const entry = this.entries.get(channelId);
130
+ if (!entry) {
131
+ throw new UnknownChannelError(channelId);
132
+ }
133
+ return entry.adapter.probe();
134
+ }
135
+ async stop(reason = "shutdown") {
136
+ if (this.stopped) return;
137
+ this.stopped = true;
138
+ const ids = [...this.entries.keys()];
139
+ for (const id of ids.reverse()) {
140
+ try {
141
+ await this.unregister(id, reason);
142
+ } catch (err) {
143
+ this.log?.(
144
+ "warn",
145
+ `channel ${id} stop failed: ${err instanceof Error ? err.message : String(err)}`
146
+ );
147
+ }
148
+ }
149
+ }
150
+ handleInbound(channelId, msg) {
151
+ if (this.dedupSet.has(msg.idempotencyKey)) {
152
+ this.log?.("debug", `dropping duplicate inbound ${msg.idempotencyKey}`);
153
+ return;
154
+ }
155
+ this.dedupSet.add(msg.idempotencyKey);
156
+ this.dedup.push(msg.idempotencyKey);
157
+ while (this.dedup.length > this.dedupMax) {
158
+ const evicted = this.dedup.shift();
159
+ if (evicted !== void 0) this.dedupSet.delete(evicted);
160
+ }
161
+ const sink = this.inboundSink;
162
+ if (!sink) {
163
+ this.log?.(
164
+ "debug",
165
+ `no inbound sink registered; dropping ${msg.idempotencyKey}`
166
+ );
167
+ return;
168
+ }
169
+ try {
170
+ sink(msg, { attachmentId: this.entries.get(channelId)?.attachmentId });
171
+ } catch (err) {
172
+ this.log?.(
173
+ "warn",
174
+ `inbound sink threw: ${err instanceof Error ? err.message : String(err)}`
175
+ );
176
+ }
177
+ }
178
+ };
179
+
27
180
  // src/infra/config/channels.ts
28
181
  import fs from "fs";
29
182
  import os from "os";
@@ -1886,158 +2039,144 @@ function instantiateAdapter(sidecar) {
1886
2039
  return { ok: true, adapter: module.create(parsed.config, sidecar.instanceId) };
1887
2040
  }
1888
2041
 
1889
- // src/gateway/channelManager.ts
1890
- var DEFAULT_DEDUP_WINDOW = 1024;
1891
- var DuplicateChannelError = class extends Error {
1892
- constructor(id) {
1893
- super(`channel ${id} already registered`);
1894
- this.name = "DuplicateChannelError";
1895
- }
1896
- };
1897
- var UnknownChannelError = class extends Error {
1898
- constructor(id) {
1899
- super(`channel ${id} not registered`);
1900
- this.name = "UnknownChannelError";
1901
- }
1902
- };
1903
- var ChannelManager = class {
1904
- entries = /* @__PURE__ */ new Map();
1905
- dedup = [];
1906
- dedupSet = /* @__PURE__ */ new Set();
1907
- dedupMax;
1908
- log;
1909
- inboundSink = null;
1910
- healthSink = null;
1911
- stopped = false;
1912
- constructor(opts = {}) {
1913
- this.dedupMax = opts.dedupWindow ?? DEFAULT_DEDUP_WINDOW;
1914
- this.log = opts.log;
1915
- }
1916
- /** Register the single inbound dispatch target. M5 wires the router here. */
1917
- setInboundSink(sink) {
1918
- this.inboundSink = sink;
1919
- }
1920
- /**
1921
- * Returns the attachmentId associated with `channelId` at registration
1922
- * time, or undefined if the channel is unknown or registered without one.
1923
- */
1924
- getAttachmentId(channelId) {
1925
- return this.entries.get(channelId)?.attachmentId;
1926
- }
1927
- setHealthSink(sink) {
1928
- this.healthSink = sink;
1929
- }
1930
- listChannels() {
1931
- return [...this.entries.values()].map((e) => ({
1932
- id: e.adapter.id,
1933
- health: e.lastHealth
1934
- }));
1935
- }
1936
- /** Snapshot of currently registered adapters; used by the relay coordinator. */
1937
- listAdapters() {
1938
- return [...this.entries.values()].map((e) => e.adapter);
1939
- }
1940
- async register(adapter, opts = {}) {
1941
- if (this.stopped) {
1942
- throw new Error("channel manager already stopped");
1943
- }
1944
- if (this.entries.has(adapter.id)) {
1945
- throw new DuplicateChannelError(adapter.id);
1946
- }
1947
- const abort = new AbortController();
1948
- const inboundListener = (msg) => this.handleInbound(adapter.id, msg);
1949
- const healthListener = (sample) => {
1950
- const entry2 = this.entries.get(adapter.id);
1951
- if (entry2) entry2.lastHealth = sample;
1952
- this.healthSink?.(sample);
1953
- };
1954
- const startPromise = adapter.start({
1955
- log: (level, msg) => this.log?.(level, `[${adapter.id}] ${msg}`),
1956
- signal: abort.signal,
1957
- emitInbound: inboundListener,
1958
- emitHealth: healthListener
1959
- });
1960
- const entry = {
1961
- adapter,
1962
- abort,
1963
- startPromise
1964
- };
1965
- if (opts.attachmentId !== void 0) entry.attachmentId = opts.attachmentId;
1966
- this.entries.set(adapter.id, entry);
1967
- try {
1968
- await startPromise;
1969
- } catch (err) {
1970
- this.entries.delete(adapter.id);
1971
- throw err;
1972
- }
1973
- }
1974
- async unregister(id, reason) {
1975
- const entry = this.entries.get(id);
1976
- if (!entry) {
1977
- throw new UnknownChannelError(id);
1978
- }
1979
- this.entries.delete(id);
1980
- entry.abort.abort();
1981
- await entry.adapter.stop(reason);
1982
- }
1983
- async send(channelId, msg) {
1984
- const entry = this.entries.get(channelId);
1985
- if (!entry) {
1986
- throw new UnknownChannelError(channelId);
2042
+ // src/gateway/channelSidecarReconciler.ts
2043
+ var ChannelSidecarReconciler = class {
2044
+ channelManager;
2045
+ home;
2046
+ loadSidecars;
2047
+ instantiateAdapter;
2048
+ stdout;
2049
+ stderr;
2050
+ constructor(opts) {
2051
+ this.channelManager = opts.channelManager;
2052
+ this.home = opts.home;
2053
+ this.loadSidecars = opts.loadSidecars ?? loadChannelSidecars;
2054
+ this.instantiateAdapter = opts.instantiateAdapter ?? instantiateAdapter;
2055
+ this.stdout = opts.stdout ?? ((message) => process.stdout.write(message));
2056
+ this.stderr = opts.stderr ?? ((message) => process.stderr.write(message));
2057
+ }
2058
+ async reconcile(opts) {
2059
+ const results = [];
2060
+ const { sidecars, errors } = this.loadSidecars(this.home);
2061
+ for (const err of errors) {
2062
+ const id = pathIdFromSidecarPath(err.path);
2063
+ results.push({
2064
+ id,
2065
+ ok: false,
2066
+ action: "failed",
2067
+ reason: err.reason
2068
+ });
2069
+ if (opts.logFailures) {
2070
+ this.stderr(`athena-gateway: skipping ${err.path}: ${err.reason}
2071
+ `);
2072
+ }
1987
2073
  }
1988
- return entry.adapter.send(msg);
1989
- }
1990
- async probe(channelId) {
1991
- const entry = this.entries.get(channelId);
1992
- if (!entry) {
1993
- throw new UnknownChannelError(channelId);
2074
+ if (opts.unregisterStale) {
2075
+ const sidecarIds = new Set(sidecars.map((sidecar) => sidecar.instanceId));
2076
+ for (const channel of this.channelManager.listChannels()) {
2077
+ if (sidecarIds.has(channel.id)) continue;
2078
+ try {
2079
+ await this.channelManager.unregister(channel.id, "shutdown");
2080
+ results.push({
2081
+ id: channel.id,
2082
+ ok: true,
2083
+ action: "unregistered"
2084
+ });
2085
+ } catch (err) {
2086
+ const reason = errorReason(err);
2087
+ results.push({
2088
+ id: channel.id,
2089
+ ok: false,
2090
+ action: "failed",
2091
+ reason
2092
+ });
2093
+ if (opts.logFailures) {
2094
+ this.stderr(
2095
+ `athena-gateway: unregister ${channel.id} failed: ${reason}
2096
+ `
2097
+ );
2098
+ }
2099
+ }
2100
+ }
1994
2101
  }
1995
- return entry.adapter.probe();
1996
- }
1997
- async stop(reason = "shutdown") {
1998
- if (this.stopped) return;
1999
- this.stopped = true;
2000
- const ids = [...this.entries.keys()];
2001
- for (const id of ids.reverse()) {
2102
+ for (const sidecar of sidecars) {
2103
+ const existed = this.channelManager.listChannels().some((channel) => channel.id === sidecar.instanceId);
2104
+ if (existed) {
2105
+ try {
2106
+ await this.channelManager.unregister(sidecar.instanceId, "shutdown");
2107
+ } catch (err) {
2108
+ const reason = errorReason(err);
2109
+ results.push({
2110
+ id: sidecar.instanceId,
2111
+ ok: false,
2112
+ action: "failed",
2113
+ reason
2114
+ });
2115
+ if (opts.logFailures) {
2116
+ this.stderr(
2117
+ `athena-gateway: unregister ${sidecar.instanceId} failed: ${reason}
2118
+ `
2119
+ );
2120
+ }
2121
+ continue;
2122
+ }
2123
+ }
2124
+ const built = this.instantiateAdapter(sidecar);
2125
+ if (!built.ok) {
2126
+ results.push({
2127
+ id: sidecar.instanceId,
2128
+ ok: false,
2129
+ action: "failed",
2130
+ reason: built.reason
2131
+ });
2132
+ if (opts.logFailures) {
2133
+ this.stderr(
2134
+ `athena-gateway: ${sidecar.instanceId}: ${built.reason}
2135
+ `
2136
+ );
2137
+ }
2138
+ continue;
2139
+ }
2002
2140
  try {
2003
- await this.unregister(id, reason);
2004
- } catch (err) {
2005
- this.log?.(
2006
- "warn",
2007
- `channel ${id} stop failed: ${err instanceof Error ? err.message : String(err)}`
2141
+ await this.channelManager.register(
2142
+ built.adapter,
2143
+ sidecar.attachmentId !== void 0 ? { attachmentId: sidecar.attachmentId } : {}
2008
2144
  );
2145
+ results.push({
2146
+ id: sidecar.instanceId,
2147
+ ok: true,
2148
+ action: existed ? "replaced" : "registered"
2149
+ });
2150
+ if (opts.logRegistrations) {
2151
+ this.stdout(`athena-gateway: registered ${sidecar.instanceId}
2152
+ `);
2153
+ }
2154
+ } catch (err) {
2155
+ const reason = errorReason(err);
2156
+ results.push({
2157
+ id: sidecar.instanceId,
2158
+ ok: false,
2159
+ action: "failed",
2160
+ reason
2161
+ });
2162
+ if (opts.logFailures) {
2163
+ this.stderr(
2164
+ `athena-gateway: register ${sidecar.instanceId} failed: ${reason}
2165
+ `
2166
+ );
2167
+ }
2009
2168
  }
2010
2169
  }
2011
- }
2012
- handleInbound(channelId, msg) {
2013
- if (this.dedupSet.has(msg.idempotencyKey)) {
2014
- this.log?.("debug", `dropping duplicate inbound ${msg.idempotencyKey}`);
2015
- return;
2016
- }
2017
- this.dedupSet.add(msg.idempotencyKey);
2018
- this.dedup.push(msg.idempotencyKey);
2019
- while (this.dedup.length > this.dedupMax) {
2020
- const evicted = this.dedup.shift();
2021
- if (evicted !== void 0) this.dedupSet.delete(evicted);
2022
- }
2023
- const sink = this.inboundSink;
2024
- if (!sink) {
2025
- this.log?.(
2026
- "debug",
2027
- `no inbound sink registered; dropping ${msg.idempotencyKey}`
2028
- );
2029
- return;
2030
- }
2031
- try {
2032
- sink(msg, { attachmentId: this.entries.get(channelId)?.attachmentId });
2033
- } catch (err) {
2034
- this.log?.(
2035
- "warn",
2036
- `inbound sink threw: ${err instanceof Error ? err.message : String(err)}`
2037
- );
2038
- }
2170
+ return { results };
2039
2171
  }
2040
2172
  };
2173
+ function pathIdFromSidecarPath(filePath) {
2174
+ const base = filePath.split(/[\\/]/).pop() ?? filePath;
2175
+ return base.endsWith(".json") ? base.slice(0, -".json".length) : base;
2176
+ }
2177
+ function errorReason(err) {
2178
+ return err instanceof Error ? err.message : String(err);
2179
+ }
2041
2180
 
2042
2181
  // src/gateway/control/handlers.ts
2043
2182
  import { createRequire } from "module";
@@ -2241,7 +2380,7 @@ var cachedVersion = null;
2241
2380
  function readVersion() {
2242
2381
  if (cachedVersion !== null) return cachedVersion;
2243
2382
  try {
2244
- const injected = "0.5.8";
2383
+ const injected = "0.5.11";
2245
2384
  if (typeof injected === "string" && injected.length > 0) {
2246
2385
  cachedVersion = injected;
2247
2386
  return cachedVersion;
@@ -3848,10 +3987,6 @@ function buildListenerStatus(spec, resolvedPort) {
3848
3987
  loopback: isLoopbackHost(spec.host)
3849
3988
  };
3850
3989
  }
3851
- function pathIdFromSidecarPath(filePath) {
3852
- const base = filePath.split(/[\\/]/).pop() ?? filePath;
3853
- return base.endsWith(".json") ? base.slice(0, -".json".length) : base;
3854
- }
3855
3990
  async function startDaemon(opts) {
3856
3991
  const startedAt = Date.now();
3857
3992
  const pid = process.pid;
@@ -3905,125 +4040,20 @@ async function startDaemon(opts) {
3905
4040
  channelManager.setInboundSink((inbound, ctx) => {
3906
4041
  pipeline.handleInbound(inbound, ctx);
3907
4042
  });
3908
- const channelConfigHome = opts.env?.HOME;
3909
- const reloadChannels = async () => {
3910
- const results = [];
3911
- const { sidecars, errors } = loadChannelSidecars(channelConfigHome);
3912
- for (const err of errors) {
3913
- const id = pathIdFromSidecarPath(err.path);
3914
- results.push({
3915
- id,
3916
- ok: false,
3917
- action: "failed",
3918
- reason: err.reason
3919
- });
3920
- }
3921
- const sidecarIds = new Set(sidecars.map((s) => s.instanceId));
3922
- for (const channel of channelManager.listChannels()) {
3923
- if (sidecarIds.has(channel.id)) continue;
3924
- try {
3925
- await channelManager.unregister(channel.id, "shutdown");
3926
- results.push({
3927
- id: channel.id,
3928
- ok: true,
3929
- action: "unregistered"
3930
- });
3931
- } catch (err) {
3932
- results.push({
3933
- id: channel.id,
3934
- ok: false,
3935
- action: "failed",
3936
- reason: err instanceof Error ? err.message : String(err)
3937
- });
3938
- }
3939
- }
3940
- for (const sidecar of sidecars) {
3941
- const existed = channelManager.listChannels().some((channel) => channel.id === sidecar.instanceId);
3942
- if (existed) {
3943
- try {
3944
- await channelManager.unregister(sidecar.instanceId, "shutdown");
3945
- } catch (err) {
3946
- results.push({
3947
- id: sidecar.instanceId,
3948
- ok: false,
3949
- action: "failed",
3950
- reason: err instanceof Error ? err.message : String(err)
3951
- });
3952
- continue;
3953
- }
3954
- }
3955
- const built = instantiateAdapter(sidecar);
3956
- if (!built.ok) {
3957
- results.push({
3958
- id: sidecar.instanceId,
3959
- ok: false,
3960
- action: "failed",
3961
- reason: built.reason
3962
- });
3963
- continue;
3964
- }
3965
- try {
3966
- await channelManager.register(
3967
- built.adapter,
3968
- sidecar.attachmentId !== void 0 ? { attachmentId: sidecar.attachmentId } : {}
3969
- );
3970
- results.push({
3971
- id: sidecar.instanceId,
3972
- ok: true,
3973
- action: existed ? "replaced" : "registered"
3974
- });
3975
- if (!opts.silent) {
3976
- process.stdout.write(
3977
- `athena-gateway: registered ${sidecar.instanceId}
3978
- `
3979
- );
3980
- }
3981
- } catch (err) {
3982
- results.push({
3983
- id: sidecar.instanceId,
3984
- ok: false,
3985
- action: "failed",
3986
- reason: err instanceof Error ? err.message : String(err)
3987
- });
3988
- }
3989
- }
3990
- return { results };
3991
- };
4043
+ const channelSidecarReconciler = new ChannelSidecarReconciler({
4044
+ channelManager,
4045
+ home: opts.env?.HOME
4046
+ });
4047
+ const reloadChannels = async () => channelSidecarReconciler.reconcile({
4048
+ unregisterStale: true,
4049
+ logRegistrations: !opts.silent
4050
+ });
3992
4051
  if (!opts.skipChannelLoad) {
3993
- const { sidecars, errors } = loadChannelSidecars(channelConfigHome);
3994
- for (const err of errors) {
3995
- process.stderr.write(
3996
- `athena-gateway: skipping ${err.path}: ${err.reason}
3997
- `
3998
- );
3999
- }
4000
- for (const sidecar of sidecars) {
4001
- const built = instantiateAdapter(sidecar);
4002
- if (!built.ok) {
4003
- process.stderr.write(
4004
- `athena-gateway: ${sidecar.instanceId}: ${built.reason}
4005
- `
4006
- );
4007
- continue;
4008
- }
4009
- try {
4010
- await channelManager.register(
4011
- built.adapter,
4012
- sidecar.attachmentId !== void 0 ? { attachmentId: sidecar.attachmentId } : {}
4013
- );
4014
- if (!opts.silent) {
4015
- process.stdout.write(
4016
- `athena-gateway: registered ${sidecar.instanceId}
4017
- `
4018
- );
4019
- }
4020
- } catch (err) {
4021
- process.stderr.write(
4022
- `athena-gateway: register ${sidecar.instanceId} failed: ${err instanceof Error ? err.message : String(err)}
4023
- `
4024
- );
4025
- }
4026
- }
4052
+ await channelSidecarReconciler.reconcile({
4053
+ unregisterStale: false,
4054
+ logFailures: true,
4055
+ logRegistrations: !opts.silent
4056
+ });
4027
4057
  }
4028
4058
  const handler = createDispatcher({
4029
4059
  startedAt,