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.
- package/.env.example +5 -0
- package/README.md +29 -10
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dashboard/mount.ts +118 -1
- package/src/dashboard/repo-link.ts +126 -0
- package/src/dashboard/static/components.css +15 -3
- package/src/dashboard/static/dashboard.css +172 -37
- package/src/dashboard/static/dashboard.js +141 -0
- package/src/dashboard/static/fonts/onest-latin.woff2 +0 -0
- package/src/dashboard/views.tsx +503 -142
- package/src/dashboard/word-diff.ts +89 -0
- package/src/db/human-save.ts +112 -0
- package/src/db/migrations.ts +38 -0
- package/src/db/queries.ts +35 -12
- package/src/db/sweep.ts +2 -2
- package/src/mcp/file-entries.ts +53 -1
- package/src/mcp/list-entries.ts +41 -3
- package/src/mcp/mark-applied.ts +1 -1
- package/src/mcp/route.ts +6 -9
- package/src/mcp/server.ts +1 -1
- package/src/shim/daemon.ts +23 -16
- package/src/shim/main.ts +26 -5
package/src/shim/daemon.ts
CHANGED
|
@@ -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
|
|
212
|
-
if (
|
|
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
|
|
238
|
-
if (
|
|
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
|
|
293
|
-
if (
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `Mcp-Name` from `params.name`/`params.uri` on the named calls
|
|
13
|
-
* server-side, where the
|
|
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(
|