@xbghc/warden 0.16.0 → 0.16.1
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,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path12 from "path";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { existsSync as existsSync2 } 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
|
|
11
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
12
12
|
|
|
13
13
|
// node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.7/node_modules/@hono/node-server/dist/index.mjs
|
|
14
14
|
import { createServer as createServerHTTP } from "http";
|
|
@@ -652,9 +652,9 @@ var serve = (options, listeningListener) => {
|
|
|
652
652
|
};
|
|
653
653
|
|
|
654
654
|
// packages/server/src/app.ts
|
|
655
|
-
import
|
|
655
|
+
import path10 from "path";
|
|
656
656
|
import { stat as stat7 } from "fs/promises";
|
|
657
|
-
import { randomUUID as
|
|
657
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
658
658
|
|
|
659
659
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
|
|
660
660
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -813,26 +813,26 @@ var throwNestingLimitExceeded = () => {
|
|
|
813
813
|
};
|
|
814
814
|
|
|
815
815
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
|
|
816
|
-
var splitPath = (
|
|
817
|
-
const paths =
|
|
816
|
+
var splitPath = (path13) => {
|
|
817
|
+
const paths = path13.split("/");
|
|
818
818
|
if (paths[0] === "") {
|
|
819
819
|
paths.shift();
|
|
820
820
|
}
|
|
821
821
|
return paths;
|
|
822
822
|
};
|
|
823
823
|
var splitRoutingPath = (routePath) => {
|
|
824
|
-
const { groups, path:
|
|
825
|
-
const paths = splitPath(
|
|
824
|
+
const { groups, path: path13 } = extractGroupsFromPath(routePath);
|
|
825
|
+
const paths = splitPath(path13);
|
|
826
826
|
return replaceGroupMarks(paths, groups);
|
|
827
827
|
};
|
|
828
|
-
var extractGroupsFromPath = (
|
|
828
|
+
var extractGroupsFromPath = (path13) => {
|
|
829
829
|
const groups = [];
|
|
830
|
-
|
|
830
|
+
path13 = path13.replace(/\{[^}]+\}/g, (match2, index) => {
|
|
831
831
|
const mark = `@${index}`;
|
|
832
832
|
groups.push([mark, match2]);
|
|
833
833
|
return mark;
|
|
834
834
|
});
|
|
835
|
-
return { groups, path:
|
|
835
|
+
return { groups, path: path13 };
|
|
836
836
|
};
|
|
837
837
|
var replaceGroupMarks = (paths, groups) => {
|
|
838
838
|
for (let i = groups.length - 1; i >= 0; i--) {
|
|
@@ -889,8 +889,8 @@ var getPath = (request) => {
|
|
|
889
889
|
const queryIndex = url.indexOf("?", i);
|
|
890
890
|
const hashIndex = url.indexOf("#", i);
|
|
891
891
|
const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
892
|
-
const
|
|
893
|
-
return tryDecodeURI(
|
|
892
|
+
const path13 = url.slice(start, end);
|
|
893
|
+
return tryDecodeURI(path13.includes("%25") ? path13.replace(/%25/g, "%2525") : path13);
|
|
894
894
|
} else if (charCode === 63 || charCode === 35) {
|
|
895
895
|
break;
|
|
896
896
|
}
|
|
@@ -907,11 +907,11 @@ var mergePath = (base, sub, ...rest) => {
|
|
|
907
907
|
}
|
|
908
908
|
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
|
|
909
909
|
};
|
|
910
|
-
var checkOptionalParameter = (
|
|
911
|
-
if (
|
|
910
|
+
var checkOptionalParameter = (path13) => {
|
|
911
|
+
if (path13.charCodeAt(path13.length - 1) !== 63 || !path13.includes(":")) {
|
|
912
912
|
return null;
|
|
913
913
|
}
|
|
914
|
-
const segments =
|
|
914
|
+
const segments = path13.split("/");
|
|
915
915
|
const results = [];
|
|
916
916
|
let basePath = "";
|
|
917
917
|
segments.forEach((segment) => {
|
|
@@ -1053,9 +1053,9 @@ var HonoRequest = class {
|
|
|
1053
1053
|
*/
|
|
1054
1054
|
path;
|
|
1055
1055
|
bodyCache = {};
|
|
1056
|
-
constructor(request,
|
|
1056
|
+
constructor(request, path13 = "/", matchResult = [[]]) {
|
|
1057
1057
|
this.raw = request;
|
|
1058
|
-
this.path =
|
|
1058
|
+
this.path = path13;
|
|
1059
1059
|
this.#matchResult = matchResult;
|
|
1060
1060
|
}
|
|
1061
1061
|
param(key) {
|
|
@@ -1827,8 +1827,8 @@ var Hono = class _Hono {
|
|
|
1827
1827
|
return this;
|
|
1828
1828
|
};
|
|
1829
1829
|
});
|
|
1830
|
-
this.on = (method,
|
|
1831
|
-
for (const p of [
|
|
1830
|
+
this.on = (method, path13, ...handlers) => {
|
|
1831
|
+
for (const p of [path13].flat()) {
|
|
1832
1832
|
this.#path = p;
|
|
1833
1833
|
for (const m of [method].flat()) {
|
|
1834
1834
|
const methodName = m.toUpperCase();
|
|
@@ -1886,8 +1886,8 @@ var Hono = class _Hono {
|
|
|
1886
1886
|
* app.route("/api", app2) // GET /api/user
|
|
1887
1887
|
* ```
|
|
1888
1888
|
*/
|
|
1889
|
-
route(
|
|
1890
|
-
const subApp = this.basePath(
|
|
1889
|
+
route(path13, app) {
|
|
1890
|
+
const subApp = this.basePath(path13);
|
|
1891
1891
|
app.routes.map((r) => {
|
|
1892
1892
|
let handler;
|
|
1893
1893
|
if (app.errorHandler === errorHandler) {
|
|
@@ -1913,9 +1913,9 @@ var Hono = class _Hono {
|
|
|
1913
1913
|
* const api = new Hono().basePath('/api')
|
|
1914
1914
|
* ```
|
|
1915
1915
|
*/
|
|
1916
|
-
basePath(
|
|
1916
|
+
basePath(path13) {
|
|
1917
1917
|
const subApp = this.#clone();
|
|
1918
|
-
subApp._basePath = mergePath(this._basePath,
|
|
1918
|
+
subApp._basePath = mergePath(this._basePath, path13);
|
|
1919
1919
|
return subApp;
|
|
1920
1920
|
}
|
|
1921
1921
|
/**
|
|
@@ -1989,7 +1989,7 @@ var Hono = class _Hono {
|
|
|
1989
1989
|
* })
|
|
1990
1990
|
* ```
|
|
1991
1991
|
*/
|
|
1992
|
-
mount(
|
|
1992
|
+
mount(path13, applicationHandler, options) {
|
|
1993
1993
|
let replaceRequest;
|
|
1994
1994
|
let optionHandler;
|
|
1995
1995
|
if (options) {
|
|
@@ -2016,7 +2016,7 @@ var Hono = class _Hono {
|
|
|
2016
2016
|
return [c.env, executionContext];
|
|
2017
2017
|
};
|
|
2018
2018
|
replaceRequest ||= (() => {
|
|
2019
|
-
const mergedPath = mergePath(this._basePath,
|
|
2019
|
+
const mergedPath = mergePath(this._basePath, path13);
|
|
2020
2020
|
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
|
|
2021
2021
|
return (request) => {
|
|
2022
2022
|
const url = new URL(request.url);
|
|
@@ -2031,18 +2031,18 @@ var Hono = class _Hono {
|
|
|
2031
2031
|
}
|
|
2032
2032
|
await next();
|
|
2033
2033
|
};
|
|
2034
|
-
this.#addRoute(METHOD_NAME_ALL, mergePath(
|
|
2034
|
+
this.#addRoute(METHOD_NAME_ALL, mergePath(path13, "*"), handler);
|
|
2035
2035
|
return this;
|
|
2036
2036
|
}
|
|
2037
|
-
#addRoute(method,
|
|
2038
|
-
|
|
2037
|
+
#addRoute(method, path13, handler, baseRoutePath) {
|
|
2038
|
+
path13 = mergePath(this._basePath, path13);
|
|
2039
2039
|
const r = {
|
|
2040
2040
|
basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
|
|
2041
|
-
path:
|
|
2041
|
+
path: path13,
|
|
2042
2042
|
method,
|
|
2043
2043
|
handler
|
|
2044
2044
|
};
|
|
2045
|
-
this.router.add(method,
|
|
2045
|
+
this.router.add(method, path13, [handler, r]);
|
|
2046
2046
|
this.routes.push(r);
|
|
2047
2047
|
}
|
|
2048
2048
|
#handleError(err, c) {
|
|
@@ -2055,10 +2055,10 @@ var Hono = class _Hono {
|
|
|
2055
2055
|
if (method === "HEAD") {
|
|
2056
2056
|
return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
|
|
2057
2057
|
}
|
|
2058
|
-
const
|
|
2059
|
-
const matchResult = this.router.match(method,
|
|
2058
|
+
const path13 = this.getPath(request, { env });
|
|
2059
|
+
const matchResult = this.router.match(method, path13);
|
|
2060
2060
|
const c = new Context(request, {
|
|
2061
|
-
path:
|
|
2061
|
+
path: path13,
|
|
2062
2062
|
matchResult,
|
|
2063
2063
|
env,
|
|
2064
2064
|
executionCtx,
|
|
@@ -2161,7 +2161,7 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
|
|
|
2161
2161
|
|
|
2162
2162
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
2163
2163
|
var emptyParam = [];
|
|
2164
|
-
function match(method,
|
|
2164
|
+
function match(method, path13) {
|
|
2165
2165
|
const matchers = this.buildAllMatchers();
|
|
2166
2166
|
const match2 = ((method2, path22) => {
|
|
2167
2167
|
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
|
@@ -2177,7 +2177,7 @@ function match(method, path12) {
|
|
|
2177
2177
|
return [matcher[1][index], match3];
|
|
2178
2178
|
});
|
|
2179
2179
|
this.match = match2;
|
|
2180
|
-
return match2(method,
|
|
2180
|
+
return match2(method, path13);
|
|
2181
2181
|
}
|
|
2182
2182
|
|
|
2183
2183
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
@@ -2294,14 +2294,14 @@ var Trie = class {
|
|
|
2294
2294
|
#index = 0;
|
|
2295
2295
|
// dynamic path -> [handler index, param assoc]; static paths are not registered
|
|
2296
2296
|
paths = createNullObject();
|
|
2297
|
-
insert(
|
|
2297
|
+
insert(path13, isStatic) {
|
|
2298
2298
|
if (isStatic) {
|
|
2299
|
-
this.#root.insert(
|
|
2299
|
+
this.#root.insert(path13.split(""), 0, [], this.#context, true);
|
|
2300
2300
|
return;
|
|
2301
2301
|
}
|
|
2302
2302
|
const paramAssoc = [];
|
|
2303
2303
|
const groups = [];
|
|
2304
|
-
let markedPath =
|
|
2304
|
+
let markedPath = path13;
|
|
2305
2305
|
for (let i = 0; ; ) {
|
|
2306
2306
|
let replaced = false;
|
|
2307
2307
|
markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
|
|
@@ -2326,7 +2326,7 @@ var Trie = class {
|
|
|
2326
2326
|
}
|
|
2327
2327
|
}
|
|
2328
2328
|
this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
|
|
2329
|
-
this.paths[
|
|
2329
|
+
this.paths[path13] = [this.#index++, paramAssoc];
|
|
2330
2330
|
}
|
|
2331
2331
|
buildRegExp() {
|
|
2332
2332
|
let regexp = this.#root.buildRegExpStr();
|
|
@@ -2353,17 +2353,17 @@ var Trie = class {
|
|
|
2353
2353
|
|
|
2354
2354
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
|
|
2355
2355
|
var wildcardRegExpCache = createNullObject();
|
|
2356
|
-
function buildWildcardRegExp(
|
|
2357
|
-
return wildcardRegExpCache[
|
|
2358
|
-
`^${
|
|
2356
|
+
function buildWildcardRegExp(path13) {
|
|
2357
|
+
return wildcardRegExpCache[path13] ??= new RegExp(
|
|
2358
|
+
`^${path13.replace(
|
|
2359
2359
|
/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g,
|
|
2360
2360
|
(match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`
|
|
2361
2361
|
)}$`
|
|
2362
2362
|
);
|
|
2363
2363
|
}
|
|
2364
|
-
function findMiddleware(middleware,
|
|
2364
|
+
function findMiddleware(middleware, path13) {
|
|
2365
2365
|
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
|
|
2366
|
-
if (buildWildcardRegExp(k).test(
|
|
2366
|
+
if (buildWildcardRegExp(k).test(path13)) {
|
|
2367
2367
|
return [...middleware[k]];
|
|
2368
2368
|
}
|
|
2369
2369
|
}
|
|
@@ -2379,14 +2379,14 @@ var RegExpRouter = class {
|
|
|
2379
2379
|
this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
|
|
2380
2380
|
this.#tries = { [METHOD_NAME_ALL]: new Trie() };
|
|
2381
2381
|
}
|
|
2382
|
-
#insertPath(method,
|
|
2382
|
+
#insertPath(method, path13) {
|
|
2383
2383
|
try {
|
|
2384
|
-
this.#tries[method].insert(
|
|
2384
|
+
this.#tries[method].insert(path13, !/\*|\/:/.test(path13));
|
|
2385
2385
|
} catch (e) {
|
|
2386
|
-
throw e === PATH_ERROR ? new UnsupportedPathError(
|
|
2386
|
+
throw e === PATH_ERROR ? new UnsupportedPathError(path13) : e;
|
|
2387
2387
|
}
|
|
2388
2388
|
}
|
|
2389
|
-
add(method,
|
|
2389
|
+
add(method, path13, handler) {
|
|
2390
2390
|
const middleware = this.#middleware;
|
|
2391
2391
|
const routes = this.#routes;
|
|
2392
2392
|
if (!middleware) {
|
|
@@ -2402,28 +2402,28 @@ var RegExpRouter = class {
|
|
|
2402
2402
|
}
|
|
2403
2403
|
}
|
|
2404
2404
|
}
|
|
2405
|
-
if (
|
|
2406
|
-
|
|
2405
|
+
if (path13 === "/*") {
|
|
2406
|
+
path13 = "*";
|
|
2407
2407
|
}
|
|
2408
2408
|
const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
|
|
2409
|
-
if (/\*$/.test(
|
|
2410
|
-
const re = buildWildcardRegExp(
|
|
2409
|
+
if (/\*$/.test(path13)) {
|
|
2410
|
+
const re = buildWildcardRegExp(path13);
|
|
2411
2411
|
for (const m of methods) {
|
|
2412
|
-
if (!middleware[m][
|
|
2413
|
-
this.#insertPath(m,
|
|
2414
|
-
middleware[m][
|
|
2412
|
+
if (!middleware[m][path13]) {
|
|
2413
|
+
this.#insertPath(m, path13);
|
|
2414
|
+
middleware[m][path13] = findMiddleware(middleware[m], path13) || findMiddleware(middleware[METHOD_NAME_ALL], path13) || [];
|
|
2415
2415
|
}
|
|
2416
2416
|
}
|
|
2417
2417
|
for (const handlerMap of [middleware, routes]) {
|
|
2418
2418
|
for (const m of methods) {
|
|
2419
2419
|
for (const p in handlerMap[m]) {
|
|
2420
|
-
re.test(p) && handlerMap[m][p].push([handler,
|
|
2420
|
+
re.test(p) && handlerMap[m][p].push([handler, path13]);
|
|
2421
2421
|
}
|
|
2422
2422
|
}
|
|
2423
2423
|
}
|
|
2424
2424
|
return;
|
|
2425
2425
|
}
|
|
2426
|
-
const paths = checkOptionalParameter(
|
|
2426
|
+
const paths = checkOptionalParameter(path13) || [path13];
|
|
2427
2427
|
for (const path22 of paths) {
|
|
2428
2428
|
for (const m of methods) {
|
|
2429
2429
|
if (!routes[m][path22]) {
|
|
@@ -2452,11 +2452,11 @@ var RegExpRouter = class {
|
|
|
2452
2452
|
const handlerData = [];
|
|
2453
2453
|
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
|
|
2454
2454
|
for (const r of [middleware, routes]) {
|
|
2455
|
-
for (const
|
|
2456
|
-
const handlers = r[
|
|
2457
|
-
const pathData = trie.paths[
|
|
2455
|
+
for (const path13 in r) {
|
|
2456
|
+
const handlers = r[path13];
|
|
2457
|
+
const pathData = trie.paths[path13];
|
|
2458
2458
|
if (!pathData) {
|
|
2459
|
-
staticMap[
|
|
2459
|
+
staticMap[path13] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
|
|
2460
2460
|
continue;
|
|
2461
2461
|
}
|
|
2462
2462
|
handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
|
|
@@ -2480,13 +2480,13 @@ var SmartRouter = class {
|
|
|
2480
2480
|
constructor(init) {
|
|
2481
2481
|
this.#routers = init.routers;
|
|
2482
2482
|
}
|
|
2483
|
-
add(method,
|
|
2483
|
+
add(method, path13, handler) {
|
|
2484
2484
|
if (!this.#routes) {
|
|
2485
2485
|
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
2486
2486
|
}
|
|
2487
|
-
this.#routes.push([method,
|
|
2487
|
+
this.#routes.push([method, path13, handler]);
|
|
2488
2488
|
}
|
|
2489
|
-
match(method,
|
|
2489
|
+
match(method, path13) {
|
|
2490
2490
|
if (!this.#routes) {
|
|
2491
2491
|
throw new Error("Fatal error");
|
|
2492
2492
|
}
|
|
@@ -2501,7 +2501,7 @@ var SmartRouter = class {
|
|
|
2501
2501
|
for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
|
|
2502
2502
|
router.add(...routes[i2]);
|
|
2503
2503
|
}
|
|
2504
|
-
res = router.match(method,
|
|
2504
|
+
res = router.match(method, path13);
|
|
2505
2505
|
} catch (e) {
|
|
2506
2506
|
if (e instanceof UnsupportedPathError) {
|
|
2507
2507
|
continue;
|
|
@@ -2536,9 +2536,9 @@ var Node2 = class _Node2 {
|
|
|
2536
2536
|
#patterns = [];
|
|
2537
2537
|
#pattern;
|
|
2538
2538
|
#params = emptyParams;
|
|
2539
|
-
insert(method,
|
|
2539
|
+
insert(method, path13, handler) {
|
|
2540
2540
|
let curNode = this;
|
|
2541
|
-
const parts = splitRoutingPath(
|
|
2541
|
+
const parts = splitRoutingPath(path13);
|
|
2542
2542
|
const possibleKeys = /* @__PURE__ */ new Set();
|
|
2543
2543
|
let i = 0;
|
|
2544
2544
|
for (const p of parts) {
|
|
@@ -2578,12 +2578,12 @@ var Node2 = class _Node2 {
|
|
|
2578
2578
|
}
|
|
2579
2579
|
}
|
|
2580
2580
|
}
|
|
2581
|
-
search(method,
|
|
2581
|
+
search(method, path13) {
|
|
2582
2582
|
const handlerSets = [];
|
|
2583
2583
|
this.#params = emptyParams;
|
|
2584
2584
|
const curNode = this;
|
|
2585
2585
|
let curNodes = [curNode];
|
|
2586
|
-
const parts = splitPath(
|
|
2586
|
+
const parts = splitPath(path13);
|
|
2587
2587
|
const curNodesQueue = [];
|
|
2588
2588
|
const len = parts.length;
|
|
2589
2589
|
let partOffsets = null;
|
|
@@ -2625,13 +2625,13 @@ var Node2 = class _Node2 {
|
|
|
2625
2625
|
if (matcher !== true) {
|
|
2626
2626
|
if (!partOffsets) {
|
|
2627
2627
|
partOffsets = [];
|
|
2628
|
-
let offset =
|
|
2628
|
+
let offset = path13[0] === "/" ? 1 : 0;
|
|
2629
2629
|
for (let p = 0; p < len; p++) {
|
|
2630
2630
|
partOffsets[p] = offset;
|
|
2631
2631
|
offset += parts[p].length + 1;
|
|
2632
2632
|
}
|
|
2633
2633
|
}
|
|
2634
|
-
const restPathString =
|
|
2634
|
+
const restPathString = path13.slice(partOffsets[i]);
|
|
2635
2635
|
const m = matcher.exec(restPathString);
|
|
2636
2636
|
if (m) {
|
|
2637
2637
|
params[name] = m[0];
|
|
@@ -2691,13 +2691,13 @@ var Node2 = class _Node2 {
|
|
|
2691
2691
|
var TrieRouter = class {
|
|
2692
2692
|
name = "TrieRouter";
|
|
2693
2693
|
#node = new Node2();
|
|
2694
|
-
add(method,
|
|
2695
|
-
for (const result of checkOptionalParameter(
|
|
2694
|
+
add(method, path13, handler) {
|
|
2695
|
+
for (const result of checkOptionalParameter(path13) || [path13]) {
|
|
2696
2696
|
this.#node.insert(method, result, handler);
|
|
2697
2697
|
}
|
|
2698
2698
|
}
|
|
2699
|
-
match(method,
|
|
2700
|
-
return this.#node.search(method,
|
|
2699
|
+
match(method, path13) {
|
|
2700
|
+
return this.#node.search(method, path13);
|
|
2701
2701
|
}
|
|
2702
2702
|
};
|
|
2703
2703
|
|
|
@@ -3416,13 +3416,14 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
3416
3416
|
|
|
3417
3417
|
// packages/server/src/checkpoints.ts
|
|
3418
3418
|
import path3 from "path";
|
|
3419
|
-
import { randomUUID } from "crypto";
|
|
3420
|
-
import { copyFile, mkdir as mkdir2, rm, stat as stat2, utimes } from "fs/promises";
|
|
3419
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3420
|
+
import { copyFile, mkdir as mkdir2, rm, stat as stat2, utimes as utimes2 } from "fs/promises";
|
|
3421
3421
|
|
|
3422
3422
|
// packages/server/src/state.ts
|
|
3423
3423
|
import os from "os";
|
|
3424
3424
|
import path2 from "path";
|
|
3425
|
-
import {
|
|
3425
|
+
import { randomUUID } from "crypto";
|
|
3426
|
+
import { mkdir, open, readFile, rename, stat, unlink, utimes } from "fs/promises";
|
|
3426
3427
|
|
|
3427
3428
|
// packages/server/src/hash.ts
|
|
3428
3429
|
import { createHash } from "crypto";
|
|
@@ -3470,11 +3471,21 @@ var usableCheckpoint = (v) => {
|
|
|
3470
3471
|
return !!c && typeof c === "object" && Number.isInteger(c.id) && typeof c.tree === "string" && (c.worktree === void 0 || typeof c.worktree === "string");
|
|
3471
3472
|
};
|
|
3472
3473
|
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3474
|
+
var SCHEMA_VERSION = 1;
|
|
3475
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "repoRoot", "targets", "todos", "checkpoints", "prefs", "issues"]);
|
|
3476
|
+
var StateTooNew = class extends HttpError {
|
|
3477
|
+
constructor(file, version) {
|
|
3478
|
+
super(409, `${file} was written by a newer warden (schema ${version}); upgrade warden to use it`, "state_too_new");
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
var NotAState = class extends Error {
|
|
3482
|
+
};
|
|
3483
|
+
function normalise(raw2, repoRoot, file) {
|
|
3484
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) throw new NotAState();
|
|
3476
3485
|
const r = raw2;
|
|
3477
|
-
if (r.schemaVersion !==
|
|
3486
|
+
if (typeof r.schemaVersion !== "number") throw new NotAState();
|
|
3487
|
+
if (r.schemaVersion > SCHEMA_VERSION) throw new StateTooNew(file, r.schemaVersion);
|
|
3488
|
+
const extra = Object.fromEntries(Object.entries(r).filter(([k]) => !KNOWN_KEYS.has(k)));
|
|
3478
3489
|
const targets = {};
|
|
3479
3490
|
for (const [k, v] of Object.entries(r.targets ?? {})) {
|
|
3480
3491
|
if (!v || typeof v !== "object") continue;
|
|
@@ -3486,7 +3497,8 @@ function normalise(raw2, repoRoot) {
|
|
|
3486
3497
|
}
|
|
3487
3498
|
migrateLocalViews(targets);
|
|
3488
3499
|
return {
|
|
3489
|
-
|
|
3500
|
+
...extra,
|
|
3501
|
+
schemaVersion: SCHEMA_VERSION,
|
|
3490
3502
|
repoRoot: r.repoRoot ?? repoRoot,
|
|
3491
3503
|
targets,
|
|
3492
3504
|
// Issues were folded into todos (a todo links comments now); the few there were are let go,
|
|
@@ -3534,11 +3546,11 @@ var StateStore = class {
|
|
|
3534
3546
|
async load() {
|
|
3535
3547
|
try {
|
|
3536
3548
|
const text = await readFile(this.file, "utf8");
|
|
3537
|
-
return normalise(JSON.parse(text), this.repoRoot);
|
|
3549
|
+
return normalise(JSON.parse(text), this.repoRoot, this.file);
|
|
3538
3550
|
} catch (e) {
|
|
3539
3551
|
const err = e;
|
|
3540
3552
|
if (err.code === "ENOENT") return defaultState(this.repoRoot);
|
|
3541
|
-
if (e instanceof SyntaxError) {
|
|
3553
|
+
if (e instanceof SyntaxError || e instanceof NotAState) {
|
|
3542
3554
|
try {
|
|
3543
3555
|
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
3544
3556
|
} catch {
|
|
@@ -3552,14 +3564,15 @@ var StateStore = class {
|
|
|
3552
3564
|
update(fn) {
|
|
3553
3565
|
const run2 = async () => {
|
|
3554
3566
|
await mkdir(path2.dirname(this.file), { recursive: true });
|
|
3555
|
-
const
|
|
3567
|
+
const lock = await this.acquireLock();
|
|
3556
3568
|
try {
|
|
3557
3569
|
const state = await this.load();
|
|
3558
3570
|
const result = await fn(state);
|
|
3571
|
+
if (!await lock.held()) throw new HttpError(409, "the state lock was taken over while this change was prepared; try again", "state_lock_lost");
|
|
3559
3572
|
await this.writeAtomic(state);
|
|
3560
3573
|
return result;
|
|
3561
3574
|
} finally {
|
|
3562
|
-
await release();
|
|
3575
|
+
await lock.release();
|
|
3563
3576
|
}
|
|
3564
3577
|
};
|
|
3565
3578
|
const p = this.queue.then(run2, run2);
|
|
@@ -3568,30 +3581,42 @@ var StateStore = class {
|
|
|
3568
3581
|
}
|
|
3569
3582
|
async writeAtomic(state) {
|
|
3570
3583
|
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
3571
|
-
await
|
|
3584
|
+
const fh = await open(tmp, "w");
|
|
3585
|
+
try {
|
|
3586
|
+
await fh.writeFile(JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3587
|
+
await fh.sync();
|
|
3588
|
+
} finally {
|
|
3589
|
+
await fh.close();
|
|
3590
|
+
}
|
|
3572
3591
|
await rename(tmp, this.file);
|
|
3573
3592
|
}
|
|
3593
|
+
/**
|
|
3594
|
+
* An exclusive lock file beside the state file, shared by every warden process and the agent
|
|
3595
|
+
* CLI. It carries a token of its own, so a holder releases only the lock it took, and its mtime
|
|
3596
|
+
* is refreshed while held: a lock left by a process that died goes stale and is taken over, one
|
|
3597
|
+
* whose holder is merely slow does not.
|
|
3598
|
+
*/
|
|
3574
3599
|
async acquireLock() {
|
|
3575
3600
|
const lock = `${this.file}.lock`;
|
|
3601
|
+
const token = `${process.pid}:${randomUUID()}`;
|
|
3576
3602
|
const started = Date.now();
|
|
3577
3603
|
for (; ; ) {
|
|
3578
3604
|
try {
|
|
3579
3605
|
const fh = await open(lock, "wx");
|
|
3580
|
-
await fh.writeFile(
|
|
3606
|
+
await fh.writeFile(token);
|
|
3581
3607
|
await fh.close();
|
|
3582
|
-
|
|
3583
|
-
try {
|
|
3584
|
-
await unlink(lock);
|
|
3585
|
-
} catch {
|
|
3586
|
-
}
|
|
3587
|
-
};
|
|
3608
|
+
break;
|
|
3588
3609
|
} catch (e) {
|
|
3589
3610
|
const err = e;
|
|
3590
3611
|
if (err.code !== "EEXIST") throw e;
|
|
3591
3612
|
try {
|
|
3592
3613
|
const st = await stat(lock);
|
|
3593
3614
|
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
3594
|
-
|
|
3615
|
+
const aside = `${lock}.stale-${token.replace(":", "-")}`;
|
|
3616
|
+
await rename(lock, aside).then(
|
|
3617
|
+
() => unlink(aside).catch(() => void 0),
|
|
3618
|
+
() => void 0
|
|
3619
|
+
);
|
|
3595
3620
|
continue;
|
|
3596
3621
|
}
|
|
3597
3622
|
} catch {
|
|
@@ -3603,6 +3628,18 @@ var StateStore = class {
|
|
|
3603
3628
|
await sleep(20);
|
|
3604
3629
|
}
|
|
3605
3630
|
}
|
|
3631
|
+
const ours = async () => await readFile(lock, "utf8").catch(() => "") === token;
|
|
3632
|
+
const beat = setInterval(() => {
|
|
3633
|
+
void ours().then((mine) => mine ? utimes(lock, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0) : void 0);
|
|
3634
|
+
}, LOCK_STALE_MS / 4);
|
|
3635
|
+
beat.unref();
|
|
3636
|
+
return {
|
|
3637
|
+
held: ours,
|
|
3638
|
+
release: async () => {
|
|
3639
|
+
clearInterval(beat);
|
|
3640
|
+
if (await ours()) await unlink(lock).catch(() => void 0);
|
|
3641
|
+
}
|
|
3642
|
+
};
|
|
3606
3643
|
}
|
|
3607
3644
|
};
|
|
3608
3645
|
function forgetWorktreeTargets(state, worktreePath) {
|
|
@@ -3647,13 +3684,13 @@ async function withIndexCopy(cwd, store, fn) {
|
|
|
3647
3684
|
await mkdir2(path3.join(store, "objects"), { recursive: true });
|
|
3648
3685
|
await mkdir2(tmp, { recursive: true });
|
|
3649
3686
|
const [objects, index] = await Promise.all([checkpointObjects(cwd, store), repoIndex(cwd)]);
|
|
3650
|
-
const copy = path3.join(tmp, `index-${
|
|
3687
|
+
const copy = path3.join(tmp, `index-${randomUUID2()}`);
|
|
3651
3688
|
try {
|
|
3652
3689
|
const st = await stat2(index).catch(() => void 0);
|
|
3653
3690
|
if (st) {
|
|
3654
3691
|
await copyFile(index, copy);
|
|
3655
3692
|
const earlier = Math.floor(st.mtimeMs / 1e3) - 1;
|
|
3656
|
-
await
|
|
3693
|
+
await utimes2(copy, earlier, earlier);
|
|
3657
3694
|
}
|
|
3658
3695
|
return await fn({ ...objects, index: copy });
|
|
3659
3696
|
} finally {
|
|
@@ -3697,7 +3734,9 @@ function addCheckpoint(state, worktree, tree, head, handoff = false) {
|
|
|
3697
3734
|
const id = mine.reduce((n, c) => Math.max(n, c.id), 0) + 1;
|
|
3698
3735
|
const checkpoint = { id, ...worktree ? { worktree } : {}, tree, head, createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...handoff ? { handoff } : {} };
|
|
3699
3736
|
state.checkpoints.push(checkpoint);
|
|
3700
|
-
|
|
3737
|
+
const evictable = mine.filter((c) => !state.targets[checkpointKey(c)]?.comments.length);
|
|
3738
|
+
const excess = mine.length + 1 - MAX_CHECKPOINTS;
|
|
3739
|
+
for (const old of evictable.slice(0, Math.max(0, excess))) forgetCheckpoint(state, old);
|
|
3701
3740
|
return checkpoint;
|
|
3702
3741
|
}
|
|
3703
3742
|
async function takeCheckpoint(store, cwd, worktree, opts = {}) {
|
|
@@ -3962,7 +4001,8 @@ async function runDiff(ctx, args) {
|
|
|
3962
4001
|
if (ctx.target.kind !== "checkpoint") return (await runGit(args, { cwd: ctx.cwd })).stdout;
|
|
3963
4002
|
return withCheckpointIndex(ctx.cwd, checkpointOf(ctx).store, async (snapshot) => (await runGit(args, { cwd: ctx.cwd, snapshot })).stdout);
|
|
3964
4003
|
}
|
|
3965
|
-
var
|
|
4004
|
+
var DIFF_OUTPUT_ARGS = ["--no-color", "--no-ext-diff", "--no-textconv", "--src-prefix=a/", "--dst-prefix=b/", "--submodule=short", "-U3"];
|
|
4005
|
+
var DIFF_BASE_ARGS = ["diff", ...DIFF_OUTPUT_ARGS, "-M", "--find-renames"];
|
|
3966
4006
|
function resolveTargetContext(repo, worktrees, key) {
|
|
3967
4007
|
let target;
|
|
3968
4008
|
try {
|
|
@@ -3971,7 +4011,7 @@ function resolveTargetContext(repo, worktrees, key) {
|
|
|
3971
4011
|
if (e instanceof TargetKeyError) throw badRequest(e.message, "invalid_target");
|
|
3972
4012
|
throw e;
|
|
3973
4013
|
}
|
|
3974
|
-
let cwd = repo.
|
|
4014
|
+
let cwd = repo.commonRoot;
|
|
3975
4015
|
if (target.worktree) {
|
|
3976
4016
|
const wt = worktrees.find((w) => w.path === target.worktree);
|
|
3977
4017
|
if (!wt) throw badRequest(`unknown worktree: ${target.worktree}`, "unknown_worktree");
|
|
@@ -4031,7 +4071,7 @@ async function isUntracked(cwd, file) {
|
|
|
4031
4071
|
return r.stdout.split("\0").filter(Boolean).includes(file);
|
|
4032
4072
|
}
|
|
4033
4073
|
async function untrackedDiff(cwd, file) {
|
|
4034
|
-
const r = await runGit(["diff",
|
|
4074
|
+
const r = await runGit(["diff", ...DIFF_OUTPUT_ARGS, "--no-index", "--", "/dev/null", file], {
|
|
4035
4075
|
cwd,
|
|
4036
4076
|
okCodes: [0, 1]
|
|
4037
4077
|
});
|
|
@@ -4058,7 +4098,7 @@ async function mapLimit(items, limit, fn) {
|
|
|
4058
4098
|
}
|
|
4059
4099
|
async function listTargetDiffs(ctx) {
|
|
4060
4100
|
const args = await diffArgs(ctx);
|
|
4061
|
-
const files = parseUnifiedDiff(await runDiff(ctx, args));
|
|
4101
|
+
const files = parseUnifiedDiff(await runDiff(ctx, [...args, "--"]));
|
|
4062
4102
|
if (includesUntracked(ctx.target)) {
|
|
4063
4103
|
const untracked = await listUntracked(ctx.cwd);
|
|
4064
4104
|
const extra = await mapLimit(untracked, 8, (f) => untrackedDiff(ctx.cwd, f).catch(() => void 0));
|
|
@@ -4128,7 +4168,7 @@ async function pathsWithMarkers(ctx, side, paths) {
|
|
|
4128
4168
|
const found = /* @__PURE__ */ new Set();
|
|
4129
4169
|
if (paths.length === 0) return found;
|
|
4130
4170
|
const ref = await refForSide(ctx, side);
|
|
4131
|
-
const args = ["grep", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
|
|
4171
|
+
const args = ["grep", "--no-color", "--no-recurse-submodules", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
|
|
4132
4172
|
if (ref === void 0) args.push("--untracked");
|
|
4133
4173
|
else if (ref === ":0") args.push("--cached");
|
|
4134
4174
|
else args.push(ref);
|
|
@@ -4444,6 +4484,39 @@ async function listUpstreams(ctx) {
|
|
|
4444
4484
|
}
|
|
4445
4485
|
return out;
|
|
4446
4486
|
}
|
|
4487
|
+
async function commonDirOf(dir) {
|
|
4488
|
+
try {
|
|
4489
|
+
const r = await runGit(["rev-parse", "--show-toplevel", "--git-common-dir"], { cwd: dir });
|
|
4490
|
+
const [top, common] = r.stdout.trim().split("\n");
|
|
4491
|
+
if (!top || !common || await realpath2(top) !== await realpath2(dir)) return void 0;
|
|
4492
|
+
return await realpath2(path5.resolve(dir, common));
|
|
4493
|
+
} catch {
|
|
4494
|
+
return void 0;
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
var IN_PROGRESS = [
|
|
4498
|
+
["rebase-merge", "rebase"],
|
|
4499
|
+
["rebase-apply", "rebase"],
|
|
4500
|
+
["MERGE_HEAD", "merge"],
|
|
4501
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
4502
|
+
["REVERT_HEAD", "revert"],
|
|
4503
|
+
["BISECT_LOG", "bisect"]
|
|
4504
|
+
];
|
|
4505
|
+
async function checkoutState(ctx, dir) {
|
|
4506
|
+
const [ours, theirs] = await Promise.all([commonDirOf(ctx.commonRoot), commonDirOf(dir)]);
|
|
4507
|
+
if (!ours || ours !== theirs) return { foreign: true };
|
|
4508
|
+
const r = await runGit(["rev-parse", ...IN_PROGRESS.flatMap(([f]) => ["--git-path", f])], { cwd: dir });
|
|
4509
|
+
const paths = r.stdout.trim().split("\n");
|
|
4510
|
+
for (const [i, [, what]] of IN_PROGRESS.entries()) {
|
|
4511
|
+
const p = paths[i];
|
|
4512
|
+
if (p && await stat3(path5.resolve(dir, p)).then(
|
|
4513
|
+
() => true,
|
|
4514
|
+
() => false
|
|
4515
|
+
))
|
|
4516
|
+
return { foreign: false, busy: what };
|
|
4517
|
+
}
|
|
4518
|
+
return { foreign: false };
|
|
4519
|
+
}
|
|
4447
4520
|
async function listWorktreesDetailed(ctx) {
|
|
4448
4521
|
const [all, upstreams] = await Promise.all([listWorktreesAll(ctx), listUpstreams(ctx)]);
|
|
4449
4522
|
const main2 = all.find((w) => w.isMain);
|
|
@@ -4451,12 +4524,16 @@ async function listWorktreesDetailed(ctx) {
|
|
|
4451
4524
|
all.map(async (w) => {
|
|
4452
4525
|
const slot = w.isMain || w.bare ? void 0 : slotOf(ctx, w.path);
|
|
4453
4526
|
const dirty = w.prunable || w.bare ? 0 : await dirtyCount(w.path);
|
|
4527
|
+
const state = slot === void 0 || w.prunable ? { foreign: false } : await checkoutState(ctx, w.path);
|
|
4454
4528
|
const detail = {
|
|
4455
4529
|
...w,
|
|
4456
4530
|
dirty,
|
|
4457
4531
|
...slot === void 0 ? {} : { slot },
|
|
4458
|
-
|
|
4459
|
-
|
|
4532
|
+
...state.foreign ? { foreign: true } : {},
|
|
4533
|
+
...state.busy ? { busy: state.busy } : {},
|
|
4534
|
+
// Free is what the next checkout may take over: a detached HEAD, nothing that would be lost,
|
|
4535
|
+
// nothing stopped halfway, and a checkout that is this repository's own.
|
|
4536
|
+
free: slot !== void 0 && !w.prunable && w.detached && dirty === 0 && !state.foreign && !state.busy
|
|
4460
4537
|
};
|
|
4461
4538
|
if (detail.free) return detail;
|
|
4462
4539
|
const upstream = w.branch ? upstreams.get(w.branch) : void 0;
|
|
@@ -4524,6 +4601,8 @@ async function pickSlot(ctx, worktrees, wanted) {
|
|
|
4524
4601
|
}
|
|
4525
4602
|
const have = slots.find((w) => w.slot === wanted);
|
|
4526
4603
|
if (have?.prunable) throw new HttpError(409, `the directory of slot ${wanted} is gone; remove its entry first`, "slot_gone");
|
|
4604
|
+
if (have?.foreign) throw new HttpError(409, `${have.path} is not a checkout of this repository`, "not_our_checkout");
|
|
4605
|
+
if (have?.busy) throw new HttpError(409, `a ${have.busy} is in progress in slot ${wanted}`, "operation_in_progress");
|
|
4527
4606
|
if (have && !have.free) throw new HttpError(409, `slot ${wanted} is in use${have.branch ? ` by ${have.branch}` : ""}`, "slot_in_use");
|
|
4528
4607
|
if (have) return { slot: wanted, path: have.path, reuse: true };
|
|
4529
4608
|
const p = slotPath(ctx, wanted);
|
|
@@ -4604,6 +4683,9 @@ async function releaseWorktree(ctx, req) {
|
|
|
4604
4683
|
if (wt.isMain) throw badRequest("the main worktree cannot be released", "main_worktree");
|
|
4605
4684
|
if (wt.bare || slotOf(ctx, wt.path) === void 0) throw badRequest(`${wt.path} is not a slot; remove it instead`, "not_a_slot");
|
|
4606
4685
|
if (wt.prunable) throw badRequest(`the directory of ${wt.path} is gone; remove its entry instead`, "worktree_gone");
|
|
4686
|
+
const state = await checkoutState(ctx, wt.path);
|
|
4687
|
+
if (state.foreign) throw new HttpError(409, `${wt.path} is not a checkout of this repository; warden will not write in it`, "not_our_checkout");
|
|
4688
|
+
if (state.busy) throw new HttpError(409, `a ${state.busy} is in progress in ${wt.path}; finish or abort it first`, "operation_in_progress");
|
|
4607
4689
|
const dirty = await dirtyCount(wt.path);
|
|
4608
4690
|
if (dirty > 0 && !req.force) throw new HttpError(409, `${wt.path} has uncommitted changes`, "worktree_dirty");
|
|
4609
4691
|
if (dirty > 0) {
|
|
@@ -4618,6 +4700,10 @@ async function removeWorktree(ctx, req) {
|
|
|
4618
4700
|
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4619
4701
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4620
4702
|
if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
|
|
4703
|
+
if (!wt.prunable && !req.force) {
|
|
4704
|
+
const state = await checkoutState(ctx, wt.path);
|
|
4705
|
+
if (state.busy) throw new HttpError(409, `a ${state.busy} is in progress in ${wt.path}; removing it throws that away`, "needs_force");
|
|
4706
|
+
}
|
|
4621
4707
|
const args = ["worktree", "remove"];
|
|
4622
4708
|
if (req.force) args.push("--force");
|
|
4623
4709
|
args.push(wt.path);
|
|
@@ -4717,7 +4803,7 @@ function formatTodoExport({ todo, comments, replyCommand }) {
|
|
|
4717
4803
|
|
|
4718
4804
|
// packages/server/src/feedback.ts
|
|
4719
4805
|
import { realpath as realpath3 } from "fs/promises";
|
|
4720
|
-
import { randomUUID as
|
|
4806
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4721
4807
|
async function real(p) {
|
|
4722
4808
|
try {
|
|
4723
4809
|
return await realpath3(p);
|
|
@@ -4782,7 +4868,7 @@ async function addReply(store, ref, author, body) {
|
|
|
4782
4868
|
return store.update((s) => {
|
|
4783
4869
|
const { list, index } = locate(s, ref);
|
|
4784
4870
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4785
|
-
const reply2 = { id:
|
|
4871
|
+
const reply2 = { id: randomUUID3(), author, body: body.trim(), at: now };
|
|
4786
4872
|
const prev = list[index];
|
|
4787
4873
|
const next = { ...prev, replies: [...prev.replies ?? [], reply2], updatedAt: now };
|
|
4788
4874
|
if (author === "reviewer") {
|
|
@@ -4861,6 +4947,11 @@ async function findSockets(dirs = socketDirs()) {
|
|
|
4861
4947
|
for (const d of dirs) await visit(d, 0);
|
|
4862
4948
|
return [...found.entries()].map(([socket, pid]) => ({ socket, pid }));
|
|
4863
4949
|
}
|
|
4950
|
+
function chooseNvim(matching, wanted) {
|
|
4951
|
+
for (const w of wanted) if (w && matching.some((i) => i.socket === w)) return { socket: w };
|
|
4952
|
+
if (matching.length === 1) return { socket: matching[0].socket };
|
|
4953
|
+
return { error: matching.length ? "ambiguous" : "none" };
|
|
4954
|
+
}
|
|
4864
4955
|
var NvimService = class {
|
|
4865
4956
|
constructor(dirs) {
|
|
4866
4957
|
this.dirs = dirs;
|
|
@@ -4915,6 +5006,7 @@ var NvimService = class {
|
|
|
4915
5006
|
|
|
4916
5007
|
// packages/server/src/tmux.ts
|
|
4917
5008
|
import { execFile as execFile3 } from "child_process";
|
|
5009
|
+
import path7 from "path";
|
|
4918
5010
|
import { realpath as realpath4 } from "fs/promises";
|
|
4919
5011
|
var runTmux = (args) => new Promise((resolve, reject) => {
|
|
4920
5012
|
execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
@@ -4925,34 +5017,48 @@ var runTmux = (args) => new Promise((resolve, reject) => {
|
|
|
4925
5017
|
);
|
|
4926
5018
|
});
|
|
4927
5019
|
});
|
|
5020
|
+
function sessionNameFor(worktreePath) {
|
|
5021
|
+
return path7.basename(worktreePath).replace(/[.:#]/g, "_") || "warden";
|
|
5022
|
+
}
|
|
4928
5023
|
var TmuxService = class {
|
|
4929
5024
|
constructor(run2 = runTmux) {
|
|
4930
5025
|
this.run = run2;
|
|
4931
5026
|
}
|
|
4932
5027
|
run;
|
|
4933
|
-
async
|
|
4934
|
-
const
|
|
4935
|
-
|
|
5028
|
+
async list() {
|
|
5029
|
+
const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]).catch((e) => {
|
|
5030
|
+
if (e instanceof HttpError && /no server running|error connecting/i.test(e.message)) return "";
|
|
5031
|
+
throw e;
|
|
5032
|
+
});
|
|
4936
5033
|
const sessions = [];
|
|
4937
5034
|
for (const line of output.trimEnd().split("\n")) {
|
|
4938
5035
|
const [id, name, ...parts] = line.split(" ");
|
|
4939
|
-
const
|
|
4940
|
-
if (!id || !/^\$\d+$/.test(id) || !name || !
|
|
4941
|
-
|
|
5036
|
+
const dir = parts.join(" ");
|
|
5037
|
+
if (!id || !/^\$\d+$/.test(id) || !name || !dir) continue;
|
|
5038
|
+
sessions.push({ id, name, path: dir });
|
|
4942
5039
|
}
|
|
4943
5040
|
return sessions;
|
|
4944
5041
|
}
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5042
|
+
/**
|
|
5043
|
+
* A detached session in the worktree, named after its directory, with a shell and nothing run in
|
|
5044
|
+
* it. One already there for the same directory is handed back rather than doubled; one of that
|
|
5045
|
+
* name elsewhere is refused, not replaced — it is someone's work.
|
|
5046
|
+
*/
|
|
5047
|
+
async openSession(worktreePath) {
|
|
4948
5048
|
const directory = await realpath4(worktreePath);
|
|
4949
|
-
const
|
|
4950
|
-
|
|
5049
|
+
const name = sessionNameFor(directory);
|
|
5050
|
+
const existing = (await this.list()).find((x) => x.name === name);
|
|
5051
|
+
if (existing) {
|
|
5052
|
+
if (await realpath4(existing.path).catch(() => null) === directory) return { session: name, created: false };
|
|
5053
|
+
throw new HttpError(409, `\u5DF2\u6709\u540C\u540D tmux session ${name}\uFF0C\u5DE5\u4F5C\u76EE\u5F55\u662F ${existing.path}`, "tmux_name_taken");
|
|
5054
|
+
}
|
|
5055
|
+
await this.run(["new-session", "-d", "-s", name, "-c", directory.replaceAll("#", "##")]);
|
|
5056
|
+
return { session: name, created: true };
|
|
4951
5057
|
}
|
|
4952
5058
|
};
|
|
4953
5059
|
|
|
4954
5060
|
// packages/server/src/watcher.ts
|
|
4955
|
-
import
|
|
5061
|
+
import path8 from "path";
|
|
4956
5062
|
import { stat as stat5 } from "fs/promises";
|
|
4957
5063
|
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
4958
5064
|
function statusPaths(out) {
|
|
@@ -5049,7 +5155,7 @@ var RepoWatcher = class {
|
|
|
5049
5155
|
const stats = await Promise.all(
|
|
5050
5156
|
statusPaths(statusRes.stdout).map(async (rel) => {
|
|
5051
5157
|
try {
|
|
5052
|
-
const st = await stat5(
|
|
5158
|
+
const st = await stat5(path8.join(this.root, rel));
|
|
5053
5159
|
return `${rel} ${st.mtimeMs} ${st.size}`;
|
|
5054
5160
|
} catch {
|
|
5055
5161
|
return `${rel} -`;
|
|
@@ -5061,7 +5167,7 @@ var RepoWatcher = class {
|
|
|
5061
5167
|
};
|
|
5062
5168
|
|
|
5063
5169
|
// packages/server/src/static.ts
|
|
5064
|
-
import
|
|
5170
|
+
import path9 from "path";
|
|
5065
5171
|
import { readFile as readFile3, stat as stat6 } from "fs/promises";
|
|
5066
5172
|
var MIME = {
|
|
5067
5173
|
".html": "text/html; charset=utf-8",
|
|
@@ -5079,20 +5185,20 @@ var MIME = {
|
|
|
5079
5185
|
".txt": "text/plain; charset=utf-8"
|
|
5080
5186
|
};
|
|
5081
5187
|
async function serveStaticFile(c, webDir) {
|
|
5082
|
-
const root =
|
|
5188
|
+
const root = path9.resolve(webDir);
|
|
5083
5189
|
let rel;
|
|
5084
5190
|
try {
|
|
5085
5191
|
rel = decodeURIComponent(c.req.path);
|
|
5086
5192
|
} catch {
|
|
5087
5193
|
return c.text("Bad Request", 400);
|
|
5088
5194
|
}
|
|
5089
|
-
let target =
|
|
5090
|
-
if (target !== root && !target.startsWith(root +
|
|
5195
|
+
let target = path9.resolve(root, "." + rel);
|
|
5196
|
+
if (target !== root && !target.startsWith(root + path9.sep)) return c.text("Forbidden", 403);
|
|
5091
5197
|
try {
|
|
5092
5198
|
const st = await stat6(target);
|
|
5093
|
-
if (st.isDirectory()) target =
|
|
5199
|
+
if (st.isDirectory()) target = path9.join(target, "index.html");
|
|
5094
5200
|
} catch {
|
|
5095
|
-
target =
|
|
5201
|
+
target = path9.join(root, "index.html");
|
|
5096
5202
|
}
|
|
5097
5203
|
let body;
|
|
5098
5204
|
try {
|
|
@@ -5100,7 +5206,7 @@ async function serveStaticFile(c, webDir) {
|
|
|
5100
5206
|
} catch {
|
|
5101
5207
|
return c.text("Not Found", 404);
|
|
5102
5208
|
}
|
|
5103
|
-
const ext =
|
|
5209
|
+
const ext = path9.extname(target).toLowerCase();
|
|
5104
5210
|
const isAsset = rel.startsWith("/assets/");
|
|
5105
5211
|
return new Response(new Uint8Array(body), {
|
|
5106
5212
|
status: 200,
|
|
@@ -5172,8 +5278,8 @@ function createApp(opts) {
|
|
|
5172
5278
|
const checkpointHandoff = (comments) => {
|
|
5173
5279
|
const worktrees2 = new Set(comments.map((x) => tryParseTargetKey(x.targetKey)?.worktree));
|
|
5174
5280
|
for (const wt of worktrees2) {
|
|
5175
|
-
takeCheckpoint(store, wt ?? repo.
|
|
5176
|
-
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.
|
|
5281
|
+
takeCheckpoint(store, wt ?? repo.commonRoot, wt, { handoff: true }).catch((e) => {
|
|
5282
|
+
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.commonRoot}: ${e instanceof Error ? e.message : e}`);
|
|
5177
5283
|
});
|
|
5178
5284
|
}
|
|
5179
5285
|
};
|
|
@@ -5218,16 +5324,21 @@ function createApp(opts) {
|
|
|
5218
5324
|
api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
|
|
5219
5325
|
api.get("/repo", async (c) => {
|
|
5220
5326
|
const state = await store.load();
|
|
5221
|
-
const
|
|
5327
|
+
const own = repo.root === repo.commonRoot ? "working" : formatTargetKey({ kind: "working", worktree: repo.root });
|
|
5328
|
+
const info = await getRepoInfo(repo, own);
|
|
5222
5329
|
worktreeCache = { at: Date.now(), list: info.worktrees };
|
|
5223
5330
|
const last = state.prefs.lastTarget;
|
|
5224
5331
|
const target = last ? tryParseTargetKey(last) : void 0;
|
|
5225
|
-
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree)) &&
|
|
5332
|
+
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree)) && // A server started in a linked worktree is there to review that one, not whatever the main
|
|
5333
|
+
// worktree's page last looked at: the preference is shared by every server on the repository.
|
|
5334
|
+
(repo.root === repo.commonRoot || target.worktree === repo.root) && (target.kind !== "checkpoint" || !!findCheckpoint(state, target.worktree, target.id));
|
|
5226
5335
|
if (last && usable2) info.defaultTarget = last;
|
|
5227
5336
|
else if (last) {
|
|
5228
|
-
|
|
5229
|
-
s
|
|
5230
|
-
|
|
5337
|
+
if (repo.root === repo.commonRoot) {
|
|
5338
|
+
await store.update((s) => {
|
|
5339
|
+
s.prefs.lastTarget = "working";
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5231
5342
|
}
|
|
5232
5343
|
return c.json(info);
|
|
5233
5344
|
});
|
|
@@ -5383,7 +5494,7 @@ function createApp(opts) {
|
|
|
5383
5494
|
if (!anchored) throw badRequest("selection is not fully visible in the diff on that side (must be inside one hunk)", "bad_selection");
|
|
5384
5495
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5385
5496
|
const comment = {
|
|
5386
|
-
id:
|
|
5497
|
+
id: randomUUID4(),
|
|
5387
5498
|
targetKey: key,
|
|
5388
5499
|
filePath: body.filePath,
|
|
5389
5500
|
side: body.side,
|
|
@@ -5507,7 +5618,6 @@ function createApp(opts) {
|
|
|
5507
5618
|
const comments = state.targets[scope]?.comments ?? [];
|
|
5508
5619
|
const prevHead = state.targets[scope]?.head;
|
|
5509
5620
|
const head = await revParse(ctx.cwd, "HEAD") ?? "";
|
|
5510
|
-
const committed = local && prevHead !== void 0 && prevHead !== head;
|
|
5511
5621
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5512
5622
|
const located = /* @__PURE__ */ new Map();
|
|
5513
5623
|
await Promise.all(
|
|
@@ -5531,6 +5641,19 @@ function createApp(opts) {
|
|
|
5531
5641
|
}
|
|
5532
5642
|
})
|
|
5533
5643
|
);
|
|
5644
|
+
const landed = /* @__PURE__ */ new Set();
|
|
5645
|
+
const lost = comments.filter((cm) => !located.has(cm.id));
|
|
5646
|
+
if (local && lost.length && prevHead && head && prevHead !== head && await mergeBase(ctx.cwd, prevHead, head) === prevHead) {
|
|
5647
|
+
const range = await worktreeCtx(
|
|
5648
|
+
formatTargetKey({ kind: "range", base: prevHead, head, ...ctx.target.worktree ? { worktree: ctx.target.worktree } : {} })
|
|
5649
|
+
);
|
|
5650
|
+
await Promise.all(
|
|
5651
|
+
lost.map(async (cm) => {
|
|
5652
|
+
const committedDiff = await getFileDiff(range, cm.filePath).catch(() => void 0);
|
|
5653
|
+
if (committedDiff && reanchorComment(cm, committedDiff, now).status !== "orphaned") landed.add(cm.id);
|
|
5654
|
+
})
|
|
5655
|
+
);
|
|
5656
|
+
}
|
|
5534
5657
|
const result = await store.update((s) => {
|
|
5535
5658
|
const t = ensureTarget(s, scope);
|
|
5536
5659
|
const dropped = [];
|
|
@@ -5538,7 +5661,7 @@ function createApp(opts) {
|
|
|
5538
5661
|
const patch = located.get(cm.id);
|
|
5539
5662
|
if (!patch) {
|
|
5540
5663
|
if (!comments.some((x) => x.id === cm.id)) return [cm];
|
|
5541
|
-
if (
|
|
5664
|
+
if (landed.has(cm.id) && !cm.replies?.length) {
|
|
5542
5665
|
dropped.push(cm.id);
|
|
5543
5666
|
return [];
|
|
5544
5667
|
}
|
|
@@ -5602,7 +5725,7 @@ function createApp(opts) {
|
|
|
5602
5725
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5603
5726
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5604
5727
|
const todo = {
|
|
5605
|
-
id:
|
|
5728
|
+
id: randomUUID4(),
|
|
5606
5729
|
branch,
|
|
5607
5730
|
title: body.title.trim(),
|
|
5608
5731
|
body: typeof body.body === "string" ? body.body : "",
|
|
@@ -5705,13 +5828,14 @@ function createApp(opts) {
|
|
|
5705
5828
|
if (ref && !isValidRef(ref)) throw badRequest("invalid ref");
|
|
5706
5829
|
if (filePath && (filePath.startsWith("/") || filePath.split("/").includes(".."))) throw badRequest("invalid path");
|
|
5707
5830
|
if (q.length > 200 || author.length > 200) throw badRequest("search text too long");
|
|
5708
|
-
const args = ["log", `--max-count=${limit + 1}`, `--skip=${offset}`, `--format=${COMMIT_FORMAT}`];
|
|
5831
|
+
const args = ["log", "--no-show-signature", `--max-count=${limit + 1}`, `--skip=${offset}`, `--format=${COMMIT_FORMAT}`];
|
|
5709
5832
|
if (firstParent) args.push("--first-parent");
|
|
5710
5833
|
if (q || author) args.push("--fixed-strings", "--regexp-ignore-case");
|
|
5711
5834
|
if (q) args.push(`--grep=${q}`);
|
|
5712
5835
|
if (author) args.push(`--author=${author}`);
|
|
5713
5836
|
if (ref) args.push(ref);
|
|
5714
|
-
|
|
5837
|
+
args.push("--");
|
|
5838
|
+
if (filePath) args.push(literal(filePath));
|
|
5715
5839
|
let stdout = "";
|
|
5716
5840
|
try {
|
|
5717
5841
|
stdout = (await runGit(args, { cwd })).stdout;
|
|
@@ -5725,7 +5849,7 @@ function createApp(opts) {
|
|
|
5725
5849
|
if (q && offset === 0 && /^[0-9a-f]{4,40}$/i.test(q)) {
|
|
5726
5850
|
const sha = await revParse(cwd, `${q}^{commit}`);
|
|
5727
5851
|
if (sha && !commits.some((x) => x.sha === sha)) {
|
|
5728
|
-
const hit = parseCommitLog((await runGit(["log", "--max-count=1", `--format=${COMMIT_FORMAT}`, sha], { cwd })).stdout)[0];
|
|
5852
|
+
const hit = parseCommitLog((await runGit(["log", "--no-show-signature", "--max-count=1", `--format=${COMMIT_FORMAT}`, sha, "--"], { cwd })).stdout)[0];
|
|
5729
5853
|
if (hit) commits = [hit, ...commits];
|
|
5730
5854
|
}
|
|
5731
5855
|
}
|
|
@@ -5744,13 +5868,13 @@ function createApp(opts) {
|
|
|
5744
5868
|
return c.json(res);
|
|
5745
5869
|
});
|
|
5746
5870
|
const tmux = opts.tmux ?? new TmuxService();
|
|
5747
|
-
api.
|
|
5748
|
-
api.post("/tmux/windows", async (c) => {
|
|
5871
|
+
api.post("/tmux/sessions", async (c) => {
|
|
5749
5872
|
const body = await c.req.json();
|
|
5750
|
-
if (!body || typeof body.path !== "string"
|
|
5873
|
+
if (!body || typeof body.path !== "string") throw badRequest("path is required");
|
|
5751
5874
|
const wt = (await worktrees(true)).find((w) => w.path === body.path && !w.bare);
|
|
5752
5875
|
if (!wt) throw badRequest("unknown worktree", "unknown_worktree");
|
|
5753
|
-
|
|
5876
|
+
const res = await tmux.openSession(wt.path);
|
|
5877
|
+
return c.json(res, res.created ? 201 : 200);
|
|
5754
5878
|
});
|
|
5755
5879
|
api.get("/worktrees", async (c) => {
|
|
5756
5880
|
const [list, state] = await Promise.all([listWorktreesDetailed(repo), store.load()]);
|
|
@@ -5759,7 +5883,7 @@ function createApp(opts) {
|
|
|
5759
5883
|
const latest = /* @__PURE__ */ new Map();
|
|
5760
5884
|
for (const t of Object.values(state.targets)) {
|
|
5761
5885
|
for (const cm of t.comments) {
|
|
5762
|
-
const at = commentWorktree(cm) ?? repo.
|
|
5886
|
+
const at = commentWorktree(cm) ?? repo.commonRoot;
|
|
5763
5887
|
const r = review.get(at) ?? { toAgent: 0, toReviewer: 0 };
|
|
5764
5888
|
const seen = latest.get(at) ?? { agent: "", reviewer: "" };
|
|
5765
5889
|
if (awaitsAgent(cm)) {
|
|
@@ -5785,12 +5909,18 @@ function createApp(opts) {
|
|
|
5785
5909
|
const res = { remotes: await lookupRemoteBranches(repo, c.req.query("branch") ?? "") };
|
|
5786
5910
|
return c.json(res);
|
|
5787
5911
|
});
|
|
5912
|
+
let worktreeWrites = Promise.resolve();
|
|
5913
|
+
const oneAtATime = (fn) => {
|
|
5914
|
+
const run2 = worktreeWrites.then(fn, fn);
|
|
5915
|
+
worktreeWrites = run2.catch(() => void 0);
|
|
5916
|
+
return run2;
|
|
5917
|
+
};
|
|
5788
5918
|
api.post("/worktrees", async (c) => {
|
|
5789
5919
|
const body = await c.req.json();
|
|
5790
5920
|
if (typeof body.branch !== "string") throw badRequest("branch is required");
|
|
5791
5921
|
if (body.base !== void 0 && typeof body.base !== "string") throw badRequest("base must be a ref");
|
|
5792
5922
|
if (body.slot !== void 0 && (!Number.isInteger(body.slot) || body.slot < 1)) throw badRequest("slot must be a positive integer", "invalid_slot");
|
|
5793
|
-
const res = await checkoutWorktree(repo, body);
|
|
5923
|
+
const res = await oneAtATime(() => checkoutWorktree(repo, body));
|
|
5794
5924
|
worktreeCache = null;
|
|
5795
5925
|
await store.update((s) => forgetWorktreeTargets(s, res.worktree.path));
|
|
5796
5926
|
return c.json(res, 201);
|
|
@@ -5798,14 +5928,14 @@ function createApp(opts) {
|
|
|
5798
5928
|
api.post("/worktrees/release", async (c) => {
|
|
5799
5929
|
const body = await c.req.json();
|
|
5800
5930
|
if (typeof body.path !== "string") throw badRequest("path is required");
|
|
5801
|
-
const res = await releaseWorktree(repo, body);
|
|
5931
|
+
const res = await oneAtATime(() => releaseWorktree(repo, body));
|
|
5802
5932
|
worktreeCache = null;
|
|
5803
5933
|
return c.json(res);
|
|
5804
5934
|
});
|
|
5805
5935
|
api.post("/worktrees/remove", async (c) => {
|
|
5806
5936
|
const body = await c.req.json();
|
|
5807
5937
|
if (typeof body.path !== "string") throw badRequest("path is required");
|
|
5808
|
-
const res = await removeWorktree(repo, body);
|
|
5938
|
+
const res = await oneAtATime(() => removeWorktree(repo, body));
|
|
5809
5939
|
worktreeCache = null;
|
|
5810
5940
|
return c.json(res);
|
|
5811
5941
|
});
|
|
@@ -5856,9 +5986,8 @@ function createApp(opts) {
|
|
|
5856
5986
|
const instances = NvimService.matching(scan.instances, root);
|
|
5857
5987
|
const state = await store.load();
|
|
5858
5988
|
const preferred = state.prefs.nvimSocketByRoot[root];
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
else if (instances.length === 1) selected = instances[0].socket;
|
|
5989
|
+
const choice = chooseNvim(instances, [preferred]);
|
|
5990
|
+
const selected = "socket" in choice ? choice.socket : void 0;
|
|
5862
5991
|
const res = { root, nvimAvailable: scan.nvimAvailable, instances, selected, scannedAt: scan.scannedAt };
|
|
5863
5992
|
return c.json(res);
|
|
5864
5993
|
});
|
|
@@ -5872,19 +6001,38 @@ function createApp(opts) {
|
|
|
5872
6001
|
});
|
|
5873
6002
|
api.post("/nvim/open", async (c) => {
|
|
5874
6003
|
const body = await c.req.json();
|
|
5875
|
-
if (!body.
|
|
5876
|
-
if (!
|
|
5877
|
-
const
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6004
|
+
if (!body.absPath) throw badRequest("absPath is required");
|
|
6005
|
+
if (!path10.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
|
|
6006
|
+
const root = await knownRoot(body.root);
|
|
6007
|
+
const wanted = [body.socket, (await store.load()).prefs.nvimSocketByRoot[root]];
|
|
6008
|
+
const line = Number(body.line) || 1;
|
|
6009
|
+
const pick = async (force) => {
|
|
6010
|
+
const scan = await nvim.scan(force);
|
|
6011
|
+
if (!scan.nvimAvailable) throw badRequest("nvim is not on PATH", "nvim_unavailable");
|
|
6012
|
+
return { matching: NvimService.matching(scan.instances, root), choice: chooseNvim(NvimService.matching(scan.instances, root), wanted) };
|
|
6013
|
+
};
|
|
6014
|
+
let { choice } = await pick(false);
|
|
6015
|
+
if ("error" in choice) choice = (await pick(true)).choice;
|
|
6016
|
+
if ("error" in choice) {
|
|
6017
|
+
if (choice.error === "none") throw notFound(`no nvim is open inside ${root}`, "nvim_none");
|
|
6018
|
+
throw new HttpError(409, `several nvim instances are open inside ${root}; select one`, "nvim_ambiguous");
|
|
5881
6019
|
}
|
|
5882
6020
|
try {
|
|
5883
|
-
await nvim.open(
|
|
6021
|
+
await nvim.open(choice.socket, body.absPath, line);
|
|
6022
|
+
const res = { ok: true, socket: choice.socket };
|
|
6023
|
+
return c.json(res);
|
|
5884
6024
|
} catch (e) {
|
|
5885
|
-
|
|
6025
|
+
const failure = new HttpError(502, e instanceof Error ? e.message : String(e), "nvim_failed");
|
|
6026
|
+
const fresh = await pick(true);
|
|
6027
|
+
if (fresh.matching.some((i) => i.socket === choice.socket) || "error" in fresh.choice) throw failure;
|
|
6028
|
+
try {
|
|
6029
|
+
await nvim.open(fresh.choice.socket, body.absPath, line);
|
|
6030
|
+
} catch (e2) {
|
|
6031
|
+
throw new HttpError(502, e2 instanceof Error ? e2.message : String(e2), "nvim_failed");
|
|
6032
|
+
}
|
|
6033
|
+
const res = { ok: true, socket: fresh.choice.socket };
|
|
6034
|
+
return c.json(res);
|
|
5886
6035
|
}
|
|
5887
|
-
return c.json({ ok: true });
|
|
5888
6036
|
});
|
|
5889
6037
|
app.route("/api", api);
|
|
5890
6038
|
app.notFound((c) => {
|
|
@@ -5933,14 +6081,14 @@ function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
|
5933
6081
|
}
|
|
5934
6082
|
|
|
5935
6083
|
// packages/server/src/update.ts
|
|
5936
|
-
import
|
|
5937
|
-
import { mkdir as mkdir3, readFile as readFile5, writeFile
|
|
6084
|
+
import path11 from "path";
|
|
6085
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile } from "fs/promises";
|
|
5938
6086
|
var PACKAGE = "@xbghc/warden";
|
|
5939
6087
|
var LATEST_URL = "https://registry.npmjs.org/@xbghc%2fwarden/latest";
|
|
5940
6088
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
5941
6089
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
5942
6090
|
function updateCacheFile(baseDir = dataDir()) {
|
|
5943
|
-
return
|
|
6091
|
+
return path11.join(baseDir, "update-check.json");
|
|
5944
6092
|
}
|
|
5945
6093
|
function updateCheckEnabled(env = process.env) {
|
|
5946
6094
|
return !env.WARDEN_NO_UPDATE_CHECK && !env.NO_UPDATE_NOTIFIER && !env.CI;
|
|
@@ -5974,8 +6122,8 @@ async function readCache(file, now) {
|
|
|
5974
6122
|
}
|
|
5975
6123
|
async function writeCache(file, cache) {
|
|
5976
6124
|
try {
|
|
5977
|
-
await mkdir3(
|
|
5978
|
-
await
|
|
6125
|
+
await mkdir3(path11.dirname(file), { recursive: true });
|
|
6126
|
+
await writeFile(file, JSON.stringify(cache));
|
|
5979
6127
|
} catch {
|
|
5980
6128
|
}
|
|
5981
6129
|
}
|
|
@@ -6019,7 +6167,7 @@ async function startServer(opts) {
|
|
|
6019
6167
|
const repo = await resolveRepo(opts.repoPath);
|
|
6020
6168
|
const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
|
|
6021
6169
|
const store = new StateStore(stateFile, repo.commonRoot);
|
|
6022
|
-
const instanceToken =
|
|
6170
|
+
const instanceToken = randomUUID5();
|
|
6023
6171
|
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update, replyCommand: opts.replyCommand });
|
|
6024
6172
|
const listen = (p) => new Promise((resolve, reject) => {
|
|
6025
6173
|
const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
|
|
@@ -6123,7 +6271,7 @@ async function runAgentCommand(argv, replyCommand) {
|
|
|
6123
6271
|
}
|
|
6124
6272
|
|
|
6125
6273
|
// bin/cli.ts
|
|
6126
|
-
var VERSION = true ? "0.16.
|
|
6274
|
+
var VERSION = true ? "0.16.1" : "dev";
|
|
6127
6275
|
function parseArgs(argv) {
|
|
6128
6276
|
const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
|
|
6129
6277
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -6142,11 +6290,11 @@ function parseArgs(argv) {
|
|
|
6142
6290
|
} else if (a === "--help" || a === "-h") args.help = true;
|
|
6143
6291
|
else if (a === "--version" || a === "-v") args.version = true;
|
|
6144
6292
|
else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
|
|
6145
|
-
else args.repoPath =
|
|
6293
|
+
else args.repoPath = path12.resolve(a);
|
|
6146
6294
|
}
|
|
6147
6295
|
return args;
|
|
6148
6296
|
}
|
|
6149
|
-
var viaNpx = fileURLToPath(import.meta.url).includes(`${
|
|
6297
|
+
var viaNpx = fileURLToPath(import.meta.url).includes(`${path12.sep}_npx${path12.sep}`);
|
|
6150
6298
|
var selfCommand = viaNpx ? "npx @xbghc/warden" : "warden";
|
|
6151
6299
|
function usage() {
|
|
6152
6300
|
return `warden \u2014 local git diff review UI
|
|
@@ -6193,16 +6341,16 @@ async function openBrowser(url) {
|
|
|
6193
6341
|
return false;
|
|
6194
6342
|
}
|
|
6195
6343
|
function resolveWebDir() {
|
|
6196
|
-
const here =
|
|
6344
|
+
const here = path12.dirname(fileURLToPath(import.meta.url));
|
|
6197
6345
|
const candidates = [
|
|
6198
6346
|
process.env.WARDEN_WEB_DIR,
|
|
6199
|
-
|
|
6347
|
+
path12.join(here, "web"),
|
|
6200
6348
|
// dist/cli.js -> dist/web
|
|
6201
|
-
|
|
6349
|
+
path12.join(here, "..", "dist", "web")
|
|
6202
6350
|
// bin/cli.ts (tsx dev) -> dist/web
|
|
6203
6351
|
].filter((p) => !!p);
|
|
6204
6352
|
for (const dir of candidates) {
|
|
6205
|
-
if (existsSync2(
|
|
6353
|
+
if (existsSync2(path12.join(dir, "index.html"))) return dir;
|
|
6206
6354
|
}
|
|
6207
6355
|
return void 0;
|
|
6208
6356
|
}
|