@xbghc/warden 0.7.0 → 0.8.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 path8 from "path";
4
+ import path9 from "path";
5
5
  import { spawn } from "child_process";
6
6
  import { existsSync as existsSync2 } from "fs";
7
7
  import { fileURLToPath } from "url";
@@ -652,7 +652,7 @@ var serve = (options, listeningListener) => {
652
652
  };
653
653
 
654
654
  // packages/server/src/app.ts
655
- import path7 from "path";
655
+ import path8 from "path";
656
656
  import { randomUUID } from "crypto";
657
657
 
658
658
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
@@ -812,26 +812,26 @@ var throwNestingLimitExceeded = () => {
812
812
  };
813
813
 
814
814
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
815
- var splitPath = (path9) => {
816
- const paths = path9.split("/");
815
+ var splitPath = (path10) => {
816
+ const paths = path10.split("/");
817
817
  if (paths[0] === "") {
818
818
  paths.shift();
819
819
  }
820
820
  return paths;
821
821
  };
822
822
  var splitRoutingPath = (routePath) => {
823
- const { groups, path: path9 } = extractGroupsFromPath(routePath);
824
- const paths = splitPath(path9);
823
+ const { groups, path: path10 } = extractGroupsFromPath(routePath);
824
+ const paths = splitPath(path10);
825
825
  return replaceGroupMarks(paths, groups);
826
826
  };
827
- var extractGroupsFromPath = (path9) => {
827
+ var extractGroupsFromPath = (path10) => {
828
828
  const groups = [];
829
- path9 = path9.replace(/\{[^}]+\}/g, (match2, index) => {
829
+ path10 = path10.replace(/\{[^}]+\}/g, (match2, index) => {
830
830
  const mark = `@${index}`;
831
831
  groups.push([mark, match2]);
832
832
  return mark;
833
833
  });
834
- return { groups, path: path9 };
834
+ return { groups, path: path10 };
835
835
  };
836
836
  var replaceGroupMarks = (paths, groups) => {
837
837
  for (let i = groups.length - 1; i >= 0; i--) {
@@ -888,8 +888,8 @@ var getPath = (request) => {
888
888
  const queryIndex = url.indexOf("?", i);
889
889
  const hashIndex = url.indexOf("#", i);
890
890
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
891
- const path9 = url.slice(start, end);
892
- return tryDecodeURI(path9.includes("%25") ? path9.replace(/%25/g, "%2525") : path9);
891
+ const path10 = url.slice(start, end);
892
+ return tryDecodeURI(path10.includes("%25") ? path10.replace(/%25/g, "%2525") : path10);
893
893
  } else if (charCode === 63 || charCode === 35) {
894
894
  break;
895
895
  }
@@ -906,11 +906,11 @@ var mergePath = (base, sub, ...rest) => {
906
906
  }
907
907
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
908
908
  };
909
- var checkOptionalParameter = (path9) => {
910
- if (path9.charCodeAt(path9.length - 1) !== 63 || !path9.includes(":")) {
909
+ var checkOptionalParameter = (path10) => {
910
+ if (path10.charCodeAt(path10.length - 1) !== 63 || !path10.includes(":")) {
911
911
  return null;
912
912
  }
913
- const segments = path9.split("/");
913
+ const segments = path10.split("/");
914
914
  const results = [];
915
915
  let basePath = "";
916
916
  segments.forEach((segment) => {
@@ -1052,9 +1052,9 @@ var HonoRequest = class {
1052
1052
  */
1053
1053
  path;
1054
1054
  bodyCache = {};
1055
- constructor(request, path9 = "/", matchResult = [[]]) {
1055
+ constructor(request, path10 = "/", matchResult = [[]]) {
1056
1056
  this.raw = request;
1057
- this.path = path9;
1057
+ this.path = path10;
1058
1058
  this.#matchResult = matchResult;
1059
1059
  }
1060
1060
  param(key) {
@@ -1826,8 +1826,8 @@ var Hono = class _Hono {
1826
1826
  return this;
1827
1827
  };
1828
1828
  });
1829
- this.on = (method, path9, ...handlers) => {
1830
- for (const p of [path9].flat()) {
1829
+ this.on = (method, path10, ...handlers) => {
1830
+ for (const p of [path10].flat()) {
1831
1831
  this.#path = p;
1832
1832
  for (const m of [method].flat()) {
1833
1833
  const methodName = m.toUpperCase();
@@ -1885,8 +1885,8 @@ var Hono = class _Hono {
1885
1885
  * app.route("/api", app2) // GET /api/user
1886
1886
  * ```
1887
1887
  */
1888
- route(path9, app) {
1889
- const subApp = this.basePath(path9);
1888
+ route(path10, app) {
1889
+ const subApp = this.basePath(path10);
1890
1890
  app.routes.map((r) => {
1891
1891
  let handler;
1892
1892
  if (app.errorHandler === errorHandler) {
@@ -1912,9 +1912,9 @@ var Hono = class _Hono {
1912
1912
  * const api = new Hono().basePath('/api')
1913
1913
  * ```
1914
1914
  */
1915
- basePath(path9) {
1915
+ basePath(path10) {
1916
1916
  const subApp = this.#clone();
1917
- subApp._basePath = mergePath(this._basePath, path9);
1917
+ subApp._basePath = mergePath(this._basePath, path10);
1918
1918
  return subApp;
1919
1919
  }
1920
1920
  /**
@@ -1988,7 +1988,7 @@ var Hono = class _Hono {
1988
1988
  * })
1989
1989
  * ```
1990
1990
  */
1991
- mount(path9, applicationHandler, options) {
1991
+ mount(path10, applicationHandler, options) {
1992
1992
  let replaceRequest;
1993
1993
  let optionHandler;
1994
1994
  if (options) {
@@ -2015,7 +2015,7 @@ var Hono = class _Hono {
2015
2015
  return [c.env, executionContext];
2016
2016
  };
2017
2017
  replaceRequest ||= (() => {
2018
- const mergedPath = mergePath(this._basePath, path9);
2018
+ const mergedPath = mergePath(this._basePath, path10);
2019
2019
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
2020
2020
  return (request) => {
2021
2021
  const url = new URL(request.url);
@@ -2030,18 +2030,18 @@ var Hono = class _Hono {
2030
2030
  }
2031
2031
  await next();
2032
2032
  };
2033
- this.#addRoute(METHOD_NAME_ALL, mergePath(path9, "*"), handler);
2033
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path10, "*"), handler);
2034
2034
  return this;
2035
2035
  }
2036
- #addRoute(method, path9, handler, baseRoutePath) {
2037
- path9 = mergePath(this._basePath, path9);
2036
+ #addRoute(method, path10, handler, baseRoutePath) {
2037
+ path10 = mergePath(this._basePath, path10);
2038
2038
  const r = {
2039
2039
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
2040
- path: path9,
2040
+ path: path10,
2041
2041
  method,
2042
2042
  handler
2043
2043
  };
2044
- this.router.add(method, path9, [handler, r]);
2044
+ this.router.add(method, path10, [handler, r]);
2045
2045
  this.routes.push(r);
2046
2046
  }
2047
2047
  #handleError(err, c) {
@@ -2054,10 +2054,10 @@ var Hono = class _Hono {
2054
2054
  if (method === "HEAD") {
2055
2055
  return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
2056
2056
  }
2057
- const path9 = this.getPath(request, { env });
2058
- const matchResult = this.router.match(method, path9);
2057
+ const path10 = this.getPath(request, { env });
2058
+ const matchResult = this.router.match(method, path10);
2059
2059
  const c = new Context(request, {
2060
- path: path9,
2060
+ path: path10,
2061
2061
  matchResult,
2062
2062
  env,
2063
2063
  executionCtx,
@@ -2160,7 +2160,7 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
2160
2160
 
2161
2161
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
2162
2162
  var emptyParam = [];
2163
- function match(method, path9) {
2163
+ function match(method, path10) {
2164
2164
  const matchers = this.buildAllMatchers();
2165
2165
  const match2 = ((method2, path22) => {
2166
2166
  const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
@@ -2176,7 +2176,7 @@ function match(method, path9) {
2176
2176
  return [matcher[1][index], match3];
2177
2177
  });
2178
2178
  this.match = match2;
2179
- return match2(method, path9);
2179
+ return match2(method, path10);
2180
2180
  }
2181
2181
 
2182
2182
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -2293,14 +2293,14 @@ var Trie = class {
2293
2293
  #index = 0;
2294
2294
  // dynamic path -> [handler index, param assoc]; static paths are not registered
2295
2295
  paths = createNullObject();
2296
- insert(path9, isStatic) {
2296
+ insert(path10, isStatic) {
2297
2297
  if (isStatic) {
2298
- this.#root.insert(path9.split(""), 0, [], this.#context, true);
2298
+ this.#root.insert(path10.split(""), 0, [], this.#context, true);
2299
2299
  return;
2300
2300
  }
2301
2301
  const paramAssoc = [];
2302
2302
  const groups = [];
2303
- let markedPath = path9;
2303
+ let markedPath = path10;
2304
2304
  for (let i = 0; ; ) {
2305
2305
  let replaced = false;
2306
2306
  markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
@@ -2325,7 +2325,7 @@ var Trie = class {
2325
2325
  }
2326
2326
  }
2327
2327
  this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
2328
- this.paths[path9] = [this.#index++, paramAssoc];
2328
+ this.paths[path10] = [this.#index++, paramAssoc];
2329
2329
  }
2330
2330
  buildRegExp() {
2331
2331
  let regexp = this.#root.buildRegExpStr();
@@ -2352,17 +2352,17 @@ var Trie = class {
2352
2352
 
2353
2353
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
2354
2354
  var wildcardRegExpCache = createNullObject();
2355
- function buildWildcardRegExp(path9) {
2356
- return wildcardRegExpCache[path9] ??= new RegExp(
2357
- `^${path9.replace(
2355
+ function buildWildcardRegExp(path10) {
2356
+ return wildcardRegExpCache[path10] ??= new RegExp(
2357
+ `^${path10.replace(
2358
2358
  /\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g,
2359
2359
  (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`
2360
2360
  )}$`
2361
2361
  );
2362
2362
  }
2363
- function findMiddleware(middleware, path9) {
2363
+ function findMiddleware(middleware, path10) {
2364
2364
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
2365
- if (buildWildcardRegExp(k).test(path9)) {
2365
+ if (buildWildcardRegExp(k).test(path10)) {
2366
2366
  return [...middleware[k]];
2367
2367
  }
2368
2368
  }
@@ -2378,14 +2378,14 @@ var RegExpRouter = class {
2378
2378
  this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
2379
2379
  this.#tries = { [METHOD_NAME_ALL]: new Trie() };
2380
2380
  }
2381
- #insertPath(method, path9) {
2381
+ #insertPath(method, path10) {
2382
2382
  try {
2383
- this.#tries[method].insert(path9, !/\*|\/:/.test(path9));
2383
+ this.#tries[method].insert(path10, !/\*|\/:/.test(path10));
2384
2384
  } catch (e) {
2385
- throw e === PATH_ERROR ? new UnsupportedPathError(path9) : e;
2385
+ throw e === PATH_ERROR ? new UnsupportedPathError(path10) : e;
2386
2386
  }
2387
2387
  }
2388
- add(method, path9, handler) {
2388
+ add(method, path10, handler) {
2389
2389
  const middleware = this.#middleware;
2390
2390
  const routes = this.#routes;
2391
2391
  if (!middleware) {
@@ -2401,28 +2401,28 @@ var RegExpRouter = class {
2401
2401
  }
2402
2402
  }
2403
2403
  }
2404
- if (path9 === "/*") {
2405
- path9 = "*";
2404
+ if (path10 === "/*") {
2405
+ path10 = "*";
2406
2406
  }
2407
2407
  const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
2408
- if (/\*$/.test(path9)) {
2409
- const re = buildWildcardRegExp(path9);
2408
+ if (/\*$/.test(path10)) {
2409
+ const re = buildWildcardRegExp(path10);
2410
2410
  for (const m of methods) {
2411
- if (!middleware[m][path9]) {
2412
- this.#insertPath(m, path9);
2413
- middleware[m][path9] = findMiddleware(middleware[m], path9) || findMiddleware(middleware[METHOD_NAME_ALL], path9) || [];
2411
+ if (!middleware[m][path10]) {
2412
+ this.#insertPath(m, path10);
2413
+ middleware[m][path10] = findMiddleware(middleware[m], path10) || findMiddleware(middleware[METHOD_NAME_ALL], path10) || [];
2414
2414
  }
2415
2415
  }
2416
2416
  for (const handlerMap of [middleware, routes]) {
2417
2417
  for (const m of methods) {
2418
2418
  for (const p in handlerMap[m]) {
2419
- re.test(p) && handlerMap[m][p].push([handler, path9]);
2419
+ re.test(p) && handlerMap[m][p].push([handler, path10]);
2420
2420
  }
2421
2421
  }
2422
2422
  }
2423
2423
  return;
2424
2424
  }
2425
- const paths = checkOptionalParameter(path9) || [path9];
2425
+ const paths = checkOptionalParameter(path10) || [path10];
2426
2426
  for (const path22 of paths) {
2427
2427
  for (const m of methods) {
2428
2428
  if (!routes[m][path22]) {
@@ -2451,11 +2451,11 @@ var RegExpRouter = class {
2451
2451
  const handlerData = [];
2452
2452
  const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
2453
2453
  for (const r of [middleware, routes]) {
2454
- for (const path9 in r) {
2455
- const handlers = r[path9];
2456
- const pathData = trie.paths[path9];
2454
+ for (const path10 in r) {
2455
+ const handlers = r[path10];
2456
+ const pathData = trie.paths[path10];
2457
2457
  if (!pathData) {
2458
- staticMap[path9] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2458
+ staticMap[path10] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2459
2459
  continue;
2460
2460
  }
2461
2461
  handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
@@ -2479,13 +2479,13 @@ var SmartRouter = class {
2479
2479
  constructor(init) {
2480
2480
  this.#routers = init.routers;
2481
2481
  }
2482
- add(method, path9, handler) {
2482
+ add(method, path10, handler) {
2483
2483
  if (!this.#routes) {
2484
2484
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
2485
2485
  }
2486
- this.#routes.push([method, path9, handler]);
2486
+ this.#routes.push([method, path10, handler]);
2487
2487
  }
2488
- match(method, path9) {
2488
+ match(method, path10) {
2489
2489
  if (!this.#routes) {
2490
2490
  throw new Error("Fatal error");
2491
2491
  }
@@ -2500,7 +2500,7 @@ var SmartRouter = class {
2500
2500
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
2501
2501
  router.add(...routes[i2]);
2502
2502
  }
2503
- res = router.match(method, path9);
2503
+ res = router.match(method, path10);
2504
2504
  } catch (e) {
2505
2505
  if (e instanceof UnsupportedPathError) {
2506
2506
  continue;
@@ -2535,9 +2535,9 @@ var Node2 = class _Node2 {
2535
2535
  #patterns = [];
2536
2536
  #pattern;
2537
2537
  #params = emptyParams;
2538
- insert(method, path9, handler) {
2538
+ insert(method, path10, handler) {
2539
2539
  let curNode = this;
2540
- const parts = splitRoutingPath(path9);
2540
+ const parts = splitRoutingPath(path10);
2541
2541
  const possibleKeys = /* @__PURE__ */ new Set();
2542
2542
  let i = 0;
2543
2543
  for (const p of parts) {
@@ -2577,12 +2577,12 @@ var Node2 = class _Node2 {
2577
2577
  }
2578
2578
  }
2579
2579
  }
2580
- search(method, path9) {
2580
+ search(method, path10) {
2581
2581
  const handlerSets = [];
2582
2582
  this.#params = emptyParams;
2583
2583
  const curNode = this;
2584
2584
  let curNodes = [curNode];
2585
- const parts = splitPath(path9);
2585
+ const parts = splitPath(path10);
2586
2586
  const curNodesQueue = [];
2587
2587
  const len = parts.length;
2588
2588
  let partOffsets = null;
@@ -2624,13 +2624,13 @@ var Node2 = class _Node2 {
2624
2624
  if (matcher !== true) {
2625
2625
  if (!partOffsets) {
2626
2626
  partOffsets = [];
2627
- let offset = path9[0] === "/" ? 1 : 0;
2627
+ let offset = path10[0] === "/" ? 1 : 0;
2628
2628
  for (let p = 0; p < len; p++) {
2629
2629
  partOffsets[p] = offset;
2630
2630
  offset += parts[p].length + 1;
2631
2631
  }
2632
2632
  }
2633
- const restPathString = path9.slice(partOffsets[i]);
2633
+ const restPathString = path10.slice(partOffsets[i]);
2634
2634
  const m = matcher.exec(restPathString);
2635
2635
  if (m) {
2636
2636
  params[name] = m[0];
@@ -2690,13 +2690,13 @@ var Node2 = class _Node2 {
2690
2690
  var TrieRouter = class {
2691
2691
  name = "TrieRouter";
2692
2692
  #node = new Node2();
2693
- add(method, path9, handler) {
2694
- for (const result of checkOptionalParameter(path9) || [path9]) {
2693
+ add(method, path10, handler) {
2694
+ for (const result of checkOptionalParameter(path10) || [path10]) {
2695
2695
  this.#node.insert(method, result, handler);
2696
2696
  }
2697
2697
  }
2698
- match(method, path9) {
2699
- return this.#node.search(method, path9);
2698
+ match(method, path10) {
2699
+ return this.#node.search(method, path10);
2700
2700
  }
2701
2701
  };
2702
2702
 
@@ -3049,7 +3049,7 @@ function parseCommitLog(stdout) {
3049
3049
  // packages/server/src/git.ts
3050
3050
  import { execFile } from "child_process";
3051
3051
  import { existsSync } from "fs";
3052
- var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base", "rev-list"]);
3052
+ var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base", "rev-list", "for-each-ref", "check-ref-format"]);
3053
3053
  var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
3054
3054
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
3055
3055
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -3190,6 +3190,45 @@ async function applyOnce(cwd, patch, opts) {
3190
3190
  child.stdin?.end(patch);
3191
3191
  });
3192
3192
  }
3193
+ async function runGitWrite(args, opts) {
3194
+ for (const arg of args) if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
3195
+ return new Promise((resolve, reject) => {
3196
+ execFile(
3197
+ "git",
3198
+ [...args],
3199
+ {
3200
+ cwd: opts.cwd,
3201
+ timeout: opts.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
3202
+ maxBuffer: 16 * 1024 * 1024,
3203
+ encoding: "utf8",
3204
+ env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
3205
+ windowsHide: true
3206
+ },
3207
+ (error, stdout, stderr) => {
3208
+ const err = error;
3209
+ if (!err) {
3210
+ resolve({ stdout, stderr, code: 0 });
3211
+ return;
3212
+ }
3213
+ if (err.killed || err.signal === "SIGTERM") {
3214
+ reject(new GitError(`git ${args[0]} timed out`, null, stderr, 504, "git_timeout"));
3215
+ return;
3216
+ }
3217
+ if (err.code === "ENOENT") {
3218
+ if (!existsSync(opts.cwd)) {
3219
+ reject(new GitError(`working directory no longer exists: ${opts.cwd}`, null, "", 400, "unknown_worktree"));
3220
+ return;
3221
+ }
3222
+ reject(new GitError("git executable not found in PATH", null, "", 500, "git_missing"));
3223
+ return;
3224
+ }
3225
+ const code = typeof err.code === "number" ? err.code : -1;
3226
+ const first = stderr.split("\n").find((l) => l.trim()) ?? err.message;
3227
+ reject(new GitError(first.trim().replace(/^(fatal|error): /, ""), code, stderr, 500, "git_failed"));
3228
+ }
3229
+ );
3230
+ });
3231
+ }
3193
3232
  async function refExists(cwd, ref) {
3194
3233
  try {
3195
3234
  await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { cwd });
@@ -3240,19 +3279,20 @@ async function resolveRepo(dir) {
3240
3279
  const commonRoot = path.basename(commonDir) === ".git" ? path.dirname(commonDir) : root;
3241
3280
  return { root, commonRoot };
3242
3281
  }
3243
- async function listWorktrees(ctx) {
3282
+ async function listWorktreesAll(ctx) {
3244
3283
  const r = await runGit(["worktree", "list", "--porcelain"], { cwd: ctx.root });
3245
3284
  const out = [];
3246
3285
  let cur = null;
3247
3286
  const flush = () => {
3248
- if (cur && cur.path && !cur.prunable) {
3287
+ if (cur && cur.path) {
3249
3288
  out.push({
3250
3289
  path: cur.path,
3251
3290
  head: cur.head ?? "",
3252
3291
  branch: cur.branch,
3253
3292
  isMain: false,
3254
3293
  detached: cur.detached ?? false,
3255
- bare: cur.bare ?? false
3294
+ bare: cur.bare ?? false,
3295
+ prunable: cur.prunable ?? false
3256
3296
  });
3257
3297
  }
3258
3298
  cur = null;
@@ -3280,6 +3320,9 @@ async function listWorktrees(ctx) {
3280
3320
  }
3281
3321
  return out;
3282
3322
  }
3323
+ async function listWorktrees(ctx) {
3324
+ return (await listWorktreesAll(ctx)).filter((w) => !w.prunable).map(({ prunable: _prunable, ...w }) => w);
3325
+ }
3283
3326
  async function currentBranch(cwd) {
3284
3327
  const res = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).catch(() => ({ stdout: "" }));
3285
3328
  const name = res.stdout.trim();
@@ -3927,20 +3970,137 @@ function buildStagePatch(diff, mode, selection) {
3927
3970
  return { patch: [...header, ...hunks].join("\n") + "\n", lines };
3928
3971
  }
3929
3972
 
3973
+ // packages/server/src/worktrees.ts
3974
+ import path3 from "path";
3975
+ import { readdir, realpath as realpath2, stat } from "fs/promises";
3976
+ function worktreePathPrefix(ctx) {
3977
+ return path3.join(path3.dirname(ctx.commonRoot), `${path3.basename(ctx.commonRoot)}-`);
3978
+ }
3979
+ async function dirtyCount(cwd) {
3980
+ try {
3981
+ const r = await runGit(["status", "--porcelain", "--untracked-files=all"], { cwd });
3982
+ return r.stdout.split("\n").filter((l) => l.trim()).length;
3983
+ } catch {
3984
+ return 0;
3985
+ }
3986
+ }
3987
+ async function listWorktreesDetailed(ctx) {
3988
+ const all = await listWorktreesAll(ctx);
3989
+ return Promise.all(all.map(async (w) => ({ ...w, dirty: w.prunable || w.bare ? 0 : await dirtyCount(w.path) })));
3990
+ }
3991
+ async function listBranches(ctx, worktrees) {
3992
+ const r = await runGit(["for-each-ref", "--format=%(refname:short)%09%(objectname:short)", "refs/heads"], { cwd: ctx.commonRoot });
3993
+ const at = /* @__PURE__ */ new Map();
3994
+ for (const w of worktrees) if (w.branch) at.set(w.branch, w.path);
3995
+ return r.stdout.split("\n").filter(Boolean).map((line) => {
3996
+ const [name = "", sha = ""] = line.split(" ");
3997
+ const worktree = at.get(name);
3998
+ return { name, sha, ...worktree ? { worktree } : {} };
3999
+ });
4000
+ }
4001
+ async function assertBranchName(ctx, name) {
4002
+ if (!isValidRef(name) || name.includes("@{") || name.startsWith("refs/")) throw badRequest(`invalid branch name: ${name}`, "invalid_branch");
4003
+ try {
4004
+ await runGit(["check-ref-format", "--branch", name], { cwd: ctx.commonRoot });
4005
+ } catch {
4006
+ throw badRequest(`invalid branch name: ${name}`, "invalid_branch");
4007
+ }
4008
+ }
4009
+ async function assertNewWorktreePath(ctx, worktrees, p) {
4010
+ if (!p || !path3.isAbsolute(p)) throw badRequest("worktree path must be absolute", "invalid_path");
4011
+ const target = path3.resolve(p);
4012
+ const parent = path3.dirname(ctx.commonRoot);
4013
+ if (target === parent || !target.startsWith(parent + path3.sep)) throw badRequest(`worktree path must be under ${parent}`, "invalid_path");
4014
+ for (const w of worktrees) {
4015
+ if (target === w.path || target.startsWith(w.path + path3.sep)) throw badRequest(`${target} is inside the worktree at ${w.path}`, "invalid_path");
4016
+ }
4017
+ let st;
4018
+ try {
4019
+ st = await stat(target);
4020
+ } catch (e) {
4021
+ if (e.code === "ENOENT") return target;
4022
+ throw e;
4023
+ }
4024
+ if (!st.isDirectory() || (await readdir(target)).length > 0) throw badRequest(`${target} already exists`, "path_exists");
4025
+ return target;
4026
+ }
4027
+ async function addWorktree(ctx, req) {
4028
+ const branch = req.branch.trim();
4029
+ const base = req.base?.trim();
4030
+ await assertBranchName(ctx, branch);
4031
+ const existing = await listWorktreesAll(ctx);
4032
+ const target = await assertNewWorktreePath(ctx, existing, req.path.trim());
4033
+ const cwd = ctx.commonRoot;
4034
+ const branchExists = await refExists(cwd, `refs/heads/${branch}`);
4035
+ let args;
4036
+ if (base) {
4037
+ if (!isValidRef(base)) throw badRequest(`invalid base ref: ${base}`, "invalid_ref");
4038
+ if (!await refExists(cwd, base)) throw badRequest(`unknown ref: ${base}`, "unknown_ref");
4039
+ if (branchExists) throw new HttpError(409, `branch ${branch} already exists`, "branch_exists");
4040
+ args = ["worktree", "add", "-b", branch, target, base];
4041
+ } else {
4042
+ if (!branchExists) throw badRequest(`unknown branch: ${branch}`, "unknown_ref");
4043
+ const holder = existing.find((w) => w.branch === branch);
4044
+ if (holder) throw new HttpError(409, `${branch} is already checked out at ${holder.path}`, "branch_in_use");
4045
+ args = ["worktree", "add", target, branch];
4046
+ }
4047
+ try {
4048
+ await runGitWrite(args, { cwd });
4049
+ } catch (e) {
4050
+ if (e instanceof GitError && /already (checked out|used by worktree)/.test(e.stderr)) throw new HttpError(409, e.message, "branch_in_use");
4051
+ throw e;
4052
+ }
4053
+ const real = await realpath2(target).catch(() => target);
4054
+ const made = (await listWorktrees(ctx)).find((w) => w.path === real);
4055
+ if (!made) throw new HttpError(500, `git created ${target} but does not list it`, "git_failed");
4056
+ return made;
4057
+ }
4058
+ async function removeWorktree(ctx, req) {
4059
+ const p = req.path.trim();
4060
+ const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path3.resolve(p));
4061
+ if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
4062
+ if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
4063
+ const cwd = ctx.commonRoot;
4064
+ const args = ["worktree", "remove"];
4065
+ if (req.force) args.push("--force");
4066
+ args.push(wt.path);
4067
+ try {
4068
+ await runGitWrite(args, { cwd });
4069
+ } catch (e) {
4070
+ if (e instanceof GitError && /contains modified or untracked files/.test(e.stderr)) {
4071
+ throw new HttpError(409, `${wt.path} has uncommitted changes`, "worktree_dirty");
4072
+ }
4073
+ if (e instanceof GitError && !req.force && /use --force|validation failed/.test(e.stderr)) {
4074
+ throw new HttpError(409, e.message, "needs_force");
4075
+ }
4076
+ throw e;
4077
+ }
4078
+ const res = { ok: true, branchDeleted: false };
4079
+ if (req.deleteBranch && wt.branch) {
4080
+ try {
4081
+ await runGitWrite(["branch", "-d", wt.branch], { cwd });
4082
+ res.branchDeleted = true;
4083
+ } catch (e) {
4084
+ res.branchError = e instanceof Error ? e.message : String(e);
4085
+ }
4086
+ }
4087
+ return res;
4088
+ }
4089
+
3930
4090
  // packages/server/src/state.ts
3931
4091
  import os from "os";
3932
- import path3 from "path";
3933
- import { mkdir, open, readFile as readFile2, rename, stat, unlink, writeFile } from "fs/promises";
4092
+ import path4 from "path";
4093
+ import { mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "fs/promises";
3934
4094
  function dataDir() {
3935
4095
  const xdg = process.env.XDG_DATA_HOME;
3936
- const base = xdg && xdg.trim() ? xdg : path3.join(os.homedir(), ".local", "share");
3937
- return path3.join(base, "warden");
4096
+ const base = xdg && xdg.trim() ? xdg : path4.join(os.homedir(), ".local", "share");
4097
+ return path4.join(base, "warden");
3938
4098
  }
3939
4099
  function repoHash(repoRoot) {
3940
4100
  return sha1(repoRoot).slice(0, 12);
3941
4101
  }
3942
4102
  function stateFilePath(repoRoot, baseDir = dataDir()) {
3943
- return path3.join(baseDir, repoHash(repoRoot), "state.json");
4103
+ return path4.join(baseDir, repoHash(repoRoot), "state.json");
3944
4104
  }
3945
4105
  function defaultPrefs() {
3946
4106
  return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true };
@@ -4035,7 +4195,7 @@ var StateStore = class {
4035
4195
  /** Apply a mutation under lock. The callback receives the freshest on-disk state. */
4036
4196
  update(fn) {
4037
4197
  const run2 = async () => {
4038
- await mkdir(path3.dirname(this.file), { recursive: true });
4198
+ await mkdir(path4.dirname(this.file), { recursive: true });
4039
4199
  const release = await this.acquireLock();
4040
4200
  try {
4041
4201
  const state = await this.load();
@@ -4073,7 +4233,7 @@ var StateStore = class {
4073
4233
  const err = e;
4074
4234
  if (err.code !== "EEXIST") throw e;
4075
4235
  try {
4076
- const st = await stat(lock);
4236
+ const st = await stat2(lock);
4077
4237
  if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
4078
4238
  await unlink(lock).catch(() => void 0);
4079
4239
  continue;
@@ -4170,9 +4330,9 @@ function formatIssueExport({ repoRoot, issue, comments }) {
4170
4330
 
4171
4331
  // packages/server/src/nvim.ts
4172
4332
  import os2 from "os";
4173
- import path4 from "path";
4333
+ import path5 from "path";
4174
4334
  import { execFile as execFile2 } from "child_process";
4175
- import { readdir, stat as stat2 } from "fs/promises";
4335
+ import { readdir as readdir2, stat as stat3 } from "fs/promises";
4176
4336
  var QUERY_TIMEOUT_MS = 500;
4177
4337
  var CACHE_TTL_MS = 1e4;
4178
4338
  function execNvim(args, timeoutMs) {
@@ -4201,14 +4361,14 @@ function socketDirs(env = process.env) {
4201
4361
  }
4202
4362
  }
4203
4363
  const tmp = env.TMPDIR || "/tmp";
4204
- if (user) dirs.push(path4.join(tmp, `nvim.${user}`));
4205
- if (env.NVIM_LISTEN_ADDRESS) dirs.push(path4.dirname(env.NVIM_LISTEN_ADDRESS));
4364
+ if (user) dirs.push(path5.join(tmp, `nvim.${user}`));
4365
+ if (env.NVIM_LISTEN_ADDRESS) dirs.push(path5.dirname(env.NVIM_LISTEN_ADDRESS));
4206
4366
  return [...new Set(dirs)];
4207
4367
  }
4208
4368
  var SOCKET_NAME = /^nvim\.(\d+)\.\d+$/;
4209
4369
  async function isSocket(p) {
4210
4370
  try {
4211
- return (await stat2(p)).isSocket();
4371
+ return (await stat3(p)).isSocket();
4212
4372
  } catch {
4213
4373
  return false;
4214
4374
  }
@@ -4218,12 +4378,12 @@ async function findSockets(dirs = socketDirs()) {
4218
4378
  const visit = async (dir, depth) => {
4219
4379
  let entries;
4220
4380
  try {
4221
- entries = await readdir(dir, { withFileTypes: true });
4381
+ entries = await readdir2(dir, { withFileTypes: true });
4222
4382
  } catch {
4223
4383
  return;
4224
4384
  }
4225
4385
  for (const ent of entries) {
4226
- const full = path4.join(dir, ent.name);
4386
+ const full = path5.join(dir, ent.name);
4227
4387
  const m = SOCKET_NAME.exec(ent.name);
4228
4388
  if (m && (ent.isSocket() || await isSocket(full))) {
4229
4389
  found.set(full, Number(m[1]));
@@ -4288,8 +4448,8 @@ var NvimService = class {
4288
4448
  };
4289
4449
 
4290
4450
  // packages/server/src/watcher.ts
4291
- import path5 from "path";
4292
- import { stat as stat3 } from "fs/promises";
4451
+ import path6 from "path";
4452
+ import { stat as stat4 } from "fs/promises";
4293
4453
  var DEFAULT_POLL_INTERVAL_MS = 1500;
4294
4454
  function statusPaths(out) {
4295
4455
  const parts = out.split("\0");
@@ -4385,7 +4545,7 @@ var RepoWatcher = class {
4385
4545
  const stats = await Promise.all(
4386
4546
  statusPaths(statusRes.stdout).map(async (rel) => {
4387
4547
  try {
4388
- const st = await stat3(path5.join(this.root, rel));
4548
+ const st = await stat4(path6.join(this.root, rel));
4389
4549
  return `${rel} ${st.mtimeMs} ${st.size}`;
4390
4550
  } catch {
4391
4551
  return `${rel} -`;
@@ -4397,8 +4557,8 @@ var RepoWatcher = class {
4397
4557
  };
4398
4558
 
4399
4559
  // packages/server/src/static.ts
4400
- import path6 from "path";
4401
- import { readFile as readFile3, stat as stat4 } from "fs/promises";
4560
+ import path7 from "path";
4561
+ import { readFile as readFile3, stat as stat5 } from "fs/promises";
4402
4562
  var MIME = {
4403
4563
  ".html": "text/html; charset=utf-8",
4404
4564
  ".js": "text/javascript; charset=utf-8",
@@ -4415,20 +4575,20 @@ var MIME = {
4415
4575
  ".txt": "text/plain; charset=utf-8"
4416
4576
  };
4417
4577
  async function serveStaticFile(c, webDir) {
4418
- const root = path6.resolve(webDir);
4578
+ const root = path7.resolve(webDir);
4419
4579
  let rel;
4420
4580
  try {
4421
4581
  rel = decodeURIComponent(c.req.path);
4422
4582
  } catch {
4423
4583
  return c.text("Bad Request", 400);
4424
4584
  }
4425
- let target = path6.resolve(root, "." + rel);
4426
- if (target !== root && !target.startsWith(root + path6.sep)) return c.text("Forbidden", 403);
4585
+ let target = path7.resolve(root, "." + rel);
4586
+ if (target !== root && !target.startsWith(root + path7.sep)) return c.text("Forbidden", 403);
4427
4587
  try {
4428
- const st = await stat4(target);
4429
- if (st.isDirectory()) target = path6.join(target, "index.html");
4588
+ const st = await stat5(target);
4589
+ if (st.isDirectory()) target = path7.join(target, "index.html");
4430
4590
  } catch {
4431
- target = path6.join(root, "index.html");
4591
+ target = path7.join(root, "index.html");
4432
4592
  }
4433
4593
  let body;
4434
4594
  try {
@@ -4436,7 +4596,7 @@ async function serveStaticFile(c, webDir) {
4436
4596
  } catch {
4437
4597
  return c.text("Not Found", 404);
4438
4598
  }
4439
- const ext = path6.extname(target).toLowerCase();
4599
+ const ext = path7.extname(target).toLowerCase();
4440
4600
  const isAsset = rel.startsWith("/assets/");
4441
4601
  return new Response(new Uint8Array(body), {
4442
4602
  status: 200,
@@ -4507,6 +4667,12 @@ function createApp(opts) {
4507
4667
  return c.json({ error: err instanceof Error ? err.message : String(err), code: "internal" }, 500);
4508
4668
  });
4509
4669
  const api = new Hono2();
4670
+ api.use("*", async (c, next) => {
4671
+ if (c.req.method !== "GET" && c.req.method !== "HEAD" && c.req.header("sec-fetch-site") === "cross-site") {
4672
+ throw new HttpError(403, "cross-site requests are refused", "cross_site");
4673
+ }
4674
+ await next();
4675
+ });
4510
4676
  api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
4511
4677
  api.get("/repo", async (c) => {
4512
4678
  const state = await store.load();
@@ -5043,6 +5209,26 @@ function createApp(opts) {
5043
5209
  const res = { base, sha, ahead: Number(counts[1] ?? 0), behind: Number(counts[0] ?? 0) };
5044
5210
  return c.json(res);
5045
5211
  });
5212
+ api.get("/worktrees", async (c) => {
5213
+ const list = await listWorktreesDetailed(repo);
5214
+ const res = { worktrees: list, branches: await listBranches(repo, list), pathPrefix: worktreePathPrefix(repo) };
5215
+ return c.json(res);
5216
+ });
5217
+ api.post("/worktrees", async (c) => {
5218
+ const body = await c.req.json();
5219
+ if (typeof body.path !== "string" || typeof body.branch !== "string") throw badRequest("path and branch are required");
5220
+ if (body.base !== void 0 && typeof body.base !== "string") throw badRequest("base must be a ref");
5221
+ const made = await addWorktree(repo, body);
5222
+ worktreeCache = null;
5223
+ return c.json(made, 201);
5224
+ });
5225
+ api.post("/worktrees/remove", async (c) => {
5226
+ const body = await c.req.json();
5227
+ if (typeof body.path !== "string") throw badRequest("path is required");
5228
+ const res = await removeWorktree(repo, body);
5229
+ worktreeCache = null;
5230
+ return c.json(res);
5231
+ });
5046
5232
  api.get("/events", async (c) => {
5047
5233
  const root = c.req.query("root") || repo.root;
5048
5234
  const known = root === repo.root || root === repo.commonRoot || (await worktrees()).some((w) => w.path === root);
@@ -5094,7 +5280,7 @@ function createApp(opts) {
5094
5280
  api.post("/nvim/open", async (c) => {
5095
5281
  const body = await c.req.json();
5096
5282
  if (!body.socket || !body.absPath) throw badRequest("socket and absPath are required");
5097
- if (!path7.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
5283
+ if (!path8.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
5098
5284
  const scan = await nvim.scan();
5099
5285
  if (!scan.instances.some((i) => i.socket === body.socket)) {
5100
5286
  const fresh = await nvim.scan(true);
@@ -5220,7 +5406,7 @@ async function startServer(opts) {
5220
5406
  }
5221
5407
 
5222
5408
  // bin/cli.ts
5223
- var VERSION = true ? "0.7.0" : "dev";
5409
+ var VERSION = true ? "0.8.0" : "dev";
5224
5410
  function parseArgs(argv) {
5225
5411
  const args = { repoPath: process.cwd(), open: true, help: false, version: false };
5226
5412
  for (let i = 0; i < argv.length; i++) {
@@ -5238,7 +5424,7 @@ function parseArgs(argv) {
5238
5424
  } else if (a === "--help" || a === "-h") args.help = true;
5239
5425
  else if (a === "--version" || a === "-v") args.version = true;
5240
5426
  else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
5241
- else args.repoPath = path8.resolve(a);
5427
+ else args.repoPath = path9.resolve(a);
5242
5428
  }
5243
5429
  return args;
5244
5430
  }
@@ -5281,16 +5467,16 @@ async function openBrowser(url) {
5281
5467
  return false;
5282
5468
  }
5283
5469
  function resolveWebDir() {
5284
- const here = path8.dirname(fileURLToPath(import.meta.url));
5470
+ const here = path9.dirname(fileURLToPath(import.meta.url));
5285
5471
  const candidates = [
5286
5472
  process.env.WARDEN_WEB_DIR,
5287
- path8.join(here, "web"),
5473
+ path9.join(here, "web"),
5288
5474
  // dist/cli.js -> dist/web
5289
- path8.join(here, "..", "dist", "web")
5475
+ path9.join(here, "..", "dist", "web")
5290
5476
  // bin/cli.ts (tsx dev) -> dist/web
5291
5477
  ].filter((p) => !!p);
5292
5478
  for (const dir of candidates) {
5293
- if (existsSync2(path8.join(dir, "index.html"))) return dir;
5479
+ if (existsSync2(path9.join(dir, "index.html"))) return dir;
5294
5480
  }
5295
5481
  return void 0;
5296
5482
  }