gitwarren 0.1.8-beta.1 → 0.1.9

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
@@ -8,33 +8,58 @@ This package is the command line. There is also a desktop app; see
8
8
  [gitwarren-app](https://github.com/klarluft/gitwarren-app).
9
9
 
10
10
  ```
11
- npx gitwarren serve
11
+ npx gitwarren serve --open
12
12
  ```
13
13
 
14
- That serves the review UI on `127.0.0.1:41427` and prints a URL carrying a
15
- token minted for that launch. Open it, add a repository, pick two refs, and
16
- review the diff.
14
+ That serves the review UI on `127.0.0.1:41427`, prints a URL carrying a token
15
+ minted for that launch, and opens it. Add a repository, pick two refs, and
16
+ review the diff. Ctrl-C stops it. Needs Node 22 or newer; on a machine without
17
+ Node, the Homebrew formula and the install script in the
18
+ [app repository](https://github.com/klarluft/gitwarren-app#the-gitwarren-command-line)
19
+ bring their own.
17
20
 
18
21
  ## Commands
19
22
 
23
+ **Run it now**
24
+
25
+ | | |
26
+ | --- | --- |
27
+ | `gitwarren serve [--open]` | run GitWarren in this terminal and print its URL; `--open` opens it too |
28
+ | `gitwarren open [link]` | open the running GitWarren in your browser, carrying the token |
29
+
30
+ **Keep it running**
31
+
20
32
  | | |
21
33
  | --- | --- |
22
- | `gitwarren serve` | serve the web view on loopback and print its URL |
23
- | `gitwarren serve --stdio` | answer GitWarren's protocol on stdin/stdout |
24
- | `gitwarren open [link]` | open this machine's GitWarren in a browser, carrying the token |
25
- | `gitwarren service install` | write the agent launcher, and start at login |
26
- | `gitwarren service uninstall` | remove the login item |
27
- | `gitwarren service status` | what is registered, and what is running |
34
+ | `gitwarren service install` | run GitWarren in the background, from now and at every login |
35
+ | `gitwarren service uninstall` | stop that, and remove the login item |
36
+ | `gitwarren service status` | what is running, and where the data is |
37
+
38
+ **Let a coding agent in**
39
+
40
+ | | |
41
+ | --- | --- |
42
+ | `gitwarren agent-setup` | print the one sentence to give an agent so it can reach GitWarren over MCP |
43
+
44
+ `gitwarren serve --stdio` answers GitWarren's protocol on stdin and stdout; it
45
+ is what a GitWarren on another machine spawns, and nothing a person needs to
46
+ type.
28
47
 
29
48
  `gitwarren open` takes a `gitwarren://` deep link or the `http://127.0.0.1`
30
49
  URL an agent hands out, and lands on that review rather than the home screen.
31
50
 
32
51
  ## Letting an agent in
33
52
 
34
- `gitwarren service install` writes `~/.gitwarren/bin/gitwarren-mcp`, which is a
35
- GitWarren MCP server your coding agent can start. Point the agent at that one
36
- path — it is the same on every machine, and GitWarren keeps it pointing at
37
- whichever install ran last.
53
+ Every install carries GitWarren's MCP server. What an agent needs is one
54
+ stable command to start it with, and that is `~/.gitwarren/bin/gitwarren-mcp`
55
+ the same path on every machine, written by `gitwarren serve`, `gitwarren
56
+ agent-setup` and `gitwarren service install` alike, and kept pointing at
57
+ whichever install ran last. `gitwarren agent-setup` prints the sentence to
58
+ paste into the agent.
59
+
60
+ The MCP server reads the same SQLite file the browser view does, so an agent
61
+ can open and comment on reviews whether or not GitWarren is being served. What
62
+ serving adds is that the links an agent hands you have something to open.
38
63
 
39
64
  ## Where your data is
40
65
 
package/lib/gitwarren.cjs CHANGED
@@ -10242,7 +10242,7 @@ function cmdQuote(value) {
10242
10242
  if (value.includes('"')) throw new Error(`a Windows path cannot contain a quote: ${value}`);
10243
10243
  return `"${value}"`;
10244
10244
  }
10245
- const APP_VERSION = "0.1.8-beta.1";
10245
+ const APP_VERSION = "0.1.9";
10246
10246
  const REMOTE_ROOT = "$HOME/.gitwarren";
10247
10247
  const runOnHost = (route2, options) => {
10248
10248
  switch (route2.kind) {
@@ -16691,7 +16691,7 @@ function describeMcpLaunch() {
16691
16691
  available,
16692
16692
  stable: true,
16693
16693
  direct: { command: launcher, args: [], env: {} },
16694
- note: available ? void 0 : "No MCP launcher on this machine yet. `gitwarren service install` writes it."
16694
+ note: available ? void 0 : "No MCP launcher on this machine yet. `gitwarren agent-setup` writes it, and so does `gitwarren serve`."
16695
16695
  };
16696
16696
  }
16697
16697
  const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
@@ -26868,7 +26868,7 @@ Connection: close\r
26868
26868
  }
26869
26869
  };
26870
26870
  }
26871
- const VERSION$1 = "0.1.8-beta.1";
26871
+ const VERSION$1 = "0.1.9";
26872
26872
  function isWebBuild(directory) {
26873
26873
  return fs.existsSync(node_path.join(directory, "index.html")) && !fs.existsSync(node_path.join(directory, "main.ts"));
26874
26874
  }
@@ -26914,11 +26914,12 @@ function whereTheOwnerIs(owner) {
26914
26914
  ${url}
26915
26915
  `;
26916
26916
  }
26917
- function runListen() {
26917
+ function runListen(hooks = {}) {
26918
26918
  const owner = readLiveDaemonRuntime();
26919
26919
  if (owner) {
26920
+ const who = owner.owner === "gui" ? "the desktop app" : "another `gitwarren serve`, in a terminal or as the background service (which `gitwarren service uninstall` stops)";
26920
26921
  console.error(
26921
- `[gitwarren-serve] this machine's GitWarren is already being served by ${owner.owner === "gui" ? "the app" : "another gitwarren serve"} (pid ${owner.pid}). A data directory has one owner. Quit that first, or use the one that is running` + whereTheOwnerIs(owner)
26922
+ `[gitwarren] GitWarren is already running on this machine (pid ${owner.pid}), as ${who}. Only one can serve a data directory at a time. Quit that first, or use the one that is running` + whereTheOwnerIs(owner)
26922
26923
  );
26923
26924
  process.exitCode = 1;
26924
26925
  return false;
@@ -26926,7 +26927,7 @@ function runListen() {
26926
26927
  const webRoot = resolveWebRoot();
26927
26928
  if (!webRoot) {
26928
26929
  console.error(
26929
- "[gitwarren-serve] no web build found. Run `npm run build:web`, or set GITWARREN_WEB_ROOT to the directory holding its index.html."
26930
+ "[gitwarren] no web build found. Run `npm run build:web`, or set GITWARREN_WEB_ROOT to the directory holding its index.html."
26930
26931
  );
26931
26932
  process.exitCode = 1;
26932
26933
  return false;
@@ -26953,10 +26954,10 @@ function runListen() {
26953
26954
  server.on("error", (error2) => {
26954
26955
  if (error2.code === "EADDRINUSE") {
26955
26956
  console.error(
26956
- `[gitwarren-serve] port ${LINK_SERVER_PORT} is already in use. That port is the one every GitWarren link names, so serving on a different one would mean serving where nobody is looking. Free it and start again.`
26957
+ `[gitwarren] port ${LINK_SERVER_PORT} is already in use. That port is the one every GitWarren link names, so serving on a different one would mean serving where nobody is looking. Free it and start again.`
26957
26958
  );
26958
26959
  } else {
26959
- console.error("[gitwarren-serve] the server failed", error2);
26960
+ console.error("[gitwarren] the server failed", error2);
26960
26961
  }
26961
26962
  shutdownListen();
26962
26963
  process.exitCode = 1;
@@ -26969,13 +26970,16 @@ function runListen() {
26969
26970
  linkPort,
26970
26971
  owner: "daemon"
26971
26972
  });
26973
+ const url = `http://${LINK_SERVER_HOST}:${LINK_SERVER_PORT}/?${TOKEN_PARAM}=${token}`;
26972
26974
  console.error(
26973
- `[gitwarren-serve] GitWarren is at
26975
+ `[gitwarren] GitWarren is running at
26974
26976
 
26975
- http://${LINK_SERVER_HOST}:${LINK_SERVER_PORT}/?${TOKEN_PARAM}=${token}
26977
+ ${url}
26976
26978
 
26977
- The token is new each time this starts, and is exchanged for a session cookie the first time the URL is opened.`
26979
+ Open that URL, or run \`gitwarren open\` in another terminal. The token in it is minted for this launch and becomes a cookie the first time the page loads.
26980
+ Press Ctrl-C to stop. To keep GitWarren running in the background instead, from now and at every login, use \`gitwarren service install\`.`
26978
26981
  );
26982
+ hooks.onListening?.(url);
26979
26983
  });
26980
26984
  closeOnExit = () => {
26981
26985
  handler.close();
@@ -27002,10 +27006,11 @@ hand it is a way to see what the protocol says:
27002
27006
 
27003
27007
  --listen serves the web view on loopback and prints a URL carrying this
27004
27008
  launch's token. It claims this machine's data directory, so it refuses to
27005
- start while the app is running - see daemon/listen.ts.
27009
+ start while the app is running - see daemon/listen.ts. \`gitwarren serve\` with
27010
+ no flag means this one.
27006
27011
  `;
27007
- function runDaemon(argv) {
27008
- if (argv.includes("--listen")) return runListen();
27012
+ function runDaemon(argv, hooks = {}) {
27013
+ if (argv.includes("--listen")) return runListen(hooks);
27009
27014
  if (!argv.includes("--stdio")) {
27010
27015
  console.error(USAGE$4);
27011
27016
  return false;
@@ -27062,6 +27067,99 @@ command = ${tomlString(mcp.command)}
27062
27067
  }
27063
27068
  ];
27064
27069
  }
27070
+ const WEB_ROOT_ENV_VAR = "GITWARREN_WEB_ROOT";
27071
+ function relaunchEnv() {
27072
+ const env = { [MIGRATIONS_DIR_ENV_VAR]: resolveMigrationsFolder() };
27073
+ const webRoot = resolveWebRoot();
27074
+ if (webRoot) env[WEB_ROOT_ENV_VAR] = webRoot;
27075
+ const dataDir = process.env[DATA_DIR_ENV_VAR]?.trim();
27076
+ if (dataDir) env[DATA_DIR_ENV_VAR] = dataDir;
27077
+ return env;
27078
+ }
27079
+ function findMcpServer(script) {
27080
+ const candidates = [
27081
+ script && node_path.join(node_path.dirname(script), "server.cjs"),
27082
+ script && node_path.join(node_path.dirname(script), "..", "lib", "server.cjs"),
27083
+ node_path.join(process.cwd(), "out", "mcp", "server.cjs")
27084
+ ];
27085
+ for (const candidate of candidates) {
27086
+ if (candidate && fs.existsSync(candidate)) return fs.realpathSync(candidate);
27087
+ }
27088
+ return null;
27089
+ }
27090
+ function describeSelf() {
27091
+ const argv1 = process.argv[1];
27092
+ let script = null;
27093
+ if (argv1 !== void 0) {
27094
+ try {
27095
+ const resolved2 = fs.realpathSync(argv1);
27096
+ script = /\.[cm]?ts$/i.test(resolved2) ? null : resolved2;
27097
+ } catch {
27098
+ script = null;
27099
+ }
27100
+ }
27101
+ return { node: fs.realpathSync(process.execPath), script, mcpServer: findMcpServer(script), env: relaunchEnv() };
27102
+ }
27103
+ const BANNER = (what) => [
27104
+ `GitWarren ${what}. Written by the gitwarren command line, so a config or a`,
27105
+ "login item that names this path keeps working when the install moves.",
27106
+ "Generated file - rerun `gitwarren service install` to refresh it, do not edit this."
27107
+ ];
27108
+ function bannerFor(what, comment, eol) {
27109
+ return BANNER(what).map((line) => `${comment} ${line}${eol}`).join("");
27110
+ }
27111
+ function posixLauncher(what, self, script) {
27112
+ const exports2 = Object.entries(self.env).map(([name, value]) => `export ${name}=${shellQuote(value)}
27113
+ `).join("");
27114
+ return "#!/bin/sh\n" + bannerFor(what, "#", "\n") + exports2 + `exec ${shellQuote(self.node)} ${shellQuote(script)} "$@"
27115
+ `;
27116
+ }
27117
+ function windowsLauncher(what, self, script) {
27118
+ const sets = Object.entries(self.env).map(([name, value]) => `set "${name}=${value}"\r
27119
+ `).join("");
27120
+ return "@echo off\r\n" + bannerFor(what, "REM", "\r\n") + "setlocal\r\n" + sets + `${cmdQuote(self.node)} ${cmdQuote(script)} %*\r
27121
+ exit /b %ERRORLEVEL%\r
27122
+ `;
27123
+ }
27124
+ function write(path, contents) {
27125
+ let current2 = null;
27126
+ try {
27127
+ current2 = fs.readFileSync(path, "utf8");
27128
+ } catch {
27129
+ }
27130
+ if (current2 !== contents) {
27131
+ fs.mkdirSync(getLauncherDirectory(), { recursive: true });
27132
+ fs.writeFileSync(path, contents, "utf8");
27133
+ }
27134
+ if (process.platform !== "win32") fs.chmodSync(path, 493);
27135
+ return current2 === null;
27136
+ }
27137
+ function writeLaunchers(self) {
27138
+ if (self.script === null) {
27139
+ throw new Error(
27140
+ "this gitwarren is running from a source checkout, so there is no single file a login item could name. Build it first (`npm run build:daemon`), or install GitWarren from a tarball, Homebrew or npm."
27141
+ );
27142
+ }
27143
+ const posix = process.platform !== "win32";
27144
+ const created = [];
27145
+ const cli = getCliLauncherPath();
27146
+ if (write(cli, (posix ? posixLauncher : windowsLauncher)("CLI", self, self.script))) created.push(cli);
27147
+ let mcp = null;
27148
+ if (self.mcpServer !== null) {
27149
+ mcp = getMcpLauncherPath();
27150
+ if (write(mcp, (posix ? posixLauncher : windowsLauncher)("MCP server", self, self.mcpServer)))
27151
+ created.push(mcp);
27152
+ }
27153
+ return { cli, mcp, created };
27154
+ }
27155
+ function ensureLaunchers() {
27156
+ try {
27157
+ const { created } = writeLaunchers(describeSelf());
27158
+ return { created, refused: null };
27159
+ } catch (error2) {
27160
+ return { created: [], refused: error2 instanceof Error ? error2.message : String(error2) };
27161
+ }
27162
+ }
27065
27163
  const USAGE$3 = `gitwarren agent-setup [--manual]
27066
27164
 
27067
27165
  Prints the sentence to paste into an agent so it can reach this machine's
@@ -27089,14 +27187,25 @@ function runAgentSetup(argv) {
27089
27187
  console.log(snippet.text.trimEnd());
27090
27188
  }
27091
27189
  }
27092
- if (!fs.existsSync(command)) {
27190
+ const launchers = ensureLaunchers();
27191
+ if (launchers.created.includes(command)) {
27192
+ console.error(`
27193
+ [gitwarren] wrote ${command}, which the sentence above names.`);
27194
+ } else if (!fs.existsSync(command)) {
27093
27195
  console.error(
27094
27196
  `
27095
- [gitwarren] no launcher at ${command} yet. \`gitwarren service install\` writes it.`
27197
+ [gitwarren] no launcher at ${command} yet, and this run could not write one` + (launchers.refused ? `: ${launchers.refused}` : ".")
27096
27198
  );
27097
27199
  }
27098
27200
  return true;
27099
27201
  }
27202
+ function openInBrowser(url, onError) {
27203
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
27204
+ const child = node_child_process.execFile(command, args, { windowsHide: true }, (error2) => {
27205
+ if (error2) onError(`could not ask ${command} to open a browser (${error2.message})`);
27206
+ });
27207
+ child.unref();
27208
+ }
27100
27209
  const REVIEW_TABS = ["conversation", "commits", "files", "browse"];
27101
27210
  const HOME = { name: "repositories" };
27102
27211
  const HOST_SEGMENT = "h";
@@ -27217,21 +27326,6 @@ token so the tab is authenticated without anything being copied.
27217
27326
  --print print the URL instead of opening it - for an ssh session, or for
27218
27327
  piping somewhere.
27219
27328
  `;
27220
- function openInBrowser(url) {
27221
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
27222
- const child = node_child_process.execFile(command, args, { windowsHide: true }, (error2) => {
27223
- if (error2) {
27224
- console.error(
27225
- `[gitwarren] could not ask ${command} to open a browser (${error2.message}). The URL is:
27226
-
27227
- ${url}
27228
- `
27229
- );
27230
- process.exitCode = 1;
27231
- }
27232
- });
27233
- child.unref();
27234
- }
27235
27329
  function runOpen(argv) {
27236
27330
  const print = argv.includes("--print");
27237
27331
  const rest = argv.filter((argument) => argument !== "--print");
@@ -27242,7 +27336,7 @@ function runOpen(argv) {
27242
27336
  const owner = readLiveDaemonRuntime();
27243
27337
  if (owner === null) {
27244
27338
  console.error(
27245
- "[gitwarren] nothing is serving this machine. Start it with `gitwarren serve`, or `gitwarren service install` to have it start at login."
27339
+ "[gitwarren] GitWarren is not running on this machine, so there is nothing to open. `gitwarren serve` runs it in a terminal; `gitwarren service install` keeps it running in the background."
27246
27340
  );
27247
27341
  process.exitCode = 1;
27248
27342
  return true;
@@ -27273,91 +27367,16 @@ ${USAGE$2}`);
27273
27367
  return true;
27274
27368
  }
27275
27369
  if (print) console.log(url);
27276
- else openInBrowser(url);
27370
+ else
27371
+ openInBrowser(url, (message) => {
27372
+ console.error(`[gitwarren] ${message}. The URL is:
27373
+
27374
+ ${url}
27375
+ `);
27376
+ process.exitCode = 1;
27377
+ });
27277
27378
  return true;
27278
27379
  }
27279
- const WEB_ROOT_ENV_VAR = "GITWARREN_WEB_ROOT";
27280
- function relaunchEnv() {
27281
- const env = { [MIGRATIONS_DIR_ENV_VAR]: resolveMigrationsFolder() };
27282
- const webRoot = resolveWebRoot();
27283
- if (webRoot) env[WEB_ROOT_ENV_VAR] = webRoot;
27284
- const dataDir = process.env[DATA_DIR_ENV_VAR]?.trim();
27285
- if (dataDir) env[DATA_DIR_ENV_VAR] = dataDir;
27286
- return env;
27287
- }
27288
- function findMcpServer(script) {
27289
- const candidates = [
27290
- script && node_path.join(node_path.dirname(script), "server.cjs"),
27291
- script && node_path.join(node_path.dirname(script), "..", "lib", "server.cjs"),
27292
- node_path.join(process.cwd(), "out", "mcp", "server.cjs")
27293
- ];
27294
- for (const candidate of candidates) {
27295
- if (candidate && fs.existsSync(candidate)) return fs.realpathSync(candidate);
27296
- }
27297
- return null;
27298
- }
27299
- function describeSelf() {
27300
- const argv1 = process.argv[1];
27301
- let script = null;
27302
- if (argv1 !== void 0) {
27303
- try {
27304
- const resolved2 = fs.realpathSync(argv1);
27305
- script = /\.[cm]?ts$/i.test(resolved2) ? null : resolved2;
27306
- } catch {
27307
- script = null;
27308
- }
27309
- }
27310
- return { node: fs.realpathSync(process.execPath), script, mcpServer: findMcpServer(script), env: relaunchEnv() };
27311
- }
27312
- const BANNER = (what) => [
27313
- `GitWarren ${what}. Written by \`gitwarren service install\`, so a config or a`,
27314
- "login item that names this path keeps working when the install moves.",
27315
- "Generated file - run the command again, do not edit this."
27316
- ];
27317
- function bannerFor(what, comment, eol) {
27318
- return BANNER(what).map((line) => `${comment} ${line}${eol}`).join("");
27319
- }
27320
- function posixLauncher(what, self, script) {
27321
- const exports2 = Object.entries(self.env).map(([name, value]) => `export ${name}=${shellQuote(value)}
27322
- `).join("");
27323
- return "#!/bin/sh\n" + bannerFor(what, "#", "\n") + exports2 + `exec ${shellQuote(self.node)} ${shellQuote(script)} "$@"
27324
- `;
27325
- }
27326
- function windowsLauncher(what, self, script) {
27327
- const sets = Object.entries(self.env).map(([name, value]) => `set "${name}=${value}"\r
27328
- `).join("");
27329
- return "@echo off\r\n" + bannerFor(what, "REM", "\r\n") + "setlocal\r\n" + sets + `${cmdQuote(self.node)} ${cmdQuote(script)} %*\r
27330
- exit /b %ERRORLEVEL%\r
27331
- `;
27332
- }
27333
- function write(path, contents) {
27334
- let current2 = null;
27335
- try {
27336
- current2 = fs.readFileSync(path, "utf8");
27337
- } catch {
27338
- }
27339
- if (current2 !== contents) {
27340
- fs.mkdirSync(getLauncherDirectory(), { recursive: true });
27341
- fs.writeFileSync(path, contents, "utf8");
27342
- }
27343
- if (process.platform !== "win32") fs.chmodSync(path, 493);
27344
- }
27345
- function writeLaunchers(self) {
27346
- if (self.script === null) {
27347
- throw new Error(
27348
- "this gitwarren is running from a source checkout, so there is no single file a login item could name. Build it first (`npm run build:daemon`), or install GitWarren from a tarball, Homebrew or npm."
27349
- );
27350
- }
27351
- const posix = process.platform !== "win32";
27352
- const cli = getCliLauncherPath();
27353
- write(cli, (posix ? posixLauncher : windowsLauncher)("CLI", self, self.script));
27354
- let mcp = null;
27355
- if (self.mcpServer !== null) {
27356
- mcp = getMcpLauncherPath();
27357
- write(mcp, (posix ? posixLauncher : windowsLauncher)("MCP server", self, self.mcpServer));
27358
- }
27359
- return { cli, mcp };
27360
- }
27361
27380
  const LAUNCHD_LABEL = "com.gitwarren.daemon";
27362
27381
  const SYSTEMD_UNIT = "gitwarren.service";
27363
27382
  const WINDOWS_TASK = "GitWarren daemon";
@@ -27416,11 +27435,18 @@ const USAGE$1 = `gitwarren service install [--no-login-item]
27416
27435
  gitwarren service uninstall
27417
27436
  gitwarren service status
27418
27437
 
27419
- install writes ~/.gitwarren/bin/gitwarren and gitwarren-mcp, and registers a
27420
- login item that runs \`gitwarren serve --listen\`.
27421
- uninstall removes the login item. The launchers stay: an agent config may name
27422
- one, and removing a login item is not a request to break that.
27423
- status says what is registered and what is running.
27438
+ install keeps GitWarren running in the background: it starts now, and again
27439
+ at every login, so \`gitwarren open\` and the links an agent hands
27440
+ you always have something to open. Registers a LaunchAgent on
27441
+ macOS, a systemd user unit on Linux, an at-logon task on Windows.
27442
+ Also writes ~/.gitwarren/bin/gitwarren and gitwarren-mcp, the
27443
+ stable paths the app and an agent start this install by.
27444
+ --no-login-item writes those two files and registers nothing,
27445
+ for a machine that is only ever reached from another one.
27446
+ uninstall stops the background GitWarren and removes the login item. The two
27447
+ files in ~/.gitwarren/bin stay, because an agent config may name
27448
+ one; delete that directory by hand to remove them.
27449
+ status what is registered, what is running, and where the data is.
27424
27450
  `;
27425
27451
  function macLogPath() {
27426
27452
  return node_path.join(node_os.homedir(), "Library", "Logs", "GitWarren", "daemon.log");
@@ -27471,6 +27497,7 @@ function registerLoginItem(launcher) {
27471
27497
  "ONLOGON",
27472
27498
  "/F"
27473
27499
  ]);
27500
+ ask("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
27474
27501
  return `Scheduled Task "${WINDOWS_TASK}"`;
27475
27502
  }
27476
27503
  if (path === null) throw new Error("no login item path for this platform");
@@ -27526,14 +27553,22 @@ function install(argv) {
27526
27553
  );
27527
27554
  }
27528
27555
  if (argv.includes("--no-login-item")) {
27529
- console.log("--no-login-item: nothing was registered to start at login.");
27556
+ console.log("Nothing was registered to start at login (--no-login-item).");
27530
27557
  return true;
27531
27558
  }
27532
- console.log(registerLoginItem(cli) + " will run `gitwarren serve --listen` at login.");
27533
27559
  const owner = readLiveDaemonRuntime();
27560
+ console.log(`registered ${registerLoginItem(cli)}`);
27534
27561
  if (owner?.owner === "gui") {
27535
27562
  console.log(
27536
- "\nGitWarren.app is running and owns this data directory. A data directory has one owner, so the daemon will stand aside whenever the app is up - which is fine, and means the login item only does something on the logins where you do not open it."
27563
+ "\nThe desktop app is running and owns this data directory, so the background GitWarren has stood aside for now. Only one can serve at a time; the login item does its job on the logins where you do not open the app."
27564
+ );
27565
+ } else if (owner !== null) {
27566
+ console.log(
27567
+ "\nA `gitwarren serve` is already running in a terminal, so the background GitWarren has stood aside for now. Stop that one and it takes over at the next login, or straight away if you run `gitwarren service install` again."
27568
+ );
27569
+ } else {
27570
+ console.log(
27571
+ "\nGitWarren is now running in the background, and will start again when you log in. `gitwarren open` opens it in your browser; `gitwarren service uninstall` undoes all of this."
27537
27572
  );
27538
27573
  }
27539
27574
  console.log(`
@@ -27549,17 +27584,19 @@ function runService(argv) {
27549
27584
  case "uninstall":
27550
27585
  console.log(`removed ${removeLoginItem()}`);
27551
27586
  console.log(
27552
- "The launchers in ~/.gitwarren/bin were left alone. Delete them by hand if an agent no longer needs to reach this machine."
27587
+ "GitWarren no longer runs in the background or starts at login. `gitwarren serve` still runs it in a terminal. The two files in ~/.gitwarren/bin were left alone, because an agent config may name one; delete that directory to remove them."
27553
27588
  );
27554
27589
  return true;
27555
27590
  case "status": {
27556
27591
  const owner = readLiveDaemonRuntime();
27557
- console.log(`Login item: ${loginItemState()}`);
27558
- console.log(`Launcher: ${fs.existsSync(getCliLauncherPath()) ? getCliLauncherPath() : "not written"}`);
27592
+ const mcp = getMcpLauncherPath();
27593
+ console.log(`Login item: ${loginItemState()}`);
27594
+ console.log(`CLI launcher: ${fs.existsSync(getCliLauncherPath()) ? getCliLauncherPath() : "not written"}`);
27595
+ console.log(`MCP launcher: ${fs.existsSync(mcp) ? mcp : "not written - `gitwarren agent-setup` writes it"}`);
27559
27596
  console.log(
27560
- `Serving now: ${owner === null ? "nothing" : `${owner.owner === "gui" ? "GitWarren.app" : "gitwarren serve"} (pid ${owner.pid})`}`
27597
+ `Running now: ${owner === null ? "nothing - `gitwarren serve` runs it in this terminal" : `${owner.owner === "gui" ? "the desktop app" : "gitwarren serve"} (pid ${owner.pid})`}`
27561
27598
  );
27562
- console.log(`Data: ${getDataDirectory()}`);
27599
+ console.log(`Data: ${getDataDirectory()}`);
27563
27600
  return true;
27564
27601
  }
27565
27602
  default:
@@ -27572,30 +27609,75 @@ function runService(argv) {
27572
27609
  return true;
27573
27610
  }
27574
27611
  }
27575
- const VERSION = "0.1.8-beta.1";
27576
- const USAGE = `gitwarren - local-only code review, from the command line
27612
+ const VERSION = "0.1.9";
27613
+ const USAGE = `gitwarren - code review for your own git repositories, in a browser tab
27614
+
27615
+ Run it now
27616
+ gitwarren serve run GitWarren in this terminal and print its URL
27617
+ (--open also opens the browser; Ctrl-C stops it)
27618
+ gitwarren open [link] open the running GitWarren in your browser
27619
+
27620
+ Keep it running
27621
+ gitwarren service install run GitWarren in the background, from now and at
27622
+ every login
27623
+ gitwarren service uninstall stop that, and remove the login item
27624
+ gitwarren service status what is running, and where the data is
27577
27625
 
27578
- gitwarren serve serve the web view on loopback and print its URL
27579
- gitwarren serve --stdio answer GitWarren's protocol on stdin/stdout
27580
- gitwarren open [link] open this machine's GitWarren in a browser
27581
- gitwarren service install write the launchers and start at login
27582
- gitwarren service uninstall remove the login item
27583
- gitwarren service status what is registered, and what is running
27584
- gitwarren agent-setup print the sentence that points an agent here
27626
+ Let a coding agent in
27627
+ gitwarren agent-setup print the one sentence to give an agent so it can
27628
+ reach this GitWarren over MCP
27629
+
27630
+ gitwarren serve --stdio answer GitWarren's protocol on stdin/stdout (this
27631
+ is what another machine's GitWarren spawns)
27585
27632
  gitwarren --version
27586
27633
 
27587
- Run a subcommand with no valid arguments to see its own usage.
27634
+ Run a subcommand with --help to see its own usage.
27588
27635
 
27589
- The web view is served on 127.0.0.1 only, behind a token minted per launch.
27590
- \`gitwarren open\` carries that token for you; see docs/across-hosts.md.
27636
+ GitWarren serves on 127.0.0.1 only, behind a token minted per launch, and
27637
+ \`gitwarren open\` carries that token for you. Reviews live in one SQLite file
27638
+ on this machine - serving, the background service and the MCP server all read
27639
+ the same one, so an agent can use GitWarren whether or not it is being served.
27591
27640
  `;
27641
+ const SERVE_USAGE = `gitwarren serve [--open]
27642
+ gitwarren serve --stdio
27643
+
27644
+ Runs GitWarren in this terminal, serving the review UI on 127.0.0.1 and printing
27645
+ a URL that carries this launch's token. Ctrl-C stops it.
27646
+
27647
+ --open also open that URL in your browser
27648
+ --stdio answer GitWarren's protocol on stdin/stdout instead. This is what a
27649
+ GitWarren on another machine spawns over ssh or wsl.exe; by hand it
27650
+ is a way to see what the protocol says.
27651
+
27652
+ Serving also writes ~/.gitwarren/bin/gitwarren-mcp, the command a coding agent
27653
+ starts the MCP server with - see \`gitwarren agent-setup\`.
27654
+ `;
27655
+ function afterListening(url, open2) {
27656
+ const launchers = ensureLaunchers();
27657
+ if (launchers.refused) {
27658
+ console.error(
27659
+ `
27660
+ [gitwarren] no launcher was written to ~/.gitwarren/bin, so an agent has nothing to start yet: ${launchers.refused}`
27661
+ );
27662
+ } else if (launchers.created.length > 0) {
27663
+ console.error(`
27664
+ [gitwarren] wrote ${launchers.created.join(" and ")}`);
27665
+ }
27666
+ if (open2) {
27667
+ openInBrowser(url, (message) => console.error(`[gitwarren] ${message}. The URL is above.`));
27668
+ }
27669
+ }
27592
27670
  function runCli(argv) {
27593
27671
  const [command, ...rest] = argv;
27594
27672
  switch (command) {
27595
- case "serve":
27596
- return runDaemon(
27597
- rest.includes("--stdio") || rest.includes("--listen") ? rest : [...rest, "--listen"]
27598
- );
27673
+ case "serve": {
27674
+ if (rest.includes("--help") || rest.includes("-h")) {
27675
+ console.log(SERVE_USAGE);
27676
+ return true;
27677
+ }
27678
+ const argv2 = rest.includes("--stdio") || rest.includes("--listen") ? rest : [...rest, "--listen"];
27679
+ return runDaemon(argv2, { onListening: (url) => afterListening(url, rest.includes("--open")) });
27680
+ }
27599
27681
  case "open":
27600
27682
  return runOpen(rest);
27601
27683
  case "service":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitwarren",
3
- "version": "0.1.8-beta.1",
3
+ "version": "0.1.9",
4
4
  "description": "Code review for your own machines and your own agents, served on loopback. No Electron, no account.",
5
5
  "keywords": ["code-review", "git", "diff", "mcp", "local-first", "ssh", "wsl", "tailscale", "self-hosted"],
6
6
  "homepage": "https://github.com/klarluft/gitwarren-app",