@xbghc/warden 0.1.0 → 0.3.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/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/cli.ts
4
- import path7 from "path";
4
+ import path8 from "path";
5
5
  import { spawn } from "child_process";
6
6
  import { existsSync } from "fs";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // packages/server/src/index.ts
10
10
  import net from "net";
11
+ import { randomUUID as randomUUID2 } from "crypto";
11
12
 
12
13
  // node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.7/node_modules/@hono/node-server/dist/index.mjs
13
14
  import { createServer as createServerHTTP } from "http";
@@ -324,13 +325,13 @@ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromi
324
325
  }
325
326
  }
326
327
  }
327
- function writeFromReadableStream(stream, writable) {
328
- if (stream.locked) {
328
+ function writeFromReadableStream(stream2, writable) {
329
+ if (stream2.locked) {
329
330
  throw new TypeError("ReadableStream is locked.");
330
331
  } else if (writable.destroyed) {
331
332
  return;
332
333
  }
333
- return writeFromReadableStreamDefaultReader(stream.getReader(), writable);
334
+ return writeFromReadableStreamDefaultReader(stream2.getReader(), writable);
334
335
  }
335
336
  var buildOutgoingHttpHeaders = (headers) => {
336
337
  const res = {};
@@ -651,7 +652,7 @@ var serve = (options, listeningListener) => {
651
652
  };
652
653
 
653
654
  // packages/server/src/app.ts
654
- import path6 from "path";
655
+ import path7 from "path";
655
656
  import { randomUUID } from "crypto";
656
657
 
657
658
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
@@ -811,26 +812,26 @@ var throwNestingLimitExceeded = () => {
811
812
  };
812
813
 
813
814
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
814
- var splitPath = (path8) => {
815
- const paths = path8.split("/");
815
+ var splitPath = (path9) => {
816
+ const paths = path9.split("/");
816
817
  if (paths[0] === "") {
817
818
  paths.shift();
818
819
  }
819
820
  return paths;
820
821
  };
821
822
  var splitRoutingPath = (routePath) => {
822
- const { groups, path: path8 } = extractGroupsFromPath(routePath);
823
- const paths = splitPath(path8);
823
+ const { groups, path: path9 } = extractGroupsFromPath(routePath);
824
+ const paths = splitPath(path9);
824
825
  return replaceGroupMarks(paths, groups);
825
826
  };
826
- var extractGroupsFromPath = (path8) => {
827
+ var extractGroupsFromPath = (path9) => {
827
828
  const groups = [];
828
- path8 = path8.replace(/\{[^}]+\}/g, (match2, index) => {
829
+ path9 = path9.replace(/\{[^}]+\}/g, (match2, index) => {
829
830
  const mark = `@${index}`;
830
831
  groups.push([mark, match2]);
831
832
  return mark;
832
833
  });
833
- return { groups, path: path8 };
834
+ return { groups, path: path9 };
834
835
  };
835
836
  var replaceGroupMarks = (paths, groups) => {
836
837
  for (let i = groups.length - 1; i >= 0; i--) {
@@ -887,8 +888,8 @@ var getPath = (request) => {
887
888
  const queryIndex = url.indexOf("?", i);
888
889
  const hashIndex = url.indexOf("#", i);
889
890
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
890
- const path8 = url.slice(start, end);
891
- return tryDecodeURI(path8.includes("%25") ? path8.replace(/%25/g, "%2525") : path8);
891
+ const path9 = url.slice(start, end);
892
+ return tryDecodeURI(path9.includes("%25") ? path9.replace(/%25/g, "%2525") : path9);
892
893
  } else if (charCode === 63 || charCode === 35) {
893
894
  break;
894
895
  }
@@ -905,11 +906,11 @@ var mergePath = (base, sub, ...rest) => {
905
906
  }
906
907
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
907
908
  };
908
- var checkOptionalParameter = (path8) => {
909
- if (path8.charCodeAt(path8.length - 1) !== 63 || !path8.includes(":")) {
909
+ var checkOptionalParameter = (path9) => {
910
+ if (path9.charCodeAt(path9.length - 1) !== 63 || !path9.includes(":")) {
910
911
  return null;
911
912
  }
912
- const segments = path8.split("/");
913
+ const segments = path9.split("/");
913
914
  const results = [];
914
915
  let basePath = "";
915
916
  segments.forEach((segment) => {
@@ -1051,9 +1052,9 @@ var HonoRequest = class {
1051
1052
  */
1052
1053
  path;
1053
1054
  bodyCache = {};
1054
- constructor(request, path8 = "/", matchResult = [[]]) {
1055
+ constructor(request, path9 = "/", matchResult = [[]]) {
1055
1056
  this.raw = request;
1056
- this.path = path8;
1057
+ this.path = path9;
1057
1058
  this.#matchResult = matchResult;
1058
1059
  }
1059
1060
  param(key) {
@@ -1825,8 +1826,8 @@ var Hono = class _Hono {
1825
1826
  return this;
1826
1827
  };
1827
1828
  });
1828
- this.on = (method, path8, ...handlers) => {
1829
- for (const p of [path8].flat()) {
1829
+ this.on = (method, path9, ...handlers) => {
1830
+ for (const p of [path9].flat()) {
1830
1831
  this.#path = p;
1831
1832
  for (const m of [method].flat()) {
1832
1833
  const methodName = m.toUpperCase();
@@ -1884,8 +1885,8 @@ var Hono = class _Hono {
1884
1885
  * app.route("/api", app2) // GET /api/user
1885
1886
  * ```
1886
1887
  */
1887
- route(path8, app) {
1888
- const subApp = this.basePath(path8);
1888
+ route(path9, app) {
1889
+ const subApp = this.basePath(path9);
1889
1890
  app.routes.map((r) => {
1890
1891
  let handler;
1891
1892
  if (app.errorHandler === errorHandler) {
@@ -1911,9 +1912,9 @@ var Hono = class _Hono {
1911
1912
  * const api = new Hono().basePath('/api')
1912
1913
  * ```
1913
1914
  */
1914
- basePath(path8) {
1915
+ basePath(path9) {
1915
1916
  const subApp = this.#clone();
1916
- subApp._basePath = mergePath(this._basePath, path8);
1917
+ subApp._basePath = mergePath(this._basePath, path9);
1917
1918
  return subApp;
1918
1919
  }
1919
1920
  /**
@@ -1987,7 +1988,7 @@ var Hono = class _Hono {
1987
1988
  * })
1988
1989
  * ```
1989
1990
  */
1990
- mount(path8, applicationHandler, options) {
1991
+ mount(path9, applicationHandler, options) {
1991
1992
  let replaceRequest;
1992
1993
  let optionHandler;
1993
1994
  if (options) {
@@ -2014,7 +2015,7 @@ var Hono = class _Hono {
2014
2015
  return [c.env, executionContext];
2015
2016
  };
2016
2017
  replaceRequest ||= (() => {
2017
- const mergedPath = mergePath(this._basePath, path8);
2018
+ const mergedPath = mergePath(this._basePath, path9);
2018
2019
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
2019
2020
  return (request) => {
2020
2021
  const url = new URL(request.url);
@@ -2029,18 +2030,18 @@ var Hono = class _Hono {
2029
2030
  }
2030
2031
  await next();
2031
2032
  };
2032
- this.#addRoute(METHOD_NAME_ALL, mergePath(path8, "*"), handler);
2033
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path9, "*"), handler);
2033
2034
  return this;
2034
2035
  }
2035
- #addRoute(method, path8, handler, baseRoutePath) {
2036
- path8 = mergePath(this._basePath, path8);
2036
+ #addRoute(method, path9, handler, baseRoutePath) {
2037
+ path9 = mergePath(this._basePath, path9);
2037
2038
  const r = {
2038
2039
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
2039
- path: path8,
2040
+ path: path9,
2040
2041
  method,
2041
2042
  handler
2042
2043
  };
2043
- this.router.add(method, path8, [handler, r]);
2044
+ this.router.add(method, path9, [handler, r]);
2044
2045
  this.routes.push(r);
2045
2046
  }
2046
2047
  #handleError(err, c) {
@@ -2053,10 +2054,10 @@ var Hono = class _Hono {
2053
2054
  if (method === "HEAD") {
2054
2055
  return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
2055
2056
  }
2056
- const path8 = this.getPath(request, { env });
2057
- const matchResult = this.router.match(method, path8);
2057
+ const path9 = this.getPath(request, { env });
2058
+ const matchResult = this.router.match(method, path9);
2058
2059
  const c = new Context(request, {
2059
- path: path8,
2060
+ path: path9,
2060
2061
  matchResult,
2061
2062
  env,
2062
2063
  executionCtx,
@@ -2159,7 +2160,7 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
2159
2160
 
2160
2161
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
2161
2162
  var emptyParam = [];
2162
- function match(method, path8) {
2163
+ function match(method, path9) {
2163
2164
  const matchers = this.buildAllMatchers();
2164
2165
  const match2 = ((method2, path22) => {
2165
2166
  const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
@@ -2175,7 +2176,7 @@ function match(method, path8) {
2175
2176
  return [matcher[1][index], match3];
2176
2177
  });
2177
2178
  this.match = match2;
2178
- return match2(method, path8);
2179
+ return match2(method, path9);
2179
2180
  }
2180
2181
 
2181
2182
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -2292,14 +2293,14 @@ var Trie = class {
2292
2293
  #index = 0;
2293
2294
  // dynamic path -> [handler index, param assoc]; static paths are not registered
2294
2295
  paths = createNullObject();
2295
- insert(path8, isStatic) {
2296
+ insert(path9, isStatic) {
2296
2297
  if (isStatic) {
2297
- this.#root.insert(path8.split(""), 0, [], this.#context, true);
2298
+ this.#root.insert(path9.split(""), 0, [], this.#context, true);
2298
2299
  return;
2299
2300
  }
2300
2301
  const paramAssoc = [];
2301
2302
  const groups = [];
2302
- let markedPath = path8;
2303
+ let markedPath = path9;
2303
2304
  for (let i = 0; ; ) {
2304
2305
  let replaced = false;
2305
2306
  markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
@@ -2324,7 +2325,7 @@ var Trie = class {
2324
2325
  }
2325
2326
  }
2326
2327
  this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
2327
- this.paths[path8] = [this.#index++, paramAssoc];
2328
+ this.paths[path9] = [this.#index++, paramAssoc];
2328
2329
  }
2329
2330
  buildRegExp() {
2330
2331
  let regexp = this.#root.buildRegExpStr();
@@ -2351,17 +2352,17 @@ var Trie = class {
2351
2352
 
2352
2353
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
2353
2354
  var wildcardRegExpCache = createNullObject();
2354
- function buildWildcardRegExp(path8) {
2355
- return wildcardRegExpCache[path8] ??= new RegExp(
2356
- `^${path8.replace(
2355
+ function buildWildcardRegExp(path9) {
2356
+ return wildcardRegExpCache[path9] ??= new RegExp(
2357
+ `^${path9.replace(
2357
2358
  /\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g,
2358
2359
  (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`
2359
2360
  )}$`
2360
2361
  );
2361
2362
  }
2362
- function findMiddleware(middleware, path8) {
2363
+ function findMiddleware(middleware, path9) {
2363
2364
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
2364
- if (buildWildcardRegExp(k).test(path8)) {
2365
+ if (buildWildcardRegExp(k).test(path9)) {
2365
2366
  return [...middleware[k]];
2366
2367
  }
2367
2368
  }
@@ -2377,14 +2378,14 @@ var RegExpRouter = class {
2377
2378
  this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
2378
2379
  this.#tries = { [METHOD_NAME_ALL]: new Trie() };
2379
2380
  }
2380
- #insertPath(method, path8) {
2381
+ #insertPath(method, path9) {
2381
2382
  try {
2382
- this.#tries[method].insert(path8, !/\*|\/:/.test(path8));
2383
+ this.#tries[method].insert(path9, !/\*|\/:/.test(path9));
2383
2384
  } catch (e) {
2384
- throw e === PATH_ERROR ? new UnsupportedPathError(path8) : e;
2385
+ throw e === PATH_ERROR ? new UnsupportedPathError(path9) : e;
2385
2386
  }
2386
2387
  }
2387
- add(method, path8, handler) {
2388
+ add(method, path9, handler) {
2388
2389
  const middleware = this.#middleware;
2389
2390
  const routes = this.#routes;
2390
2391
  if (!middleware) {
@@ -2400,28 +2401,28 @@ var RegExpRouter = class {
2400
2401
  }
2401
2402
  }
2402
2403
  }
2403
- if (path8 === "/*") {
2404
- path8 = "*";
2404
+ if (path9 === "/*") {
2405
+ path9 = "*";
2405
2406
  }
2406
2407
  const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
2407
- if (/\*$/.test(path8)) {
2408
- const re = buildWildcardRegExp(path8);
2408
+ if (/\*$/.test(path9)) {
2409
+ const re = buildWildcardRegExp(path9);
2409
2410
  for (const m of methods) {
2410
- if (!middleware[m][path8]) {
2411
- this.#insertPath(m, path8);
2412
- middleware[m][path8] = findMiddleware(middleware[m], path8) || findMiddleware(middleware[METHOD_NAME_ALL], path8) || [];
2411
+ if (!middleware[m][path9]) {
2412
+ this.#insertPath(m, path9);
2413
+ middleware[m][path9] = findMiddleware(middleware[m], path9) || findMiddleware(middleware[METHOD_NAME_ALL], path9) || [];
2413
2414
  }
2414
2415
  }
2415
2416
  for (const handlerMap of [middleware, routes]) {
2416
2417
  for (const m of methods) {
2417
2418
  for (const p in handlerMap[m]) {
2418
- re.test(p) && handlerMap[m][p].push([handler, path8]);
2419
+ re.test(p) && handlerMap[m][p].push([handler, path9]);
2419
2420
  }
2420
2421
  }
2421
2422
  }
2422
2423
  return;
2423
2424
  }
2424
- const paths = checkOptionalParameter(path8) || [path8];
2425
+ const paths = checkOptionalParameter(path9) || [path9];
2425
2426
  for (const path22 of paths) {
2426
2427
  for (const m of methods) {
2427
2428
  if (!routes[m][path22]) {
@@ -2450,11 +2451,11 @@ var RegExpRouter = class {
2450
2451
  const handlerData = [];
2451
2452
  const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
2452
2453
  for (const r of [middleware, routes]) {
2453
- for (const path8 in r) {
2454
- const handlers = r[path8];
2455
- const pathData = trie.paths[path8];
2454
+ for (const path9 in r) {
2455
+ const handlers = r[path9];
2456
+ const pathData = trie.paths[path9];
2456
2457
  if (!pathData) {
2457
- staticMap[path8] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2458
+ staticMap[path9] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2458
2459
  continue;
2459
2460
  }
2460
2461
  handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
@@ -2478,13 +2479,13 @@ var SmartRouter = class {
2478
2479
  constructor(init) {
2479
2480
  this.#routers = init.routers;
2480
2481
  }
2481
- add(method, path8, handler) {
2482
+ add(method, path9, handler) {
2482
2483
  if (!this.#routes) {
2483
2484
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
2484
2485
  }
2485
- this.#routes.push([method, path8, handler]);
2486
+ this.#routes.push([method, path9, handler]);
2486
2487
  }
2487
- match(method, path8) {
2488
+ match(method, path9) {
2488
2489
  if (!this.#routes) {
2489
2490
  throw new Error("Fatal error");
2490
2491
  }
@@ -2499,7 +2500,7 @@ var SmartRouter = class {
2499
2500
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
2500
2501
  router.add(...routes[i2]);
2501
2502
  }
2502
- res = router.match(method, path8);
2503
+ res = router.match(method, path9);
2503
2504
  } catch (e) {
2504
2505
  if (e instanceof UnsupportedPathError) {
2505
2506
  continue;
@@ -2534,9 +2535,9 @@ var Node2 = class _Node2 {
2534
2535
  #patterns = [];
2535
2536
  #pattern;
2536
2537
  #params = emptyParams;
2537
- insert(method, path8, handler) {
2538
+ insert(method, path9, handler) {
2538
2539
  let curNode = this;
2539
- const parts = splitRoutingPath(path8);
2540
+ const parts = splitRoutingPath(path9);
2540
2541
  const possibleKeys = /* @__PURE__ */ new Set();
2541
2542
  let i = 0;
2542
2543
  for (const p of parts) {
@@ -2576,12 +2577,12 @@ var Node2 = class _Node2 {
2576
2577
  }
2577
2578
  }
2578
2579
  }
2579
- search(method, path8) {
2580
+ search(method, path9) {
2580
2581
  const handlerSets = [];
2581
2582
  this.#params = emptyParams;
2582
2583
  const curNode = this;
2583
2584
  let curNodes = [curNode];
2584
- const parts = splitPath(path8);
2585
+ const parts = splitPath(path9);
2585
2586
  const curNodesQueue = [];
2586
2587
  const len = parts.length;
2587
2588
  let partOffsets = null;
@@ -2623,13 +2624,13 @@ var Node2 = class _Node2 {
2623
2624
  if (matcher !== true) {
2624
2625
  if (!partOffsets) {
2625
2626
  partOffsets = [];
2626
- let offset = path8[0] === "/" ? 1 : 0;
2627
+ let offset = path9[0] === "/" ? 1 : 0;
2627
2628
  for (let p = 0; p < len; p++) {
2628
2629
  partOffsets[p] = offset;
2629
2630
  offset += parts[p].length + 1;
2630
2631
  }
2631
2632
  }
2632
- const restPathString = path8.slice(partOffsets[i]);
2633
+ const restPathString = path9.slice(partOffsets[i]);
2633
2634
  const m = matcher.exec(restPathString);
2634
2635
  if (m) {
2635
2636
  params[name] = m[0];
@@ -2689,13 +2690,13 @@ var Node2 = class _Node2 {
2689
2690
  var TrieRouter = class {
2690
2691
  name = "TrieRouter";
2691
2692
  #node = new Node2();
2692
- add(method, path8, handler) {
2693
- for (const result of checkOptionalParameter(path8) || [path8]) {
2693
+ add(method, path9, handler) {
2694
+ for (const result of checkOptionalParameter(path9) || [path9]) {
2694
2695
  this.#node.insert(method, result, handler);
2695
2696
  }
2696
2697
  }
2697
- match(method, path8) {
2698
- return this.#node.search(method, path8);
2698
+ match(method, path9) {
2699
+ return this.#node.search(method, path9);
2699
2700
  }
2700
2701
  };
2701
2702
 
@@ -2714,6 +2715,167 @@ var Hono2 = class extends Hono {
2714
2715
  }
2715
2716
  };
2716
2717
 
2718
+ // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/stream.js
2719
+ var StreamingApi = class {
2720
+ writer;
2721
+ encoder;
2722
+ writable;
2723
+ abortSubscribers = [];
2724
+ responseReadable;
2725
+ /**
2726
+ * Whether the stream has been aborted.
2727
+ */
2728
+ aborted = false;
2729
+ /**
2730
+ * Whether the stream has been closed normally.
2731
+ */
2732
+ closed = false;
2733
+ constructor(writable, _readable) {
2734
+ this.writable = writable;
2735
+ this.writer = writable.getWriter();
2736
+ this.encoder = new TextEncoder();
2737
+ const reader = _readable.getReader();
2738
+ this.abortSubscribers.push(async () => {
2739
+ await reader.cancel();
2740
+ });
2741
+ this.responseReadable = new ReadableStream({
2742
+ async pull(controller) {
2743
+ const { done, value } = await reader.read();
2744
+ done ? controller.close() : controller.enqueue(value);
2745
+ },
2746
+ cancel: () => {
2747
+ if (!this.closed) {
2748
+ this.abort();
2749
+ }
2750
+ }
2751
+ });
2752
+ }
2753
+ async write(input) {
2754
+ try {
2755
+ if (typeof input === "string") {
2756
+ input = this.encoder.encode(input);
2757
+ }
2758
+ await this.writer.write(input);
2759
+ } catch {
2760
+ }
2761
+ return this;
2762
+ }
2763
+ async writeln(input) {
2764
+ await this.write(input + "\n");
2765
+ return this;
2766
+ }
2767
+ sleep(ms) {
2768
+ return new Promise((res) => setTimeout(res, ms));
2769
+ }
2770
+ async close() {
2771
+ this.closed = true;
2772
+ try {
2773
+ await this.writer.close();
2774
+ } catch {
2775
+ }
2776
+ }
2777
+ async pipe(body) {
2778
+ this.writer.releaseLock();
2779
+ try {
2780
+ await body.pipeTo(this.writable, { preventClose: true, preventAbort: true });
2781
+ } finally {
2782
+ this.writer = this.writable.getWriter();
2783
+ }
2784
+ }
2785
+ onAbort(listener) {
2786
+ this.abortSubscribers.push(listener);
2787
+ }
2788
+ /**
2789
+ * Abort the stream.
2790
+ * You can call this method when stream is aborted by external event.
2791
+ */
2792
+ abort() {
2793
+ if (!this.aborted) {
2794
+ this.aborted = true;
2795
+ this.abortSubscribers.forEach((subscriber) => {
2796
+ try {
2797
+ void Promise.resolve(subscriber()).catch(() => {
2798
+ });
2799
+ } catch {
2800
+ }
2801
+ });
2802
+ }
2803
+ }
2804
+ };
2805
+
2806
+ // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/helper/streaming/utils.js
2807
+ var isOldBunVersion = () => {
2808
+ const version = typeof Bun !== "undefined" ? Bun.version : void 0;
2809
+ if (version === void 0) {
2810
+ return false;
2811
+ }
2812
+ const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
2813
+ isOldBunVersion = () => result;
2814
+ return result;
2815
+ };
2816
+
2817
+ // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/helper/streaming/sse.js
2818
+ var SSEStreamingApi = class extends StreamingApi {
2819
+ constructor(writable, readable) {
2820
+ super(writable, readable);
2821
+ }
2822
+ async writeSSE(message) {
2823
+ const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
2824
+ const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
2825
+ return `data: ${line}`;
2826
+ }).join("\n");
2827
+ for (const key of ["event", "id"]) {
2828
+ const value = message[key];
2829
+ if (value && /[\r\n]/.test(value)) {
2830
+ throw new Error(`${key} must not contain "\\r" or "\\n"`);
2831
+ }
2832
+ }
2833
+ const sseData = [
2834
+ message.event && `event: ${message.event}`,
2835
+ dataLines,
2836
+ message.id !== void 0 && `id: ${message.id}`,
2837
+ message.retry !== void 0 && `retry: ${message.retry}`
2838
+ ].filter(Boolean).join("\n") + "\n\n";
2839
+ await this.write(sseData);
2840
+ }
2841
+ };
2842
+ var run = async (stream2, cb, onError) => {
2843
+ try {
2844
+ await cb(stream2);
2845
+ } catch (e) {
2846
+ if (e instanceof Error && onError) {
2847
+ await onError(e, stream2);
2848
+ await stream2.writeSSE({
2849
+ event: "error",
2850
+ data: e.message
2851
+ });
2852
+ } else {
2853
+ console.error(e);
2854
+ }
2855
+ } finally {
2856
+ stream2.close();
2857
+ }
2858
+ };
2859
+ var contextStash = /* @__PURE__ */ new WeakMap();
2860
+ var streamSSE = (c, cb, onError) => {
2861
+ const { readable, writable } = new TransformStream();
2862
+ const stream2 = new SSEStreamingApi(writable, readable);
2863
+ if (isOldBunVersion()) {
2864
+ c.req.raw.signal.addEventListener("abort", () => {
2865
+ if (!stream2.closed) {
2866
+ stream2.abort();
2867
+ }
2868
+ });
2869
+ }
2870
+ contextStash.set(stream2.responseReadable, c);
2871
+ c.header("Transfer-Encoding", "chunked");
2872
+ c.header("Content-Type", "text/event-stream");
2873
+ c.header("Cache-Control", "no-cache");
2874
+ c.header("Connection", "keep-alive");
2875
+ run(stream2, cb, onError);
2876
+ return c.newResponse(stream2.responseReadable);
2877
+ };
2878
+
2717
2879
  // packages/shared/src/target.ts
2718
2880
  var TargetKeyError = class extends Error {
2719
2881
  constructor(message) {
@@ -2758,6 +2920,47 @@ function parseTargetKey(key) {
2758
2920
  }
2759
2921
  return parseSuffix(key);
2760
2922
  }
2923
+ function formatTargetKey(t) {
2924
+ let suffix;
2925
+ switch (t.kind) {
2926
+ case "working":
2927
+ case "staged":
2928
+ case "all":
2929
+ suffix = t.kind;
2930
+ break;
2931
+ case "commit":
2932
+ suffix = `commit:${t.sha}`;
2933
+ break;
2934
+ case "range":
2935
+ suffix = `range:${t.base}..${t.head}`;
2936
+ break;
2937
+ }
2938
+ return t.worktree ? `worktree:${t.worktree}:${suffix}` : suffix;
2939
+ }
2940
+ function tryParseTargetKey(key) {
2941
+ try {
2942
+ return parseTargetKey(key);
2943
+ } catch {
2944
+ return void 0;
2945
+ }
2946
+ }
2947
+ function isLocalTarget(t) {
2948
+ return t.kind === "working" || t.kind === "staged" || t.kind === "all";
2949
+ }
2950
+ function commentScopeKey(key) {
2951
+ const t = tryParseTargetKey(key);
2952
+ if (!t || !isLocalTarget(t)) return key;
2953
+ return t.worktree ? `worktree:${t.worktree}:local` : "local";
2954
+ }
2955
+ function localViewKeys(key) {
2956
+ const t = tryParseTargetKey(key);
2957
+ const wt = t?.worktree ? { worktree: t.worktree } : {};
2958
+ return [
2959
+ formatTargetKey({ kind: "working", ...wt }),
2960
+ formatTargetKey({ kind: "staged", ...wt }),
2961
+ formatTargetKey({ kind: "all", ...wt })
2962
+ ];
2963
+ }
2761
2964
 
2762
2965
  // packages/server/src/errors.ts
2763
2966
  var HttpError = class extends Error {
@@ -2939,16 +3142,24 @@ async function listWorktrees(ctx) {
2939
3142
  }
2940
3143
  return out;
2941
3144
  }
3145
+ async function currentBranch(cwd) {
3146
+ const res = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).catch(() => ({ stdout: "" }));
3147
+ const name = res.stdout.trim();
3148
+ if (name && name !== "HEAD") return name;
3149
+ const head = await runGit(["rev-parse", "--verify", "--quiet", "HEAD"], { cwd }).catch(() => ({ stdout: "" }));
3150
+ const sha = head.stdout.trim();
3151
+ return sha ? sha.slice(0, 7) : "HEAD";
3152
+ }
2942
3153
  async function getRepoInfo(ctx, defaultTarget) {
2943
- const [branchRes, headRes, worktrees] = await Promise.all([
2944
- runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: ctx.root }).catch(() => ({ stdout: "HEAD" })),
3154
+ const [branch, headRes, worktrees] = await Promise.all([
3155
+ currentBranch(ctx.root),
2945
3156
  runGit(["rev-parse", "HEAD"], { cwd: ctx.root }).catch(() => ({ stdout: "" })),
2946
3157
  listWorktrees(ctx)
2947
3158
  ]);
2948
3159
  return {
2949
3160
  root: ctx.root,
2950
3161
  commonRoot: ctx.commonRoot,
2951
- branch: branchRes.stdout.trim(),
3162
+ branch,
2952
3163
  head: headRes.stdout.trim(),
2953
3164
  worktrees,
2954
3165
  defaultTarget
@@ -3459,10 +3670,10 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
3459
3670
  return path3.join(baseDir, repoHash(repoRoot), "state.json");
3460
3671
  }
3461
3672
  function defaultPrefs() {
3462
- return { viewMode: "unified", nvimSocketByRoot: {} };
3673
+ return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true };
3463
3674
  }
3464
3675
  function defaultState(repoRoot) {
3465
- return { schemaVersion: 1, repoRoot, targets: {}, issues: [], prefs: defaultPrefs() };
3676
+ return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
3466
3677
  }
3467
3678
  function ensureTarget(state, key) {
3468
3679
  let t = state.targets[key];
@@ -3482,17 +3693,40 @@ function normalise(raw2, repoRoot) {
3482
3693
  if (!v || typeof v !== "object") continue;
3483
3694
  targets[k] = {
3484
3695
  viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
3485
- comments: Array.isArray(v.comments) ? v.comments : []
3696
+ comments: Array.isArray(v.comments) ? v.comments : [],
3697
+ ...typeof v.head === "string" ? { head: v.head } : {}
3486
3698
  };
3487
3699
  }
3700
+ migrateLocalComments(targets);
3488
3701
  return {
3489
3702
  schemaVersion: 1,
3490
3703
  repoRoot: r.repoRoot ?? repoRoot,
3491
3704
  targets,
3492
3705
  issues: Array.isArray(r.issues) ? r.issues : [],
3493
- prefs: { ...defaultPrefs(), ...r.prefs ?? {}, nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {} }
3706
+ todos: Array.isArray(r.todos) ? r.todos : [],
3707
+ prefs: {
3708
+ ...defaultPrefs(),
3709
+ ...r.prefs ?? {},
3710
+ nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
3711
+ autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true
3712
+ }
3494
3713
  };
3495
3714
  }
3715
+ function migrateLocalComments(targets) {
3716
+ for (const key of Object.keys(targets).sort()) {
3717
+ const scope = commentScopeKey(key);
3718
+ if (scope === key) continue;
3719
+ const src = targets[key];
3720
+ if (src.comments.length === 0) continue;
3721
+ let dst = targets[scope];
3722
+ if (!dst) {
3723
+ dst = { viewed: {}, comments: [] };
3724
+ targets[scope] = dst;
3725
+ }
3726
+ dst.comments.push(...src.comments);
3727
+ src.comments = [];
3728
+ }
3729
+ }
3496
3730
  var LOCK_STALE_MS = 5e3;
3497
3731
  var LOCK_WAIT_MS = 5e3;
3498
3732
  async function sleep(ms) {
@@ -3525,7 +3759,7 @@ var StateStore = class {
3525
3759
  }
3526
3760
  /** Apply a mutation under lock. The callback receives the freshest on-disk state. */
3527
3761
  update(fn) {
3528
- const run = async () => {
3762
+ const run2 = async () => {
3529
3763
  await mkdir(path3.dirname(this.file), { recursive: true });
3530
3764
  const release = await this.acquireLock();
3531
3765
  try {
@@ -3537,7 +3771,7 @@ var StateStore = class {
3537
3771
  await release();
3538
3772
  }
3539
3773
  };
3540
- const p = this.queue.then(run, run);
3774
+ const p = this.queue.then(run2, run2);
3541
3775
  this.queue = p.catch(() => void 0);
3542
3776
  return p;
3543
3777
  }
@@ -3658,6 +3892,16 @@ function formatIssueExport({ repoRoot, issue, comments }) {
3658
3892
  parts.push(...comments.map(formatCommentSection));
3659
3893
  return parts.join("\n\n") + "\n";
3660
3894
  }
3895
+ function formatTodosExport({ repoRoot, branch, todos }) {
3896
+ const head = ["# TODO", `Branch: ${branch}`, `Repo: ${repoRoot}`, `Count: ${todos.length}`];
3897
+ const sections = todos.map((t, i) => {
3898
+ const title = `## ${i + 1}. ${t.title}${t.status === "done" ? " (done)" : ""}`;
3899
+ const body = t.body.replace(/\r\n/g, "\n").trim();
3900
+ return body ? `${title}
3901
+ ${body}` : title;
3902
+ });
3903
+ return [head.join("\n"), ...sections].join("\n\n") + "\n";
3904
+ }
3661
3905
 
3662
3906
  // packages/server/src/nvim.ts
3663
3907
  import os2 from "os";
@@ -3778,9 +4022,116 @@ var NvimService = class {
3778
4022
  }
3779
4023
  };
3780
4024
 
3781
- // packages/server/src/static.ts
4025
+ // packages/server/src/watcher.ts
3782
4026
  import path5 from "path";
3783
- import { readFile as readFile3, stat as stat3 } from "fs/promises";
4027
+ import { stat as stat3 } from "fs/promises";
4028
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
4029
+ function statusPaths(out) {
4030
+ const parts = out.split("\0");
4031
+ const paths = [];
4032
+ for (let i = 0; i < parts.length; i++) {
4033
+ const entry = parts[i];
4034
+ if (!entry || entry.length < 3) continue;
4035
+ const x = entry[0];
4036
+ const y = entry[1];
4037
+ const p = entry.slice(3);
4038
+ if (p) paths.push(p);
4039
+ if (x === "R" || x === "C" || y === "R" || y === "C") {
4040
+ const origin = parts[++i];
4041
+ if (origin) paths.push(origin);
4042
+ }
4043
+ }
4044
+ return paths;
4045
+ }
4046
+ var RepoWatcher = class {
4047
+ constructor(root, intervalMs = DEFAULT_POLL_INTERVAL_MS) {
4048
+ this.root = root;
4049
+ this.intervalMs = intervalMs;
4050
+ }
4051
+ root;
4052
+ intervalMs;
4053
+ listeners = /* @__PURE__ */ new Set();
4054
+ timer = null;
4055
+ signature = null;
4056
+ polling = false;
4057
+ get subscriberCount() {
4058
+ return this.listeners.size;
4059
+ }
4060
+ subscribe(listener) {
4061
+ this.listeners.add(listener);
4062
+ this.start();
4063
+ let released = false;
4064
+ return () => {
4065
+ if (released) return;
4066
+ released = true;
4067
+ this.listeners.delete(listener);
4068
+ if (this.listeners.size === 0) this.stop();
4069
+ };
4070
+ }
4071
+ start() {
4072
+ if (this.timer) return;
4073
+ this.timer = setInterval(() => void this.poll(), this.intervalMs);
4074
+ this.timer.unref?.();
4075
+ void this.poll();
4076
+ }
4077
+ stop() {
4078
+ if (this.timer) clearInterval(this.timer);
4079
+ this.timer = null;
4080
+ this.signature = null;
4081
+ }
4082
+ /**
4083
+ * Runs one comparison. Returns the change that was broadcast, or undefined when nothing moved.
4084
+ * The first poll of a watch only records the baseline. Public so tests can step it deterministically.
4085
+ */
4086
+ async poll() {
4087
+ if (this.polling) return void 0;
4088
+ this.polling = true;
4089
+ try {
4090
+ const snap = await this.snapshot();
4091
+ const baseline = this.signature === null;
4092
+ if (this.signature === snap.signature) return void 0;
4093
+ this.signature = snap.signature;
4094
+ if (baseline) return void 0;
4095
+ const change = { head: snap.head, branch: snap.branch, at: (/* @__PURE__ */ new Date()).toISOString() };
4096
+ for (const listener of [...this.listeners]) {
4097
+ try {
4098
+ listener(change);
4099
+ } catch {
4100
+ }
4101
+ }
4102
+ return change;
4103
+ } catch {
4104
+ return void 0;
4105
+ } finally {
4106
+ this.polling = false;
4107
+ }
4108
+ }
4109
+ async snapshot() {
4110
+ const [statusRes, headRes, branch] = await Promise.all([
4111
+ runGit(["status", "--porcelain", "-z"], { cwd: this.root }),
4112
+ runGit(["rev-parse", "HEAD"], { cwd: this.root }).catch(() => ({ stdout: "" })),
4113
+ // The same label getRepoInfo reports, so the branch identity the client filters todos by
4114
+ // never flips between `HEAD` and a short sha depending on which of the two spoke last.
4115
+ currentBranch(this.root)
4116
+ ]);
4117
+ const head = headRes.stdout.trim();
4118
+ const stats = await Promise.all(
4119
+ statusPaths(statusRes.stdout).map(async (rel) => {
4120
+ try {
4121
+ const st = await stat3(path5.join(this.root, rel));
4122
+ return `${rel} ${st.mtimeMs} ${st.size}`;
4123
+ } catch {
4124
+ return `${rel} -`;
4125
+ }
4126
+ })
4127
+ );
4128
+ return { signature: [head, branch, statusRes.stdout, ...stats].join("\n"), head, branch };
4129
+ }
4130
+ };
4131
+
4132
+ // packages/server/src/static.ts
4133
+ import path6 from "path";
4134
+ import { readFile as readFile3, stat as stat4 } from "fs/promises";
3784
4135
  var MIME = {
3785
4136
  ".html": "text/html; charset=utf-8",
3786
4137
  ".js": "text/javascript; charset=utf-8",
@@ -3797,20 +4148,20 @@ var MIME = {
3797
4148
  ".txt": "text/plain; charset=utf-8"
3798
4149
  };
3799
4150
  async function serveStaticFile(c, webDir) {
3800
- const root = path5.resolve(webDir);
4151
+ const root = path6.resolve(webDir);
3801
4152
  let rel;
3802
4153
  try {
3803
4154
  rel = decodeURIComponent(c.req.path);
3804
4155
  } catch {
3805
4156
  return c.text("Bad Request", 400);
3806
4157
  }
3807
- let target = path5.resolve(root, "." + rel);
3808
- if (target !== root && !target.startsWith(root + path5.sep)) return c.text("Forbidden", 403);
4158
+ let target = path6.resolve(root, "." + rel);
4159
+ if (target !== root && !target.startsWith(root + path6.sep)) return c.text("Forbidden", 403);
3809
4160
  try {
3810
- const st = await stat3(target);
3811
- if (st.isDirectory()) target = path5.join(target, "index.html");
4161
+ const st = await stat4(target);
4162
+ if (st.isDirectory()) target = path6.join(target, "index.html");
3812
4163
  } catch {
3813
- target = path5.join(root, "index.html");
4164
+ target = path6.join(root, "index.html");
3814
4165
  }
3815
4166
  let body;
3816
4167
  try {
@@ -3818,7 +4169,7 @@ async function serveStaticFile(c, webDir) {
3818
4169
  } catch {
3819
4170
  return c.text("Not Found", 404);
3820
4171
  }
3821
- const ext = path5.extname(target).toLowerCase();
4172
+ const ext = path6.extname(target).toLowerCase();
3822
4173
  const isAsset = rel.startsWith("/assets/");
3823
4174
  return new Response(new Uint8Array(body), {
3824
4175
  status: 200,
@@ -3830,6 +4181,7 @@ async function serveStaticFile(c, webDir) {
3830
4181
  }
3831
4182
 
3832
4183
  // packages/server/src/app.ts
4184
+ var SSE_HEARTBEAT_MS = 15e3;
3833
4185
  var LISTING_HINT_TTL_MS = 5 * 6e4;
3834
4186
  function createApp(opts) {
3835
4187
  const { repo, store } = opts;
@@ -3837,6 +4189,15 @@ function createApp(opts) {
3837
4189
  const app = new Hono2();
3838
4190
  const listings = /* @__PURE__ */ new Map();
3839
4191
  const changedNotices = /* @__PURE__ */ new Map();
4192
+ const watchers = /* @__PURE__ */ new Map();
4193
+ const watcherFor = (root) => {
4194
+ let w = watchers.get(root);
4195
+ if (!w) {
4196
+ w = new RepoWatcher(root, opts.watchIntervalMs);
4197
+ watchers.set(root, w);
4198
+ }
4199
+ return w;
4200
+ };
3840
4201
  let worktreeCache = null;
3841
4202
  const worktrees = async (force = false) => {
3842
4203
  if (!force && worktreeCache && Date.now() - worktreeCache.at < 1e4) return worktreeCache.list;
@@ -3886,6 +4247,7 @@ function createApp(opts) {
3886
4247
  return c.json({ error: err instanceof Error ? err.message : String(err), code: "internal" }, 500);
3887
4248
  });
3888
4249
  const api = new Hono2();
4250
+ api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
3889
4251
  api.get("/repo", async (c) => {
3890
4252
  const state = await store.load();
3891
4253
  const info = await getRepoInfo(repo, state.prefs.lastTarget ?? "working");
@@ -3898,6 +4260,7 @@ function createApp(opts) {
3898
4260
  const prefs = await store.update((s) => {
3899
4261
  if (body.viewMode === "unified" || body.viewMode === "split") s.prefs.viewMode = body.viewMode;
3900
4262
  if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
4263
+ if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
3901
4264
  if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
3902
4265
  s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
3903
4266
  }
@@ -3937,7 +4300,8 @@ function createApp(opts) {
3937
4300
  viewed: !stale.includes(d.path) && viewed[d.path] === d.contentHash,
3938
4301
  changed: notices.has(d.path)
3939
4302
  }));
3940
- const res = { targetKey: key, root: ctx.cwd, files, comments: t?.comments ?? [] };
4303
+ const scope = state.targets[commentScopeKey(key)];
4304
+ const res = { targetKey: key, root: ctx.cwd, files, comments: scope?.comments ?? [] };
3941
4305
  return c.json(res);
3942
4306
  });
3943
4307
  api.get("/targets/:key/file", async (c) => {
@@ -3978,7 +4342,7 @@ function createApp(opts) {
3978
4342
  api.get("/targets/:key/comments", async (c) => {
3979
4343
  const key = decodeKey(c.req.param("key"));
3980
4344
  const state = await store.load();
3981
- return c.json({ comments: state.targets[key]?.comments ?? [] });
4345
+ return c.json({ comments: state.targets[commentScopeKey(key)]?.comments ?? [] });
3982
4346
  });
3983
4347
  api.post("/targets/:key/comments", async (c) => {
3984
4348
  const key = decodeKey(c.req.param("key"));
@@ -4009,7 +4373,7 @@ function createApp(opts) {
4009
4373
  updatedAt: now
4010
4374
  };
4011
4375
  await store.update((s) => {
4012
- ensureTarget(s, key).comments.push(comment);
4376
+ ensureTarget(s, commentScopeKey(key)).comments.push(comment);
4013
4377
  });
4014
4378
  return c.json(comment, 201);
4015
4379
  });
@@ -4018,8 +4382,9 @@ function createApp(opts) {
4018
4382
  const id = c.req.param("id");
4019
4383
  const ctx = await targetCtx(key);
4020
4384
  const body = await c.req.json();
4385
+ const scope = commentScopeKey(key);
4021
4386
  const state = await store.load();
4022
- const existing = state.targets[key]?.comments.find((x) => x.id === id);
4387
+ const existing = state.targets[scope]?.comments.find((x) => x.id === id);
4023
4388
  if (!existing) throw notFound("comment not found");
4024
4389
  let patch = {};
4025
4390
  if (typeof body.body === "string") {
@@ -4042,11 +4407,13 @@ function createApp(opts) {
4042
4407
  endLine: anchored.endLine,
4043
4408
  codeSnippet: anchored.snippet,
4044
4409
  anchor: anchored.anchor,
4045
- status: existing.exportedAt ? "exported" : "active"
4410
+ status: existing.exportedAt ? "exported" : "active",
4411
+ // Re-attaching in a view moves the comment to it.
4412
+ targetKey: key
4046
4413
  };
4047
4414
  }
4048
4415
  const updated = await store.update((s) => {
4049
- const t = ensureTarget(s, key);
4416
+ const t = ensureTarget(s, scope);
4050
4417
  const idx = t.comments.findIndex((x) => x.id === id);
4051
4418
  if (idx < 0) throw notFound("comment not found");
4052
4419
  const next = { ...t.comments[idx], ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -4059,7 +4426,7 @@ function createApp(opts) {
4059
4426
  const key = decodeKey(c.req.param("key"));
4060
4427
  const id = c.req.param("id");
4061
4428
  const removed = await store.update((s) => {
4062
- const t = s.targets[key];
4429
+ const t = s.targets[commentScopeKey(key)];
4063
4430
  if (!t) return false;
4064
4431
  const before = t.comments.length;
4065
4432
  t.comments = t.comments.filter((x) => x.id !== id);
@@ -4072,30 +4439,88 @@ function createApp(opts) {
4072
4439
  api.post("/targets/:key/comments/reanchor", async (c) => {
4073
4440
  const key = decodeKey(c.req.param("key"));
4074
4441
  const ctx = await targetCtx(key);
4442
+ const scope = commentScopeKey(key);
4443
+ const local = isLocalTarget(ctx.target);
4075
4444
  const body = await c.req.json().catch(() => ({})) ?? {};
4076
4445
  const provided = /* @__PURE__ */ new Map();
4077
4446
  for (const f of body.files ?? []) if (f && typeof f.path === "string" && Array.isArray(f.hunks)) provided.set(f.path, f);
4078
- const state = await store.load();
4079
- const comments = state.targets[key]?.comments ?? [];
4080
- const paths = [...new Set(comments.map((x) => x.filePath))];
4447
+ const views = local ? localViewKeys(key) : [key];
4448
+ const contexts = /* @__PURE__ */ new Map([[key, ctx]]);
4081
4449
  const diffs = /* @__PURE__ */ new Map();
4450
+ const diffFor = (view, filePath) => {
4451
+ let byPath = diffs.get(view);
4452
+ if (!byPath) {
4453
+ byPath = /* @__PURE__ */ new Map();
4454
+ diffs.set(view, byPath);
4455
+ }
4456
+ let pending = byPath.get(filePath);
4457
+ if (!pending) {
4458
+ pending = (async () => {
4459
+ if (view === key && provided.has(filePath)) return provided.get(filePath);
4460
+ try {
4461
+ let viewCtx = contexts.get(view);
4462
+ if (!viewCtx) {
4463
+ viewCtx = await targetCtx(view);
4464
+ contexts.set(view, viewCtx);
4465
+ }
4466
+ return await fileDiffWithHints(viewCtx, filePath);
4467
+ } catch {
4468
+ return void 0;
4469
+ }
4470
+ })();
4471
+ byPath.set(filePath, pending);
4472
+ }
4473
+ return pending;
4474
+ };
4475
+ const state = await store.load();
4476
+ const comments = state.targets[scope]?.comments ?? [];
4477
+ const prevHead = state.targets[scope]?.head;
4478
+ const head = await revParse(ctx.cwd, "HEAD") ?? "";
4479
+ const committed = local && prevHead !== void 0 && prevHead !== head;
4480
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4481
+ const located = /* @__PURE__ */ new Map();
4082
4482
  await Promise.all(
4083
- paths.map(async (p) => {
4084
- if (provided.has(p)) {
4085
- diffs.set(p, provided.get(p));
4483
+ comments.map(async (cm) => {
4484
+ const ordered = views.includes(cm.targetKey) ? [cm.targetKey, ...views.filter((v) => v !== cm.targetKey)] : views;
4485
+ for (const view of ordered) {
4486
+ const diff = await diffFor(view, cm.filePath);
4487
+ if (!diff) continue;
4488
+ const next = reanchorComment(cm, diff, now);
4489
+ if (next.status === "orphaned") continue;
4490
+ located.set(cm.id, {
4491
+ side: next.side,
4492
+ startLine: next.startLine,
4493
+ endLine: next.endLine,
4494
+ codeSnippet: next.codeSnippet,
4495
+ anchor: next.anchor,
4496
+ status: next.status,
4497
+ targetKey: view
4498
+ });
4086
4499
  return;
4087
4500
  }
4088
- try {
4089
- diffs.set(p, await fileDiffWithHints(ctx, p));
4090
- } catch {
4091
- diffs.set(p, void 0);
4092
- }
4093
4501
  })
4094
4502
  );
4095
- const now = (/* @__PURE__ */ new Date()).toISOString();
4096
4503
  const result = await store.update((s) => {
4097
- const t = ensureTarget(s, key);
4098
- t.comments = t.comments.map((cm) => reanchorComment(cm, diffs.get(cm.filePath), now));
4504
+ const t = ensureTarget(s, scope);
4505
+ const dropped = [];
4506
+ t.comments = t.comments.flatMap((cm) => {
4507
+ const patch = located.get(cm.id);
4508
+ if (!patch) {
4509
+ if (!comments.some((x) => x.id === cm.id)) return [cm];
4510
+ if (committed) {
4511
+ dropped.push(cm.id);
4512
+ return [];
4513
+ }
4514
+ return [cm.status === "orphaned" ? cm : { ...cm, status: "orphaned", updatedAt: now }];
4515
+ }
4516
+ const next = { ...cm, ...patch };
4517
+ const unchanged = cm.targetKey === next.targetKey && cm.status === next.status && cm.startLine === next.startLine && cm.endLine === next.endLine && cm.anchor.hunkHash === next.anchor.hunkHash;
4518
+ return [unchanged ? cm : { ...next, updatedAt: now }];
4519
+ });
4520
+ for (const id of dropped) {
4521
+ for (const issue of s.issues) issue.commentIds = issue.commentIds.filter((x) => x !== id);
4522
+ }
4523
+ t.head = head;
4099
4524
  return t.comments;
4100
4525
  });
4101
4526
  const res = { comments: result };
@@ -4194,6 +4619,73 @@ function createApp(opts) {
4194
4619
  });
4195
4620
  return c.json(res);
4196
4621
  });
4622
+ const knownRoot = async (rootParam) => {
4623
+ if (!rootParam) return repo.root;
4624
+ if (rootParam === repo.root || rootParam === repo.commonRoot) return rootParam;
4625
+ if ((await worktrees()).some((w) => w.path === rootParam)) return rootParam;
4626
+ throw badRequest(`unknown root: ${rootParam}`, "unknown_worktree");
4627
+ };
4628
+ api.get("/todos", async (c) => {
4629
+ const branch = c.req.query("branch") || void 0;
4630
+ const s = await store.load();
4631
+ const res = {
4632
+ todos: branch ? s.todos.filter((t) => t.branch === branch) : s.todos,
4633
+ ...branch ? { branch } : {}
4634
+ };
4635
+ return c.json(res);
4636
+ });
4637
+ api.post("/todos", async (c) => {
4638
+ const body = await c.req.json();
4639
+ if (!body.title || !body.title.trim()) throw badRequest("title is required");
4640
+ const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
4641
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4642
+ const todo = {
4643
+ id: randomUUID(),
4644
+ branch,
4645
+ title: body.title.trim(),
4646
+ body: typeof body.body === "string" ? body.body : "",
4647
+ status: "open",
4648
+ createdAt: now,
4649
+ updatedAt: now
4650
+ };
4651
+ await store.update((s) => {
4652
+ s.todos.push(todo);
4653
+ });
4654
+ return c.json(todo, 201);
4655
+ });
4656
+ api.post("/todos/export", async (c) => {
4657
+ const body = await c.req.json();
4658
+ const branch = typeof body.branch === "string" ? body.branch.trim() : "";
4659
+ if (!branch) throw badRequest("branch is required");
4660
+ const s = await store.load();
4661
+ const todos = s.todos.filter((t) => t.branch === branch && (body.includeDone === true || t.status === "open"));
4662
+ const res = { text: formatTodosExport({ repoRoot: repo.root, branch, todos }), count: todos.length };
4663
+ return c.json(res);
4664
+ });
4665
+ api.patch("/todos/:id", async (c) => {
4666
+ const id = c.req.param("id");
4667
+ const body = await c.req.json();
4668
+ const updated = await store.update((s) => {
4669
+ const todo = s.todos.find((t) => t.id === id);
4670
+ if (!todo) throw notFound("todo not found");
4671
+ if (typeof body.title === "string" && body.title.trim()) todo.title = body.title.trim();
4672
+ if (typeof body.body === "string") todo.body = body.body;
4673
+ if (body.status === "open" || body.status === "done") todo.status = body.status;
4674
+ todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4675
+ return todo;
4676
+ });
4677
+ return c.json(updated);
4678
+ });
4679
+ api.delete("/todos/:id", async (c) => {
4680
+ const id = c.req.param("id");
4681
+ const removed = await store.update((s) => {
4682
+ const before = s.todos.length;
4683
+ s.todos = s.todos.filter((t) => t.id !== id);
4684
+ return s.todos.length !== before;
4685
+ });
4686
+ if (!removed) throw notFound("todo not found");
4687
+ return c.json({ ok: true });
4688
+ });
4197
4689
  api.get("/commits", async (c) => {
4198
4690
  const limitRaw = Number(c.req.query("limit") ?? 200);
4199
4691
  const limit = Math.min(Math.max(Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 200, 1), 1e3);
@@ -4231,6 +4723,33 @@ function createApp(opts) {
4231
4723
  const res = { commits: commits.slice(0, limit), hasMore };
4232
4724
  return c.json(res);
4233
4725
  });
4726
+ api.get("/events", async (c) => {
4727
+ const root = c.req.query("root") || repo.root;
4728
+ const known = root === repo.root || root === repo.commonRoot || (await worktrees()).some((w) => w.path === root);
4729
+ if (!known) throw badRequest(`unknown root: ${root}`, "unknown_worktree");
4730
+ return streamSSE(c, async (stream2) => {
4731
+ const watcher = watcherFor(root);
4732
+ let chain = Promise.resolve();
4733
+ const enqueue = (write) => {
4734
+ chain = chain.then(write).catch(() => stream2.abort());
4735
+ return chain;
4736
+ };
4737
+ const release = watcher.subscribe((change) => {
4738
+ const event = { type: "changed", ...change };
4739
+ void enqueue(() => stream2.writeSSE({ event: "changed", data: JSON.stringify(event) }));
4740
+ });
4741
+ stream2.onAbort(release);
4742
+ try {
4743
+ while (!stream2.aborted && !stream2.closed) {
4744
+ await stream2.sleep(SSE_HEARTBEAT_MS);
4745
+ if (stream2.aborted || stream2.closed) break;
4746
+ await enqueue(() => stream2.write(": ping\n\n"));
4747
+ }
4748
+ } finally {
4749
+ release();
4750
+ }
4751
+ });
4752
+ });
4234
4753
  api.get("/nvim/instances", async (c) => {
4235
4754
  const root = c.req.query("root") || repo.root;
4236
4755
  const force = c.req.query("rescan") === "1";
@@ -4255,7 +4774,7 @@ function createApp(opts) {
4255
4774
  api.post("/nvim/open", async (c) => {
4256
4775
  const body = await c.req.json();
4257
4776
  if (!body.socket || !body.absPath) throw badRequest("socket and absPath are required");
4258
- if (!path6.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
4777
+ if (!path7.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
4259
4778
  const scan = await nvim.scan();
4260
4779
  if (!scan.instances.some((i) => i.socket === body.socket)) {
4261
4780
  const fresh = await nvim.scan(true);
@@ -4283,6 +4802,37 @@ function createApp(opts) {
4283
4802
  return app;
4284
4803
  }
4285
4804
 
4805
+ // packages/server/src/wsl.ts
4806
+ import { execFile as execFile3 } from "child_process";
4807
+ import { readFile as readFile4 } from "fs/promises";
4808
+ var PROBE_TIMEOUT_MS = 3e3;
4809
+ async function isWsl(env = process.env, platform = process.platform) {
4810
+ if (platform !== "linux") return false;
4811
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
4812
+ try {
4813
+ return /microsoft/i.test(await readFile4("/proc/sys/kernel/osrelease", "utf8"));
4814
+ } catch {
4815
+ return false;
4816
+ }
4817
+ }
4818
+ function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
4819
+ return new Promise((resolve) => {
4820
+ execFile3(
4821
+ "curl.exe",
4822
+ ["--silent", "--max-time", String(Math.ceil(timeoutMs / 1e3)), `http://127.0.0.1:${port}/api/ping`],
4823
+ { timeout: timeoutMs + 1e3, encoding: "utf8", windowsHide: true },
4824
+ (error, stdout) => {
4825
+ const code = error?.code;
4826
+ if (typeof code === "string") {
4827
+ resolve(void 0);
4828
+ return;
4829
+ }
4830
+ resolve(stdout.trim() === token);
4831
+ }
4832
+ );
4833
+ });
4834
+ }
4835
+
4286
4836
  // packages/server/src/index.ts
4287
4837
  var HOST = "127.0.0.1";
4288
4838
  var DEFAULT_PORT_START = 4100;
@@ -4301,35 +4851,56 @@ async function findFreePort(start = DEFAULT_PORT_START, host = HOST) {
4301
4851
  }
4302
4852
  throw new Error(`no free port found starting at ${start}`);
4303
4853
  }
4854
+ var PROBE_ATTEMPTS = 5;
4304
4855
  async function startServer(opts) {
4305
4856
  const repo = await resolveRepo(opts.repoPath);
4306
4857
  const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
4307
4858
  const store = new StateStore(stateFile, repo.commonRoot);
4308
- const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir });
4859
+ const instanceToken = randomUUID2();
4860
+ const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken });
4861
+ const listen = (p) => new Promise((resolve, reject) => {
4862
+ const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
4863
+ s.once("error", reject);
4864
+ });
4865
+ const stop = (s) => new Promise((resolve, reject) => {
4866
+ s.close((err) => err ? reject(err) : resolve());
4867
+ if ("closeAllConnections" in s) s.closeAllConnections();
4868
+ });
4309
4869
  let port;
4870
+ let server;
4871
+ const skippedPorts = [];
4872
+ let windowsReachable;
4310
4873
  if (opts.port !== void 0) {
4311
4874
  if (!await isPortFree(opts.port)) throw new Error(`port ${opts.port} is already in use`);
4312
4875
  port = opts.port;
4876
+ server = await listen(port);
4313
4877
  } else {
4314
- port = await findFreePort();
4878
+ const probe = opts.probeWindows ?? (await isWsl() ? reachableFromWindows : void 0);
4879
+ let start = DEFAULT_PORT_START;
4880
+ for (let attempt = 1; ; attempt++) {
4881
+ port = await findFreePort(start);
4882
+ server = await listen(port);
4883
+ if (!probe) break;
4884
+ windowsReachable = await probe(port, instanceToken);
4885
+ if (windowsReachable !== false || attempt >= PROBE_ATTEMPTS) break;
4886
+ await stop(server);
4887
+ skippedPorts.push(port);
4888
+ start = port + 1;
4889
+ }
4315
4890
  }
4316
- const server = await new Promise((resolve, reject) => {
4317
- const s = serve({ fetch: app.fetch, hostname: HOST, port }, () => resolve(s));
4318
- s.once("error", reject);
4319
- });
4320
4891
  return {
4321
4892
  url: `http://${HOST}:${port}/`,
4322
4893
  port,
4323
4894
  repo,
4324
4895
  stateFile,
4325
- close: () => new Promise((resolve, reject) => {
4326
- server.close((err) => err ? reject(err) : resolve());
4327
- })
4896
+ skippedPorts,
4897
+ windowsReachable,
4898
+ close: () => stop(server)
4328
4899
  };
4329
4900
  }
4330
4901
 
4331
4902
  // bin/cli.ts
4332
- var VERSION = true ? "0.1.0" : "dev";
4903
+ var VERSION = true ? "0.3.0" : "dev";
4333
4904
  function parseArgs(argv) {
4334
4905
  const args = { repoPath: process.cwd(), open: true, help: false, version: false };
4335
4906
  for (let i = 0; i < argv.length; i++) {
@@ -4347,7 +4918,7 @@ function parseArgs(argv) {
4347
4918
  } else if (a === "--help" || a === "-h") args.help = true;
4348
4919
  else if (a === "--version" || a === "-v") args.version = true;
4349
4920
  else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
4350
- else args.repoPath = path7.resolve(a);
4921
+ else args.repoPath = path8.resolve(a);
4351
4922
  }
4352
4923
  return args;
4353
4924
  }
@@ -4357,7 +4928,8 @@ function usage() {
4357
4928
  Usage: warden [repoPath] [options]
4358
4929
 
4359
4930
  Options:
4360
- --port <n>, -p <n> Listen on a fixed port (default: first free port from 4100)
4931
+ --port <n>, -p <n> Listen on a fixed port (default: first free port from 4100, and under WSL
4932
+ the first one the Windows side can reach)
4361
4933
  --no-open Do not try to open the browser
4362
4934
  -h, --help Show this help
4363
4935
  -v, --version Print version
@@ -4389,16 +4961,16 @@ async function openBrowser(url) {
4389
4961
  return false;
4390
4962
  }
4391
4963
  function resolveWebDir() {
4392
- const here = path7.dirname(fileURLToPath(import.meta.url));
4964
+ const here = path8.dirname(fileURLToPath(import.meta.url));
4393
4965
  const candidates = [
4394
4966
  process.env.WARDEN_WEB_DIR,
4395
- path7.join(here, "web"),
4967
+ path8.join(here, "web"),
4396
4968
  // dist/cli.js -> dist/web
4397
- path7.join(here, "..", "dist", "web")
4969
+ path8.join(here, "..", "dist", "web")
4398
4970
  // bin/cli.ts (tsx dev) -> dist/web
4399
4971
  ].filter((p) => !!p);
4400
4972
  for (const dir of candidates) {
4401
- if (existsSync(path7.join(dir, "index.html"))) return dir;
4973
+ if (existsSync(path8.join(dir, "index.html"))) return dir;
4402
4974
  }
4403
4975
  return void 0;
4404
4976
  }
@@ -4434,6 +5006,14 @@ async function main() {
4434
5006
  console.log(` repo: ${server.repo.root}`);
4435
5007
  console.log(` state: ${server.stateFile}`);
4436
5008
  console.log(` url: ${server.url}`);
5009
+ if (server.skippedPorts.length > 0) {
5010
+ const ports = server.skippedPorts.join(", ");
5011
+ const plural = server.skippedPorts.length > 1 ? "s" : "";
5012
+ console.log(` note: skipped port${plural} ${ports} \u2014 the WSL2 localhost relay does not publish ${plural ? "them" : "it"} to Windows`);
5013
+ }
5014
+ if (server.windowsReachable === false) {
5015
+ console.error(`warning: Windows cannot reach ${server.url} either. Run with --port <n> to pick a port it can publish.`);
5016
+ }
4437
5017
  if (args.open) {
4438
5018
  const opened = await openBrowser(server.url);
4439
5019
  if (!opened) console.log(" (could not open a browser automatically; open the URL manually)");