reviewzy 0.1.0 → 0.2.0

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.
@@ -184,6 +184,8 @@ export type EnsureDaemonOptions = {
184
184
  port: number;
185
185
  /** The shim's own package version, compared semantically against the daemon's `/health` version. */
186
186
  shimVersion: string;
187
+ /** Dev mode (`REVIEWZY_DEV=1`): drain and respawn even when the daemon's version is not older. */
188
+ dev?: boolean | undefined;
187
189
  /** The bearer token for `/drain` and `/mcp` when `REVIEWZY_TOKEN` is set. */
188
190
  token?: string | undefined;
189
191
  /** The environment the spawned daemon inherits. */
@@ -192,11 +194,22 @@ export type EnsureDaemonOptions = {
192
194
  spawnDaemon?: ((env: Record<string, string | undefined>, lockPath: string) => number) | undefined;
193
195
  };
194
196
 
197
+ /**
198
+ * Why the shim must drain the running daemon, or null when it may be adopted: the shim's version
199
+ * outranks the daemon's (the production update handoff), or dev mode forces a reload so checkout
200
+ * `src/` edits reach the daemon without a version bump.
201
+ */
202
+ function drainReason(shimVersion: string, daemonVersion: string, dev: boolean): string | null {
203
+ if (dev) return "dev mode reloads checkout source";
204
+ const rank = compareSemver(shimVersion, daemonVersion);
205
+ return rank !== null && rank > 0 ? `shim ${shimVersion} outranks daemon ${daemonVersion}` : null;
206
+ }
207
+
195
208
  /**
196
209
  * Returns a live daemon handle, spawning or handshaking a handoff as needed:
197
210
  *
198
211
  * 1. A live, reviewzy-owned daemon named by a valid lockfile is adopted (and drained first, when
199
- * the shim's version semantically outranks the daemon's).
212
+ * the shim's version semantically outranks the daemon's, or when dev mode is set).
200
213
  * 2. Otherwise, under an exclusive claim, the shim re-checks for a live daemon (another shim may
201
214
  * have finished while this one waited), spawns the daemon detached, waits for `/health`, and
202
215
  * writes the lockfile from the health body — so the recorded pid is always the process that is
@@ -208,11 +221,9 @@ export async function ensureDaemon(options: EnsureDaemonOptions): Promise<Daemon
208
221
  for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
209
222
  const live = await liveDaemon(options.lockfile);
210
223
  if (live !== null) {
211
- const rank = compareSemver(options.shimVersion, live.health.version);
212
- if (rank !== null && rank > 0) {
213
- console.error(
214
- `reviewzy: shim ${options.shimVersion} outranks daemon ${live.health.version}; draining pid ${live.health.pid}`,
215
- );
224
+ const reason = drainReason(options.shimVersion, live.health.version, options.dev === true);
225
+ if (reason !== null) {
226
+ console.error(`reviewzy: ${reason}; draining pid ${live.health.pid}`);
216
227
  await drainAndAwaitExit(live.lock, options.token);
217
228
  // Falls through to the spawn phase; the drained daemon is gone and cannot be adopted.
218
229
  } else {
@@ -234,11 +245,9 @@ export async function ensureDaemon(options: EnsureDaemonOptions): Promise<Daemon
234
245
  // The same version rung the adopt path applies: a raced daemon was fully booted before the
235
246
  // winning shim released the claim (release follows the health wait), so draining it here
236
247
  // cannot interrupt a boot.
237
- const rank = compareSemver(options.shimVersion, raced.health.version);
238
- if (rank !== null && rank > 0) {
239
- console.error(
240
- `reviewzy: shim ${options.shimVersion} outranks raced daemon ${raced.health.version}; draining pid ${raced.health.pid}`,
241
- );
248
+ const reason = drainReason(options.shimVersion, raced.health.version, options.dev === true);
249
+ if (reason !== null) {
250
+ console.error(`reviewzy: ${reason}; draining pid ${raced.health.pid}`);
242
251
  await drainAndAwaitExit(raced.lock, options.token);
243
252
  // Falls through to the spawn phase; the claim is already ours.
244
253
  } else {
@@ -289,11 +298,9 @@ export async function ensureDaemon(options: EnsureDaemonOptions): Promise<Daemon
289
298
  version: incumbent.version,
290
299
  nonce: incumbent.nonce,
291
300
  };
292
- const rank = compareSemver(options.shimVersion, incumbent.version);
293
- if (rank !== null && rank > 0) {
294
- console.error(
295
- `reviewzy: shim ${options.shimVersion} outranks incumbent daemon ${incumbent.version}; draining pid ${incumbent.pid}`,
296
- );
301
+ const reason = drainReason(options.shimVersion, incumbent.version, options.dev === true);
302
+ if (reason !== null) {
303
+ console.error(`reviewzy: ${reason}; draining pid ${incumbent.pid}`);
297
304
  await drainAndAwaitExit(incumbentLock, options.token);
298
305
  return spawnAndVerify();
299
306
  }
package/src/shim/main.ts CHANGED
@@ -6,11 +6,12 @@ import { VERSION } from "../version.ts";
6
6
 
7
7
  /**
8
8
  * The shim owns zero protocol logic: one stdin JSON-RPC line in, one POST to the daemon's `/mcp`,
9
- * whatever the daemon answered back out on stdout. The only interpretation it does is mechanical
10
- * transport mapping, because the Streamable HTTP transport demands headers a stdio frame does not
11
- * carry: `Mcp-Method` from the frame's method, `MCP-Protocol-Version` from its `_meta` revision,
12
- * `Mcp-Name` from `params.name`/`params.uri` on the named calls. Validation of all of it stays
13
- * server-side, where the security control lives.
9
+ * the daemon's answer back out on stdout. The only interpretation it does is mechanical transport
10
+ * mapping, because the Streamable HTTP transport demands headers a stdio frame does not carry:
11
+ * `Mcp-Method` from the frame's method, `MCP-Protocol-Version` from its `_meta` revision,
12
+ * `Mcp-Name` from `params.name`/`params.uri` on the named calls, and an SSE answer from the legacy
13
+ * leg unwrapped into its `data:` frames. Validation of all of it stays server-side, where the
14
+ * security control lives.
14
15
  */
15
16
  type LooseFrame = {
16
17
  method?: unknown;
@@ -104,6 +105,25 @@ export async function forwardLine(line: string, port: number, token?: string): P
104
105
  return transportError(frame, `daemon replied http ${response.status} with an empty body`);
105
106
  }
106
107
 
108
+ // The legacy leg answers every request as an SSE body: a stateless server has no session to
109
+ // hang a follow-up GET stream on, so the transport frames the reply inline. stdout carries
110
+ // JSON-RPC frames only, so each event's `data:` payload is relayed as its own line; events
111
+ // without data (comments, keep-alives) carry no frame and are skipped.
112
+ if ((response.headers.get("content-type") ?? "").startsWith("text/event-stream")) {
113
+ const frames: string[] = [];
114
+ for (const event of text.split(/\r?\n\r?\n/)) {
115
+ const data = event
116
+ .split(/\r?\n/)
117
+ .filter((line) => line.startsWith("data:"))
118
+ .map((line) => line.slice("data:".length).replace(/^ /, ""));
119
+ if (data.length > 0) frames.push(data.join("\n"));
120
+ }
121
+ if (frames.length === 0) {
122
+ return transportError(frame, `daemon replied http ${response.status} with an sse body carrying no data frame`);
123
+ }
124
+ return frames.join("\n");
125
+ }
126
+
107
127
  let parsed: unknown;
108
128
  try {
109
129
  parsed = JSON.parse(text);
@@ -135,6 +155,7 @@ export async function runShim(): Promise<void> {
135
155
  port: config.REVIEWZY_PORT,
136
156
  shimVersion: VERSION,
137
157
  token: config.REVIEWZY_TOKEN,
158
+ dev: config.REVIEWZY_DEV === "1",
138
159
  env: process.env,
139
160
  });
140
161
  console.error(