@xbghc/warden 0.16.0 → 0.16.2
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");
|
|
@@ -4026,12 +4066,21 @@ async function listUntracked(cwd) {
|
|
|
4026
4066
|
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z"], { cwd });
|
|
4027
4067
|
return r.stdout.split("\0").filter(Boolean);
|
|
4028
4068
|
}
|
|
4069
|
+
async function listUnmerged(cwd, file) {
|
|
4070
|
+
const r = await runGit(["ls-files", "--unmerged", "-z", ...file ? ["--", literal(file)] : []], { cwd });
|
|
4071
|
+
return [...new Set(r.stdout.split("\0").flatMap((e) => e.includes(" ") ? [e.slice(e.indexOf(" ") + 1)] : []))];
|
|
4072
|
+
}
|
|
4073
|
+
async function conflictDiff(ctx, file) {
|
|
4074
|
+
const base = await hasHead(ctx.cwd) ? "HEAD" : EMPTY_TREE_SHA;
|
|
4075
|
+
const f = parseUnifiedDiff(await runDiff(ctx, [...DIFF_BASE_ARGS, base, "--", literal(file)])).find((d) => d.path === file);
|
|
4076
|
+
return f ? { ...f, conflicted: true } : void 0;
|
|
4077
|
+
}
|
|
4029
4078
|
async function isUntracked(cwd, file) {
|
|
4030
4079
|
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", literal(file)], { cwd });
|
|
4031
4080
|
return r.stdout.split("\0").filter(Boolean).includes(file);
|
|
4032
4081
|
}
|
|
4033
4082
|
async function untrackedDiff(cwd, file) {
|
|
4034
|
-
const r = await runGit(["diff",
|
|
4083
|
+
const r = await runGit(["diff", ...DIFF_OUTPUT_ARGS, "--no-index", "--", "/dev/null", file], {
|
|
4035
4084
|
cwd,
|
|
4036
4085
|
okCodes: [0, 1]
|
|
4037
4086
|
});
|
|
@@ -4058,7 +4107,16 @@ async function mapLimit(items, limit, fn) {
|
|
|
4058
4107
|
}
|
|
4059
4108
|
async function listTargetDiffs(ctx) {
|
|
4060
4109
|
const args = await diffArgs(ctx);
|
|
4061
|
-
const files = parseUnifiedDiff(await runDiff(ctx, args));
|
|
4110
|
+
const files = parseUnifiedDiff(await runDiff(ctx, [...args, "--"]));
|
|
4111
|
+
if (ctx.target.kind === "working") {
|
|
4112
|
+
for (const path13 of await listUnmerged(ctx.cwd)) {
|
|
4113
|
+
const d = await conflictDiff(ctx, path13);
|
|
4114
|
+
if (!d) continue;
|
|
4115
|
+
const at = files.findIndex((f) => f.path === path13);
|
|
4116
|
+
if (at >= 0) files[at] = d;
|
|
4117
|
+
else files.push(d);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4062
4120
|
if (includesUntracked(ctx.target)) {
|
|
4063
4121
|
const untracked = await listUntracked(ctx.cwd);
|
|
4064
4122
|
const extra = await mapLimit(untracked, 8, (f) => untrackedDiff(ctx.cwd, f).catch(() => void 0));
|
|
@@ -4077,6 +4135,7 @@ async function getFileDiff(ctx, filePath, hints = {}) {
|
|
|
4077
4135
|
if (includesUntracked(ctx.target) && (hints.untracked || await isUntracked(ctx.cwd, filePath))) {
|
|
4078
4136
|
return untrackedDiff(ctx.cwd, filePath);
|
|
4079
4137
|
}
|
|
4138
|
+
if (ctx.target.kind === "working" && (await listUnmerged(ctx.cwd, filePath)).includes(filePath)) return conflictDiff(ctx, filePath);
|
|
4080
4139
|
const args = await diffArgs(ctx);
|
|
4081
4140
|
const pathspec = [literal(filePath)];
|
|
4082
4141
|
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
|
|
@@ -4128,7 +4187,7 @@ async function pathsWithMarkers(ctx, side, paths) {
|
|
|
4128
4187
|
const found = /* @__PURE__ */ new Set();
|
|
4129
4188
|
if (paths.length === 0) return found;
|
|
4130
4189
|
const ref = await refForSide(ctx, side);
|
|
4131
|
-
const args = ["grep", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
|
|
4190
|
+
const args = ["grep", "--no-color", "--no-recurse-submodules", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
|
|
4132
4191
|
if (ref === void 0) args.push("--untracked");
|
|
4133
4192
|
else if (ref === ":0") args.push("--cached");
|
|
4134
4193
|
else args.push(ref);
|
|
@@ -4223,6 +4282,12 @@ function matchesAt(lines, start, hashes) {
|
|
|
4223
4282
|
}
|
|
4224
4283
|
return true;
|
|
4225
4284
|
}
|
|
4285
|
+
var DISTINCTIVE_CHARS = 24;
|
|
4286
|
+
function distinctive(lines, start, count) {
|
|
4287
|
+
let chars = 0;
|
|
4288
|
+
for (let i = start; i < start + count; i++) chars += lines[i].content.replace(/\s+/g, "").length;
|
|
4289
|
+
return chars >= DISTINCTIVE_CHARS;
|
|
4290
|
+
}
|
|
4226
4291
|
function contextScore(lines, start, count, anchor) {
|
|
4227
4292
|
let score = 0;
|
|
4228
4293
|
const before = anchor.contextBefore;
|
|
@@ -4251,7 +4316,10 @@ function locateAnchor(diff, side, anchor) {
|
|
|
4251
4316
|
if (matchesAt(lines, i, anchor.lineHashes)) candidates.push(i);
|
|
4252
4317
|
}
|
|
4253
4318
|
if (candidates.length === 0) return void 0;
|
|
4254
|
-
if (candidates.length === 1)
|
|
4319
|
+
if (candidates.length === 1) {
|
|
4320
|
+
const c = candidates[0];
|
|
4321
|
+
return contextScore(lines, c, n, anchor) > 0 || distinctive(lines, c, n) ? c : void 0;
|
|
4322
|
+
}
|
|
4255
4323
|
let best = [];
|
|
4256
4324
|
let bestScore = -1;
|
|
4257
4325
|
for (const c of candidates) {
|
|
@@ -4444,6 +4512,39 @@ async function listUpstreams(ctx) {
|
|
|
4444
4512
|
}
|
|
4445
4513
|
return out;
|
|
4446
4514
|
}
|
|
4515
|
+
async function commonDirOf(dir) {
|
|
4516
|
+
try {
|
|
4517
|
+
const r = await runGit(["rev-parse", "--show-toplevel", "--git-common-dir"], { cwd: dir });
|
|
4518
|
+
const [top, common] = r.stdout.trim().split("\n");
|
|
4519
|
+
if (!top || !common || await realpath2(top) !== await realpath2(dir)) return void 0;
|
|
4520
|
+
return await realpath2(path5.resolve(dir, common));
|
|
4521
|
+
} catch {
|
|
4522
|
+
return void 0;
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
var IN_PROGRESS = [
|
|
4526
|
+
["rebase-merge", "rebase"],
|
|
4527
|
+
["rebase-apply", "rebase"],
|
|
4528
|
+
["MERGE_HEAD", "merge"],
|
|
4529
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
4530
|
+
["REVERT_HEAD", "revert"],
|
|
4531
|
+
["BISECT_LOG", "bisect"]
|
|
4532
|
+
];
|
|
4533
|
+
async function checkoutState(ctx, dir) {
|
|
4534
|
+
const [ours, theirs] = await Promise.all([commonDirOf(ctx.commonRoot), commonDirOf(dir)]);
|
|
4535
|
+
if (!ours || ours !== theirs) return { foreign: true };
|
|
4536
|
+
const r = await runGit(["rev-parse", ...IN_PROGRESS.flatMap(([f]) => ["--git-path", f])], { cwd: dir });
|
|
4537
|
+
const paths = r.stdout.trim().split("\n");
|
|
4538
|
+
for (const [i, [, what]] of IN_PROGRESS.entries()) {
|
|
4539
|
+
const p = paths[i];
|
|
4540
|
+
if (p && await stat3(path5.resolve(dir, p)).then(
|
|
4541
|
+
() => true,
|
|
4542
|
+
() => false
|
|
4543
|
+
))
|
|
4544
|
+
return { foreign: false, busy: what };
|
|
4545
|
+
}
|
|
4546
|
+
return { foreign: false };
|
|
4547
|
+
}
|
|
4447
4548
|
async function listWorktreesDetailed(ctx) {
|
|
4448
4549
|
const [all, upstreams] = await Promise.all([listWorktreesAll(ctx), listUpstreams(ctx)]);
|
|
4449
4550
|
const main2 = all.find((w) => w.isMain);
|
|
@@ -4451,12 +4552,16 @@ async function listWorktreesDetailed(ctx) {
|
|
|
4451
4552
|
all.map(async (w) => {
|
|
4452
4553
|
const slot = w.isMain || w.bare ? void 0 : slotOf(ctx, w.path);
|
|
4453
4554
|
const dirty = w.prunable || w.bare ? 0 : await dirtyCount(w.path);
|
|
4555
|
+
const state = slot === void 0 || w.prunable ? { foreign: false } : await checkoutState(ctx, w.path);
|
|
4454
4556
|
const detail = {
|
|
4455
4557
|
...w,
|
|
4456
4558
|
dirty,
|
|
4457
4559
|
...slot === void 0 ? {} : { slot },
|
|
4458
|
-
|
|
4459
|
-
|
|
4560
|
+
...state.foreign ? { foreign: true } : {},
|
|
4561
|
+
...state.busy ? { busy: state.busy } : {},
|
|
4562
|
+
// Free is what the next checkout may take over: a detached HEAD, nothing that would be lost,
|
|
4563
|
+
// nothing stopped halfway, and a checkout that is this repository's own.
|
|
4564
|
+
free: slot !== void 0 && !w.prunable && w.detached && dirty === 0 && !state.foreign && !state.busy
|
|
4460
4565
|
};
|
|
4461
4566
|
if (detail.free) return detail;
|
|
4462
4567
|
const upstream = w.branch ? upstreams.get(w.branch) : void 0;
|
|
@@ -4524,6 +4629,8 @@ async function pickSlot(ctx, worktrees, wanted) {
|
|
|
4524
4629
|
}
|
|
4525
4630
|
const have = slots.find((w) => w.slot === wanted);
|
|
4526
4631
|
if (have?.prunable) throw new HttpError(409, `the directory of slot ${wanted} is gone; remove its entry first`, "slot_gone");
|
|
4632
|
+
if (have?.foreign) throw new HttpError(409, `${have.path} is not a checkout of this repository`, "not_our_checkout");
|
|
4633
|
+
if (have?.busy) throw new HttpError(409, `a ${have.busy} is in progress in slot ${wanted}`, "operation_in_progress");
|
|
4527
4634
|
if (have && !have.free) throw new HttpError(409, `slot ${wanted} is in use${have.branch ? ` by ${have.branch}` : ""}`, "slot_in_use");
|
|
4528
4635
|
if (have) return { slot: wanted, path: have.path, reuse: true };
|
|
4529
4636
|
const p = slotPath(ctx, wanted);
|
|
@@ -4604,6 +4711,9 @@ async function releaseWorktree(ctx, req) {
|
|
|
4604
4711
|
if (wt.isMain) throw badRequest("the main worktree cannot be released", "main_worktree");
|
|
4605
4712
|
if (wt.bare || slotOf(ctx, wt.path) === void 0) throw badRequest(`${wt.path} is not a slot; remove it instead`, "not_a_slot");
|
|
4606
4713
|
if (wt.prunable) throw badRequest(`the directory of ${wt.path} is gone; remove its entry instead`, "worktree_gone");
|
|
4714
|
+
const state = await checkoutState(ctx, wt.path);
|
|
4715
|
+
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");
|
|
4716
|
+
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
4717
|
const dirty = await dirtyCount(wt.path);
|
|
4608
4718
|
if (dirty > 0 && !req.force) throw new HttpError(409, `${wt.path} has uncommitted changes`, "worktree_dirty");
|
|
4609
4719
|
if (dirty > 0) {
|
|
@@ -4618,6 +4728,10 @@ async function removeWorktree(ctx, req) {
|
|
|
4618
4728
|
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4619
4729
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4620
4730
|
if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
|
|
4731
|
+
if (!wt.prunable && !req.force) {
|
|
4732
|
+
const state = await checkoutState(ctx, wt.path);
|
|
4733
|
+
if (state.busy) throw new HttpError(409, `a ${state.busy} is in progress in ${wt.path}; removing it throws that away`, "needs_force");
|
|
4734
|
+
}
|
|
4621
4735
|
const args = ["worktree", "remove"];
|
|
4622
4736
|
if (req.force) args.push("--force");
|
|
4623
4737
|
args.push(wt.path);
|
|
@@ -4717,7 +4831,7 @@ function formatTodoExport({ todo, comments, replyCommand }) {
|
|
|
4717
4831
|
|
|
4718
4832
|
// packages/server/src/feedback.ts
|
|
4719
4833
|
import { realpath as realpath3 } from "fs/promises";
|
|
4720
|
-
import { randomUUID as
|
|
4834
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4721
4835
|
async function real(p) {
|
|
4722
4836
|
try {
|
|
4723
4837
|
return await realpath3(p);
|
|
@@ -4746,9 +4860,16 @@ async function commentsIn(state, worktreeRoot, mainRoot) {
|
|
|
4746
4860
|
return out;
|
|
4747
4861
|
}
|
|
4748
4862
|
async function takeFeedback(store, opts) {
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4863
|
+
const waiting = (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
|
|
4864
|
+
if (opts.peek) return waiting;
|
|
4865
|
+
return markHandedOver(
|
|
4866
|
+
store,
|
|
4867
|
+
waiting.map((c) => c.id)
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4870
|
+
async function markHandedOver(store, commentIds) {
|
|
4871
|
+
return store.update((s) => {
|
|
4872
|
+
const ids = new Set(commentIds);
|
|
4752
4873
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4753
4874
|
const taken = [];
|
|
4754
4875
|
for (const t of Object.values(s.targets)) {
|
|
@@ -4782,7 +4903,7 @@ async function addReply(store, ref, author, body) {
|
|
|
4782
4903
|
return store.update((s) => {
|
|
4783
4904
|
const { list, index } = locate(s, ref);
|
|
4784
4905
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4785
|
-
const reply2 = { id:
|
|
4906
|
+
const reply2 = { id: randomUUID3(), author, body: body.trim(), at: now };
|
|
4786
4907
|
const prev = list[index];
|
|
4787
4908
|
const next = { ...prev, replies: [...prev.replies ?? [], reply2], updatedAt: now };
|
|
4788
4909
|
if (author === "reviewer") {
|
|
@@ -4861,6 +4982,11 @@ async function findSockets(dirs = socketDirs()) {
|
|
|
4861
4982
|
for (const d of dirs) await visit(d, 0);
|
|
4862
4983
|
return [...found.entries()].map(([socket, pid]) => ({ socket, pid }));
|
|
4863
4984
|
}
|
|
4985
|
+
function chooseNvim(matching, wanted) {
|
|
4986
|
+
for (const w of wanted) if (w && matching.some((i) => i.socket === w)) return { socket: w };
|
|
4987
|
+
if (matching.length === 1) return { socket: matching[0].socket };
|
|
4988
|
+
return { error: matching.length ? "ambiguous" : "none" };
|
|
4989
|
+
}
|
|
4864
4990
|
var NvimService = class {
|
|
4865
4991
|
constructor(dirs) {
|
|
4866
4992
|
this.dirs = dirs;
|
|
@@ -4915,6 +5041,7 @@ var NvimService = class {
|
|
|
4915
5041
|
|
|
4916
5042
|
// packages/server/src/tmux.ts
|
|
4917
5043
|
import { execFile as execFile3 } from "child_process";
|
|
5044
|
+
import path7 from "path";
|
|
4918
5045
|
import { realpath as realpath4 } from "fs/promises";
|
|
4919
5046
|
var runTmux = (args) => new Promise((resolve, reject) => {
|
|
4920
5047
|
execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
@@ -4925,34 +5052,48 @@ var runTmux = (args) => new Promise((resolve, reject) => {
|
|
|
4925
5052
|
);
|
|
4926
5053
|
});
|
|
4927
5054
|
});
|
|
5055
|
+
function sessionNameFor(worktreePath) {
|
|
5056
|
+
return path7.basename(worktreePath).replace(/[.:#]/g, "_") || "warden";
|
|
5057
|
+
}
|
|
4928
5058
|
var TmuxService = class {
|
|
4929
5059
|
constructor(run2 = runTmux) {
|
|
4930
5060
|
this.run = run2;
|
|
4931
5061
|
}
|
|
4932
5062
|
run;
|
|
4933
|
-
async
|
|
4934
|
-
const
|
|
4935
|
-
|
|
5063
|
+
async list() {
|
|
5064
|
+
const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]).catch((e) => {
|
|
5065
|
+
if (e instanceof HttpError && /no server running|error connecting/i.test(e.message)) return "";
|
|
5066
|
+
throw e;
|
|
5067
|
+
});
|
|
4936
5068
|
const sessions = [];
|
|
4937
5069
|
for (const line of output.trimEnd().split("\n")) {
|
|
4938
5070
|
const [id, name, ...parts] = line.split(" ");
|
|
4939
|
-
const
|
|
4940
|
-
if (!id || !/^\$\d+$/.test(id) || !name || !
|
|
4941
|
-
|
|
5071
|
+
const dir = parts.join(" ");
|
|
5072
|
+
if (!id || !/^\$\d+$/.test(id) || !name || !dir) continue;
|
|
5073
|
+
sessions.push({ id, name, path: dir });
|
|
4942
5074
|
}
|
|
4943
5075
|
return sessions;
|
|
4944
5076
|
}
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5077
|
+
/**
|
|
5078
|
+
* A detached session in the worktree, named after its directory, with a shell and nothing run in
|
|
5079
|
+
* it. One already there for the same directory is handed back rather than doubled; one of that
|
|
5080
|
+
* name elsewhere is refused, not replaced — it is someone's work.
|
|
5081
|
+
*/
|
|
5082
|
+
async openSession(worktreePath) {
|
|
4948
5083
|
const directory = await realpath4(worktreePath);
|
|
4949
|
-
const
|
|
4950
|
-
|
|
5084
|
+
const name = sessionNameFor(directory);
|
|
5085
|
+
const existing = (await this.list()).find((x) => x.name === name);
|
|
5086
|
+
if (existing) {
|
|
5087
|
+
if (await realpath4(existing.path).catch(() => null) === directory) return { session: name, created: false };
|
|
5088
|
+
throw new HttpError(409, `\u5DF2\u6709\u540C\u540D tmux session ${name}\uFF0C\u5DE5\u4F5C\u76EE\u5F55\u662F ${existing.path}`, "tmux_name_taken");
|
|
5089
|
+
}
|
|
5090
|
+
await this.run(["new-session", "-d", "-s", name, "-c", directory.replaceAll("#", "##")]);
|
|
5091
|
+
return { session: name, created: true };
|
|
4951
5092
|
}
|
|
4952
5093
|
};
|
|
4953
5094
|
|
|
4954
5095
|
// packages/server/src/watcher.ts
|
|
4955
|
-
import
|
|
5096
|
+
import path8 from "path";
|
|
4956
5097
|
import { stat as stat5 } from "fs/promises";
|
|
4957
5098
|
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
4958
5099
|
function statusPaths(out) {
|
|
@@ -5049,7 +5190,7 @@ var RepoWatcher = class {
|
|
|
5049
5190
|
const stats = await Promise.all(
|
|
5050
5191
|
statusPaths(statusRes.stdout).map(async (rel) => {
|
|
5051
5192
|
try {
|
|
5052
|
-
const st = await stat5(
|
|
5193
|
+
const st = await stat5(path8.join(this.root, rel));
|
|
5053
5194
|
return `${rel} ${st.mtimeMs} ${st.size}`;
|
|
5054
5195
|
} catch {
|
|
5055
5196
|
return `${rel} -`;
|
|
@@ -5061,7 +5202,7 @@ var RepoWatcher = class {
|
|
|
5061
5202
|
};
|
|
5062
5203
|
|
|
5063
5204
|
// packages/server/src/static.ts
|
|
5064
|
-
import
|
|
5205
|
+
import path9 from "path";
|
|
5065
5206
|
import { readFile as readFile3, stat as stat6 } from "fs/promises";
|
|
5066
5207
|
var MIME = {
|
|
5067
5208
|
".html": "text/html; charset=utf-8",
|
|
@@ -5079,20 +5220,20 @@ var MIME = {
|
|
|
5079
5220
|
".txt": "text/plain; charset=utf-8"
|
|
5080
5221
|
};
|
|
5081
5222
|
async function serveStaticFile(c, webDir) {
|
|
5082
|
-
const root =
|
|
5223
|
+
const root = path9.resolve(webDir);
|
|
5083
5224
|
let rel;
|
|
5084
5225
|
try {
|
|
5085
5226
|
rel = decodeURIComponent(c.req.path);
|
|
5086
5227
|
} catch {
|
|
5087
5228
|
return c.text("Bad Request", 400);
|
|
5088
5229
|
}
|
|
5089
|
-
let target =
|
|
5090
|
-
if (target !== root && !target.startsWith(root +
|
|
5230
|
+
let target = path9.resolve(root, "." + rel);
|
|
5231
|
+
if (target !== root && !target.startsWith(root + path9.sep)) return c.text("Forbidden", 403);
|
|
5091
5232
|
try {
|
|
5092
5233
|
const st = await stat6(target);
|
|
5093
|
-
if (st.isDirectory()) target =
|
|
5234
|
+
if (st.isDirectory()) target = path9.join(target, "index.html");
|
|
5094
5235
|
} catch {
|
|
5095
|
-
target =
|
|
5236
|
+
target = path9.join(root, "index.html");
|
|
5096
5237
|
}
|
|
5097
5238
|
let body;
|
|
5098
5239
|
try {
|
|
@@ -5100,7 +5241,7 @@ async function serveStaticFile(c, webDir) {
|
|
|
5100
5241
|
} catch {
|
|
5101
5242
|
return c.text("Not Found", 404);
|
|
5102
5243
|
}
|
|
5103
|
-
const ext =
|
|
5244
|
+
const ext = path9.extname(target).toLowerCase();
|
|
5104
5245
|
const isAsset = rel.startsWith("/assets/");
|
|
5105
5246
|
return new Response(new Uint8Array(body), {
|
|
5106
5247
|
status: 200,
|
|
@@ -5172,8 +5313,8 @@ function createApp(opts) {
|
|
|
5172
5313
|
const checkpointHandoff = (comments) => {
|
|
5173
5314
|
const worktrees2 = new Set(comments.map((x) => tryParseTargetKey(x.targetKey)?.worktree));
|
|
5174
5315
|
for (const wt of worktrees2) {
|
|
5175
|
-
takeCheckpoint(store, wt ?? repo.
|
|
5176
|
-
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.
|
|
5316
|
+
takeCheckpoint(store, wt ?? repo.commonRoot, wt, { handoff: true }).catch((e) => {
|
|
5317
|
+
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.commonRoot}: ${e instanceof Error ? e.message : e}`);
|
|
5177
5318
|
});
|
|
5178
5319
|
}
|
|
5179
5320
|
};
|
|
@@ -5218,16 +5359,21 @@ function createApp(opts) {
|
|
|
5218
5359
|
api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
|
|
5219
5360
|
api.get("/repo", async (c) => {
|
|
5220
5361
|
const state = await store.load();
|
|
5221
|
-
const
|
|
5362
|
+
const own = repo.root === repo.commonRoot ? "working" : formatTargetKey({ kind: "working", worktree: repo.root });
|
|
5363
|
+
const info = await getRepoInfo(repo, own);
|
|
5222
5364
|
worktreeCache = { at: Date.now(), list: info.worktrees };
|
|
5223
5365
|
const last = state.prefs.lastTarget;
|
|
5224
5366
|
const target = last ? tryParseTargetKey(last) : void 0;
|
|
5225
|
-
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree)) &&
|
|
5367
|
+
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
|
|
5368
|
+
// worktree's page last looked at: the preference is shared by every server on the repository.
|
|
5369
|
+
(repo.root === repo.commonRoot || target.worktree === repo.root) && (target.kind !== "checkpoint" || !!findCheckpoint(state, target.worktree, target.id));
|
|
5226
5370
|
if (last && usable2) info.defaultTarget = last;
|
|
5227
5371
|
else if (last) {
|
|
5228
|
-
|
|
5229
|
-
s
|
|
5230
|
-
|
|
5372
|
+
if (repo.root === repo.commonRoot) {
|
|
5373
|
+
await store.update((s) => {
|
|
5374
|
+
s.prefs.lastTarget = "working";
|
|
5375
|
+
});
|
|
5376
|
+
}
|
|
5231
5377
|
}
|
|
5232
5378
|
return c.json(info);
|
|
5233
5379
|
});
|
|
@@ -5354,6 +5500,7 @@ function createApp(opts) {
|
|
|
5354
5500
|
if (body.skipDebug !== void 0 && typeof body.skipDebug !== "boolean") throw badRequest("skipDebug must be a boolean", "bad_selection");
|
|
5355
5501
|
const diff = await fileDiffWithHints(ctx, body.path);
|
|
5356
5502
|
if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
|
|
5503
|
+
if (diff.conflicted) throw new HttpError(409, `${body.path} is in conflict; resolve it and git add it in the terminal`, "conflicted");
|
|
5357
5504
|
if (diff.contentHash !== body.contentHash)
|
|
5358
5505
|
throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
|
|
5359
5506
|
if (body.skipDebug) await annotateDebug(ctx, [diff]);
|
|
@@ -5383,7 +5530,7 @@ function createApp(opts) {
|
|
|
5383
5530
|
if (!anchored) throw badRequest("selection is not fully visible in the diff on that side (must be inside one hunk)", "bad_selection");
|
|
5384
5531
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5385
5532
|
const comment = {
|
|
5386
|
-
id:
|
|
5533
|
+
id: randomUUID4(),
|
|
5387
5534
|
targetKey: key,
|
|
5388
5535
|
filePath: body.filePath,
|
|
5389
5536
|
side: body.side,
|
|
@@ -5408,20 +5555,20 @@ function createApp(opts) {
|
|
|
5408
5555
|
const body = await c.req.json();
|
|
5409
5556
|
const scope = commentScopeKey(key);
|
|
5410
5557
|
const state = await store.load();
|
|
5411
|
-
const
|
|
5412
|
-
if (!
|
|
5558
|
+
const existing2 = state.targets[scope]?.comments.find((x) => x.id === id);
|
|
5559
|
+
if (!existing2) throw notFound("comment not found");
|
|
5413
5560
|
let patch = {};
|
|
5414
5561
|
if (typeof body.body === "string") {
|
|
5415
5562
|
if (!body.body.trim()) throw badRequest("body must not be empty");
|
|
5416
5563
|
patch.body = body.body;
|
|
5417
5564
|
}
|
|
5418
5565
|
if (body.startLine !== void 0 || body.side !== void 0) {
|
|
5419
|
-
const side = body.side ??
|
|
5420
|
-
const start = Number(body.startLine ??
|
|
5421
|
-
const end = Number(body.endLine ?? body.startLine ??
|
|
5566
|
+
const side = body.side ?? existing2.side;
|
|
5567
|
+
const start = Number(body.startLine ?? existing2.startLine);
|
|
5568
|
+
const end = Number(body.endLine ?? body.startLine ?? existing2.endLine);
|
|
5422
5569
|
if (side !== "old" && side !== "new") throw badRequest("side must be old|new");
|
|
5423
|
-
const diff = await fileDiffWithHints(ctx,
|
|
5424
|
-
if (!diff) throw badRequest(`file ${
|
|
5570
|
+
const diff = await fileDiffWithHints(ctx, existing2.filePath);
|
|
5571
|
+
if (!diff) throw badRequest(`file ${existing2.filePath} is not part of ${key}`, "no_diff");
|
|
5425
5572
|
const anchored = buildAnchor(diff, side, start, end);
|
|
5426
5573
|
if (!anchored) throw badRequest("selection is not fully visible in the diff on that side", "bad_selection");
|
|
5427
5574
|
patch = {
|
|
@@ -5431,7 +5578,7 @@ function createApp(opts) {
|
|
|
5431
5578
|
endLine: anchored.endLine,
|
|
5432
5579
|
codeSnippet: anchored.snippet,
|
|
5433
5580
|
anchor: anchored.anchor,
|
|
5434
|
-
status:
|
|
5581
|
+
status: existing2.exportedAt ? "exported" : "active",
|
|
5435
5582
|
// Re-attaching in a view moves the comment to it.
|
|
5436
5583
|
targetKey: key
|
|
5437
5584
|
};
|
|
@@ -5440,7 +5587,12 @@ function createApp(opts) {
|
|
|
5440
5587
|
const t = ensureTarget(s, scope);
|
|
5441
5588
|
const idx = t.comments.findIndex((x) => x.id === id);
|
|
5442
5589
|
if (idx < 0) throw notFound("comment not found");
|
|
5443
|
-
const
|
|
5590
|
+
const prev = t.comments[idx];
|
|
5591
|
+
const next = { ...prev, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5592
|
+
if (patch.body !== void 0 && patch.body !== prev.body && next.exportedAt) {
|
|
5593
|
+
delete next.exportedAt;
|
|
5594
|
+
if (next.status === "exported") next.status = "active";
|
|
5595
|
+
}
|
|
5444
5596
|
t.comments[idx] = next;
|
|
5445
5597
|
return next;
|
|
5446
5598
|
});
|
|
@@ -5507,7 +5659,6 @@ function createApp(opts) {
|
|
|
5507
5659
|
const comments = state.targets[scope]?.comments ?? [];
|
|
5508
5660
|
const prevHead = state.targets[scope]?.head;
|
|
5509
5661
|
const head = await revParse(ctx.cwd, "HEAD") ?? "";
|
|
5510
|
-
const committed = local && prevHead !== void 0 && prevHead !== head;
|
|
5511
5662
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5512
5663
|
const located = /* @__PURE__ */ new Map();
|
|
5513
5664
|
await Promise.all(
|
|
@@ -5531,6 +5682,19 @@ function createApp(opts) {
|
|
|
5531
5682
|
}
|
|
5532
5683
|
})
|
|
5533
5684
|
);
|
|
5685
|
+
const landed = /* @__PURE__ */ new Set();
|
|
5686
|
+
const lost = comments.filter((cm) => !located.has(cm.id));
|
|
5687
|
+
if (local && lost.length && prevHead && head && prevHead !== head && await mergeBase(ctx.cwd, prevHead, head) === prevHead) {
|
|
5688
|
+
const range = await worktreeCtx(
|
|
5689
|
+
formatTargetKey({ kind: "range", base: prevHead, head, ...ctx.target.worktree ? { worktree: ctx.target.worktree } : {} })
|
|
5690
|
+
);
|
|
5691
|
+
await Promise.all(
|
|
5692
|
+
lost.map(async (cm) => {
|
|
5693
|
+
const committedDiff = await getFileDiff(range, cm.filePath).catch(() => void 0);
|
|
5694
|
+
if (committedDiff && reanchorComment(cm, committedDiff, now).status !== "orphaned") landed.add(cm.id);
|
|
5695
|
+
})
|
|
5696
|
+
);
|
|
5697
|
+
}
|
|
5534
5698
|
const result = await store.update((s) => {
|
|
5535
5699
|
const t = ensureTarget(s, scope);
|
|
5536
5700
|
const dropped = [];
|
|
@@ -5538,7 +5702,7 @@ function createApp(opts) {
|
|
|
5538
5702
|
const patch = located.get(cm.id);
|
|
5539
5703
|
if (!patch) {
|
|
5540
5704
|
if (!comments.some((x) => x.id === cm.id)) return [cm];
|
|
5541
|
-
if (
|
|
5705
|
+
if (landed.has(cm.id) && !cm.replies?.length) {
|
|
5542
5706
|
dropped.push(cm.id);
|
|
5543
5707
|
return [];
|
|
5544
5708
|
}
|
|
@@ -5555,30 +5719,34 @@ function createApp(opts) {
|
|
|
5555
5719
|
const res = { comments: result };
|
|
5556
5720
|
return c.json(res);
|
|
5557
5721
|
});
|
|
5558
|
-
|
|
5559
|
-
const
|
|
5560
|
-
if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
|
|
5561
|
-
const ids = body.commentIds.filter((x) => typeof x === "string");
|
|
5562
|
-
let handed = [];
|
|
5563
|
-
const res = await store.update((s) => {
|
|
5722
|
+
const handOver = async (ids) => {
|
|
5723
|
+
const handed = await store.update((s) => {
|
|
5564
5724
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5565
|
-
const
|
|
5725
|
+
const out = [];
|
|
5566
5726
|
for (const id of ids) {
|
|
5567
5727
|
const found = findComment(s, id);
|
|
5568
5728
|
if (!found) continue;
|
|
5569
5729
|
const next = { ...found.comment, exportedAt: now, updatedAt: now };
|
|
5570
5730
|
if (next.status === "active") next.status = "exported";
|
|
5571
5731
|
s.targets[found.targetKey].comments[found.index] = next;
|
|
5572
|
-
|
|
5732
|
+
out.push(next);
|
|
5573
5733
|
}
|
|
5574
|
-
|
|
5575
|
-
return {
|
|
5576
|
-
text: formatCommentsExport({ repoRoot: repo.root, comments: selected, replyCommand: opts.replyCommand }),
|
|
5577
|
-
count: selected.length,
|
|
5578
|
-
commentIds: selected.map((x) => x.id)
|
|
5579
|
-
};
|
|
5734
|
+
return out;
|
|
5580
5735
|
});
|
|
5581
5736
|
checkpointHandoff(handed);
|
|
5737
|
+
return handed;
|
|
5738
|
+
};
|
|
5739
|
+
const existing = (state, ids) => ids.flatMap((id) => findComment(state, id)?.comment ?? []);
|
|
5740
|
+
api.post("/comments/export", async (c) => {
|
|
5741
|
+
const body = await c.req.json();
|
|
5742
|
+
if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
|
|
5743
|
+
const ids = body.commentIds.filter((x) => typeof x === "string");
|
|
5744
|
+
const comments = body.preview ? existing(await store.load(), ids) : await handOver(ids);
|
|
5745
|
+
const res = {
|
|
5746
|
+
text: formatCommentsExport({ repoRoot: repo.root, comments, replyCommand: opts.replyCommand }),
|
|
5747
|
+
count: comments.length,
|
|
5748
|
+
commentIds: comments.map((x) => x.id)
|
|
5749
|
+
};
|
|
5582
5750
|
return c.json(res);
|
|
5583
5751
|
});
|
|
5584
5752
|
const knownRoot = async (rootParam) => {
|
|
@@ -5602,7 +5770,7 @@ function createApp(opts) {
|
|
|
5602
5770
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5603
5771
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5604
5772
|
const todo = {
|
|
5605
|
-
id:
|
|
5773
|
+
id: randomUUID4(),
|
|
5606
5774
|
branch,
|
|
5607
5775
|
title: body.title.trim(),
|
|
5608
5776
|
body: typeof body.body === "string" ? body.body : "",
|
|
@@ -5653,26 +5821,17 @@ function createApp(opts) {
|
|
|
5653
5821
|
});
|
|
5654
5822
|
api.post("/todos/:id/export", async (c) => {
|
|
5655
5823
|
const id = c.req.param("id");
|
|
5656
|
-
const
|
|
5657
|
-
const
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
handed.push(next);
|
|
5668
|
-
}
|
|
5669
|
-
return {
|
|
5670
|
-
text: formatTodoExport({ todo, comments: handed, replyCommand: opts.replyCommand }),
|
|
5671
|
-
count: handed.length,
|
|
5672
|
-
commentIds: handed.map((x) => x.id)
|
|
5673
|
-
};
|
|
5674
|
-
});
|
|
5675
|
-
checkpointHandoff(handed);
|
|
5824
|
+
const body = await c.req.json().catch(() => ({})) ?? {};
|
|
5825
|
+
const state = await store.load();
|
|
5826
|
+
const todo = state.todos.find((t) => t.id === id);
|
|
5827
|
+
if (!todo) throw notFound("todo not found");
|
|
5828
|
+
const ids = todo.commentIds ?? [];
|
|
5829
|
+
const comments = body.preview ? existing(state, ids) : await handOver(ids);
|
|
5830
|
+
const res = {
|
|
5831
|
+
text: formatTodoExport({ todo, comments, replyCommand: opts.replyCommand }),
|
|
5832
|
+
count: comments.length,
|
|
5833
|
+
commentIds: comments.map((x) => x.id)
|
|
5834
|
+
};
|
|
5676
5835
|
return c.json(res);
|
|
5677
5836
|
});
|
|
5678
5837
|
api.delete("/todos/:id", async (c) => {
|
|
@@ -5705,13 +5864,14 @@ function createApp(opts) {
|
|
|
5705
5864
|
if (ref && !isValidRef(ref)) throw badRequest("invalid ref");
|
|
5706
5865
|
if (filePath && (filePath.startsWith("/") || filePath.split("/").includes(".."))) throw badRequest("invalid path");
|
|
5707
5866
|
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}`];
|
|
5867
|
+
const args = ["log", "--no-show-signature", `--max-count=${limit + 1}`, `--skip=${offset}`, `--format=${COMMIT_FORMAT}`];
|
|
5709
5868
|
if (firstParent) args.push("--first-parent");
|
|
5710
5869
|
if (q || author) args.push("--fixed-strings", "--regexp-ignore-case");
|
|
5711
5870
|
if (q) args.push(`--grep=${q}`);
|
|
5712
5871
|
if (author) args.push(`--author=${author}`);
|
|
5713
5872
|
if (ref) args.push(ref);
|
|
5714
|
-
|
|
5873
|
+
args.push("--");
|
|
5874
|
+
if (filePath) args.push(literal(filePath));
|
|
5715
5875
|
let stdout = "";
|
|
5716
5876
|
try {
|
|
5717
5877
|
stdout = (await runGit(args, { cwd })).stdout;
|
|
@@ -5725,7 +5885,7 @@ function createApp(opts) {
|
|
|
5725
5885
|
if (q && offset === 0 && /^[0-9a-f]{4,40}$/i.test(q)) {
|
|
5726
5886
|
const sha = await revParse(cwd, `${q}^{commit}`);
|
|
5727
5887
|
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];
|
|
5888
|
+
const hit = parseCommitLog((await runGit(["log", "--no-show-signature", "--max-count=1", `--format=${COMMIT_FORMAT}`, sha, "--"], { cwd })).stdout)[0];
|
|
5729
5889
|
if (hit) commits = [hit, ...commits];
|
|
5730
5890
|
}
|
|
5731
5891
|
}
|
|
@@ -5744,13 +5904,13 @@ function createApp(opts) {
|
|
|
5744
5904
|
return c.json(res);
|
|
5745
5905
|
});
|
|
5746
5906
|
const tmux = opts.tmux ?? new TmuxService();
|
|
5747
|
-
api.
|
|
5748
|
-
api.post("/tmux/windows", async (c) => {
|
|
5907
|
+
api.post("/tmux/sessions", async (c) => {
|
|
5749
5908
|
const body = await c.req.json();
|
|
5750
|
-
if (!body || typeof body.path !== "string"
|
|
5909
|
+
if (!body || typeof body.path !== "string") throw badRequest("path is required");
|
|
5751
5910
|
const wt = (await worktrees(true)).find((w) => w.path === body.path && !w.bare);
|
|
5752
5911
|
if (!wt) throw badRequest("unknown worktree", "unknown_worktree");
|
|
5753
|
-
|
|
5912
|
+
const res = await tmux.openSession(wt.path);
|
|
5913
|
+
return c.json(res, res.created ? 201 : 200);
|
|
5754
5914
|
});
|
|
5755
5915
|
api.get("/worktrees", async (c) => {
|
|
5756
5916
|
const [list, state] = await Promise.all([listWorktreesDetailed(repo), store.load()]);
|
|
@@ -5759,7 +5919,7 @@ function createApp(opts) {
|
|
|
5759
5919
|
const latest = /* @__PURE__ */ new Map();
|
|
5760
5920
|
for (const t of Object.values(state.targets)) {
|
|
5761
5921
|
for (const cm of t.comments) {
|
|
5762
|
-
const at = commentWorktree(cm) ?? repo.
|
|
5922
|
+
const at = commentWorktree(cm) ?? repo.commonRoot;
|
|
5763
5923
|
const r = review.get(at) ?? { toAgent: 0, toReviewer: 0 };
|
|
5764
5924
|
const seen = latest.get(at) ?? { agent: "", reviewer: "" };
|
|
5765
5925
|
if (awaitsAgent(cm)) {
|
|
@@ -5785,12 +5945,18 @@ function createApp(opts) {
|
|
|
5785
5945
|
const res = { remotes: await lookupRemoteBranches(repo, c.req.query("branch") ?? "") };
|
|
5786
5946
|
return c.json(res);
|
|
5787
5947
|
});
|
|
5948
|
+
let worktreeWrites = Promise.resolve();
|
|
5949
|
+
const oneAtATime = (fn) => {
|
|
5950
|
+
const run2 = worktreeWrites.then(fn, fn);
|
|
5951
|
+
worktreeWrites = run2.catch(() => void 0);
|
|
5952
|
+
return run2;
|
|
5953
|
+
};
|
|
5788
5954
|
api.post("/worktrees", async (c) => {
|
|
5789
5955
|
const body = await c.req.json();
|
|
5790
5956
|
if (typeof body.branch !== "string") throw badRequest("branch is required");
|
|
5791
5957
|
if (body.base !== void 0 && typeof body.base !== "string") throw badRequest("base must be a ref");
|
|
5792
5958
|
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);
|
|
5959
|
+
const res = await oneAtATime(() => checkoutWorktree(repo, body));
|
|
5794
5960
|
worktreeCache = null;
|
|
5795
5961
|
await store.update((s) => forgetWorktreeTargets(s, res.worktree.path));
|
|
5796
5962
|
return c.json(res, 201);
|
|
@@ -5798,14 +5964,14 @@ function createApp(opts) {
|
|
|
5798
5964
|
api.post("/worktrees/release", async (c) => {
|
|
5799
5965
|
const body = await c.req.json();
|
|
5800
5966
|
if (typeof body.path !== "string") throw badRequest("path is required");
|
|
5801
|
-
const res = await releaseWorktree(repo, body);
|
|
5967
|
+
const res = await oneAtATime(() => releaseWorktree(repo, body));
|
|
5802
5968
|
worktreeCache = null;
|
|
5803
5969
|
return c.json(res);
|
|
5804
5970
|
});
|
|
5805
5971
|
api.post("/worktrees/remove", async (c) => {
|
|
5806
5972
|
const body = await c.req.json();
|
|
5807
5973
|
if (typeof body.path !== "string") throw badRequest("path is required");
|
|
5808
|
-
const res = await removeWorktree(repo, body);
|
|
5974
|
+
const res = await oneAtATime(() => removeWorktree(repo, body));
|
|
5809
5975
|
worktreeCache = null;
|
|
5810
5976
|
return c.json(res);
|
|
5811
5977
|
});
|
|
@@ -5856,9 +6022,8 @@ function createApp(opts) {
|
|
|
5856
6022
|
const instances = NvimService.matching(scan.instances, root);
|
|
5857
6023
|
const state = await store.load();
|
|
5858
6024
|
const preferred = state.prefs.nvimSocketByRoot[root];
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
else if (instances.length === 1) selected = instances[0].socket;
|
|
6025
|
+
const choice = chooseNvim(instances, [preferred]);
|
|
6026
|
+
const selected = "socket" in choice ? choice.socket : void 0;
|
|
5862
6027
|
const res = { root, nvimAvailable: scan.nvimAvailable, instances, selected, scannedAt: scan.scannedAt };
|
|
5863
6028
|
return c.json(res);
|
|
5864
6029
|
});
|
|
@@ -5872,19 +6037,38 @@ function createApp(opts) {
|
|
|
5872
6037
|
});
|
|
5873
6038
|
api.post("/nvim/open", async (c) => {
|
|
5874
6039
|
const body = await c.req.json();
|
|
5875
|
-
if (!body.
|
|
5876
|
-
if (!
|
|
5877
|
-
const
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6040
|
+
if (!body.absPath) throw badRequest("absPath is required");
|
|
6041
|
+
if (!path10.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
|
|
6042
|
+
const root = await knownRoot(body.root);
|
|
6043
|
+
const wanted = [body.socket, (await store.load()).prefs.nvimSocketByRoot[root]];
|
|
6044
|
+
const line = Number(body.line) || 1;
|
|
6045
|
+
const pick = async (force) => {
|
|
6046
|
+
const scan = await nvim.scan(force);
|
|
6047
|
+
if (!scan.nvimAvailable) throw badRequest("nvim is not on PATH", "nvim_unavailable");
|
|
6048
|
+
return { matching: NvimService.matching(scan.instances, root), choice: chooseNvim(NvimService.matching(scan.instances, root), wanted) };
|
|
6049
|
+
};
|
|
6050
|
+
let { choice } = await pick(false);
|
|
6051
|
+
if ("error" in choice) choice = (await pick(true)).choice;
|
|
6052
|
+
if ("error" in choice) {
|
|
6053
|
+
if (choice.error === "none") throw notFound(`no nvim is open inside ${root}`, "nvim_none");
|
|
6054
|
+
throw new HttpError(409, `several nvim instances are open inside ${root}; select one`, "nvim_ambiguous");
|
|
5881
6055
|
}
|
|
5882
6056
|
try {
|
|
5883
|
-
await nvim.open(
|
|
6057
|
+
await nvim.open(choice.socket, body.absPath, line);
|
|
6058
|
+
const res = { ok: true, socket: choice.socket };
|
|
6059
|
+
return c.json(res);
|
|
5884
6060
|
} catch (e) {
|
|
5885
|
-
|
|
6061
|
+
const failure = new HttpError(502, e instanceof Error ? e.message : String(e), "nvim_failed");
|
|
6062
|
+
const fresh = await pick(true);
|
|
6063
|
+
if (fresh.matching.some((i) => i.socket === choice.socket) || "error" in fresh.choice) throw failure;
|
|
6064
|
+
try {
|
|
6065
|
+
await nvim.open(fresh.choice.socket, body.absPath, line);
|
|
6066
|
+
} catch (e2) {
|
|
6067
|
+
throw new HttpError(502, e2 instanceof Error ? e2.message : String(e2), "nvim_failed");
|
|
6068
|
+
}
|
|
6069
|
+
const res = { ok: true, socket: fresh.choice.socket };
|
|
6070
|
+
return c.json(res);
|
|
5886
6071
|
}
|
|
5887
|
-
return c.json({ ok: true });
|
|
5888
6072
|
});
|
|
5889
6073
|
app.route("/api", api);
|
|
5890
6074
|
app.notFound((c) => {
|
|
@@ -5933,14 +6117,14 @@ function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
|
5933
6117
|
}
|
|
5934
6118
|
|
|
5935
6119
|
// packages/server/src/update.ts
|
|
5936
|
-
import
|
|
5937
|
-
import { mkdir as mkdir3, readFile as readFile5, writeFile
|
|
6120
|
+
import path11 from "path";
|
|
6121
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile } from "fs/promises";
|
|
5938
6122
|
var PACKAGE = "@xbghc/warden";
|
|
5939
6123
|
var LATEST_URL = "https://registry.npmjs.org/@xbghc%2fwarden/latest";
|
|
5940
6124
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
5941
6125
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
5942
6126
|
function updateCacheFile(baseDir = dataDir()) {
|
|
5943
|
-
return
|
|
6127
|
+
return path11.join(baseDir, "update-check.json");
|
|
5944
6128
|
}
|
|
5945
6129
|
function updateCheckEnabled(env = process.env) {
|
|
5946
6130
|
return !env.WARDEN_NO_UPDATE_CHECK && !env.NO_UPDATE_NOTIFIER && !env.CI;
|
|
@@ -5974,8 +6158,8 @@ async function readCache(file, now) {
|
|
|
5974
6158
|
}
|
|
5975
6159
|
async function writeCache(file, cache) {
|
|
5976
6160
|
try {
|
|
5977
|
-
await mkdir3(
|
|
5978
|
-
await
|
|
6161
|
+
await mkdir3(path11.dirname(file), { recursive: true });
|
|
6162
|
+
await writeFile(file, JSON.stringify(cache));
|
|
5979
6163
|
} catch {
|
|
5980
6164
|
}
|
|
5981
6165
|
}
|
|
@@ -6019,7 +6203,7 @@ async function startServer(opts) {
|
|
|
6019
6203
|
const repo = await resolveRepo(opts.repoPath);
|
|
6020
6204
|
const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
|
|
6021
6205
|
const store = new StateStore(stateFile, repo.commonRoot);
|
|
6022
|
-
const instanceToken =
|
|
6206
|
+
const instanceToken = randomUUID5();
|
|
6023
6207
|
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update, replyCommand: opts.replyCommand });
|
|
6024
6208
|
const listen = (p) => new Promise((resolve, reject) => {
|
|
6025
6209
|
const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
|
|
@@ -6085,18 +6269,21 @@ async function feedback(args, replyCommand) {
|
|
|
6085
6269
|
const unknown = args.find((a) => a !== "--peek");
|
|
6086
6270
|
if (unknown) throw new Error(`unknown argument: ${unknown}`);
|
|
6087
6271
|
const { store, root, commonRoot } = await openStore();
|
|
6088
|
-
const
|
|
6089
|
-
const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek });
|
|
6272
|
+
const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek: true });
|
|
6090
6273
|
if (comments.length === 0) {
|
|
6091
6274
|
console.log("No review comments are waiting for you.");
|
|
6092
6275
|
return;
|
|
6093
6276
|
}
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6277
|
+
const text = formatCommentsExport({ repoRoot: root, comments, replyCommand });
|
|
6278
|
+
await new Promise((resolve, reject) => process.stdout.write(text, (e) => e ? reject(e) : resolve()));
|
|
6279
|
+
if (args.includes("--peek")) return;
|
|
6280
|
+
await markHandedOver(
|
|
6281
|
+
store,
|
|
6282
|
+
comments.map((c) => c.id)
|
|
6283
|
+
);
|
|
6284
|
+
await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
|
|
6285
|
+
console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
|
|
6286
|
+
});
|
|
6100
6287
|
}
|
|
6101
6288
|
async function reply(args) {
|
|
6102
6289
|
const [id, ...words] = args;
|
|
@@ -6123,7 +6310,7 @@ async function runAgentCommand(argv, replyCommand) {
|
|
|
6123
6310
|
}
|
|
6124
6311
|
|
|
6125
6312
|
// bin/cli.ts
|
|
6126
|
-
var VERSION = true ? "0.16.
|
|
6313
|
+
var VERSION = true ? "0.16.2" : "dev";
|
|
6127
6314
|
function parseArgs(argv) {
|
|
6128
6315
|
const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
|
|
6129
6316
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -6142,11 +6329,11 @@ function parseArgs(argv) {
|
|
|
6142
6329
|
} else if (a === "--help" || a === "-h") args.help = true;
|
|
6143
6330
|
else if (a === "--version" || a === "-v") args.version = true;
|
|
6144
6331
|
else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
|
|
6145
|
-
else args.repoPath =
|
|
6332
|
+
else args.repoPath = path12.resolve(a);
|
|
6146
6333
|
}
|
|
6147
6334
|
return args;
|
|
6148
6335
|
}
|
|
6149
|
-
var viaNpx = fileURLToPath(import.meta.url).includes(`${
|
|
6336
|
+
var viaNpx = fileURLToPath(import.meta.url).includes(`${path12.sep}_npx${path12.sep}`);
|
|
6150
6337
|
var selfCommand = viaNpx ? "npx @xbghc/warden" : "warden";
|
|
6151
6338
|
function usage() {
|
|
6152
6339
|
return `warden \u2014 local git diff review UI
|
|
@@ -6193,16 +6380,16 @@ async function openBrowser(url) {
|
|
|
6193
6380
|
return false;
|
|
6194
6381
|
}
|
|
6195
6382
|
function resolveWebDir() {
|
|
6196
|
-
const here =
|
|
6383
|
+
const here = path12.dirname(fileURLToPath(import.meta.url));
|
|
6197
6384
|
const candidates = [
|
|
6198
6385
|
process.env.WARDEN_WEB_DIR,
|
|
6199
|
-
|
|
6386
|
+
path12.join(here, "web"),
|
|
6200
6387
|
// dist/cli.js -> dist/web
|
|
6201
|
-
|
|
6388
|
+
path12.join(here, "..", "dist", "web")
|
|
6202
6389
|
// bin/cli.ts (tsx dev) -> dist/web
|
|
6203
6390
|
].filter((p) => !!p);
|
|
6204
6391
|
for (const dir of candidates) {
|
|
6205
|
-
if (existsSync2(
|
|
6392
|
+
if (existsSync2(path12.join(dir, "index.html"))) return dir;
|
|
6206
6393
|
}
|
|
6207
6394
|
return void 0;
|
|
6208
6395
|
}
|