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