@xbghc/warden 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -422
- package/dist/cli.js +785 -419
- package/dist/web/assets/index-gtbD7YAQ.css +1 -0
- package/dist/web/assets/index-iclVlGDF.js +198 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-CC0xb7jB.css +0 -1
- package/dist/web/assets/index-DEZiCBXl.js +0 -200
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 path11 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 randomUUID4 } 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,8 +652,9 @@ var serve = (options, listeningListener) => {
|
|
|
652
652
|
};
|
|
653
653
|
|
|
654
654
|
// packages/server/src/app.ts
|
|
655
|
-
import
|
|
656
|
-
import {
|
|
655
|
+
import path9 from "path";
|
|
656
|
+
import { stat as stat7 } from "fs/promises";
|
|
657
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
657
658
|
|
|
658
659
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
|
|
659
660
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -812,26 +813,26 @@ var throwNestingLimitExceeded = () => {
|
|
|
812
813
|
};
|
|
813
814
|
|
|
814
815
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
|
|
815
|
-
var splitPath = (
|
|
816
|
-
const paths =
|
|
816
|
+
var splitPath = (path12) => {
|
|
817
|
+
const paths = path12.split("/");
|
|
817
818
|
if (paths[0] === "") {
|
|
818
819
|
paths.shift();
|
|
819
820
|
}
|
|
820
821
|
return paths;
|
|
821
822
|
};
|
|
822
823
|
var splitRoutingPath = (routePath) => {
|
|
823
|
-
const { groups, path:
|
|
824
|
-
const paths = splitPath(
|
|
824
|
+
const { groups, path: path12 } = extractGroupsFromPath(routePath);
|
|
825
|
+
const paths = splitPath(path12);
|
|
825
826
|
return replaceGroupMarks(paths, groups);
|
|
826
827
|
};
|
|
827
|
-
var extractGroupsFromPath = (
|
|
828
|
+
var extractGroupsFromPath = (path12) => {
|
|
828
829
|
const groups = [];
|
|
829
|
-
|
|
830
|
+
path12 = path12.replace(/\{[^}]+\}/g, (match2, index) => {
|
|
830
831
|
const mark = `@${index}`;
|
|
831
832
|
groups.push([mark, match2]);
|
|
832
833
|
return mark;
|
|
833
834
|
});
|
|
834
|
-
return { groups, path:
|
|
835
|
+
return { groups, path: path12 };
|
|
835
836
|
};
|
|
836
837
|
var replaceGroupMarks = (paths, groups) => {
|
|
837
838
|
for (let i = groups.length - 1; i >= 0; i--) {
|
|
@@ -888,8 +889,8 @@ var getPath = (request) => {
|
|
|
888
889
|
const queryIndex = url.indexOf("?", i);
|
|
889
890
|
const hashIndex = url.indexOf("#", i);
|
|
890
891
|
const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
891
|
-
const
|
|
892
|
-
return tryDecodeURI(
|
|
892
|
+
const path12 = url.slice(start, end);
|
|
893
|
+
return tryDecodeURI(path12.includes("%25") ? path12.replace(/%25/g, "%2525") : path12);
|
|
893
894
|
} else if (charCode === 63 || charCode === 35) {
|
|
894
895
|
break;
|
|
895
896
|
}
|
|
@@ -906,11 +907,11 @@ var mergePath = (base, sub, ...rest) => {
|
|
|
906
907
|
}
|
|
907
908
|
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
|
|
908
909
|
};
|
|
909
|
-
var checkOptionalParameter = (
|
|
910
|
-
if (
|
|
910
|
+
var checkOptionalParameter = (path12) => {
|
|
911
|
+
if (path12.charCodeAt(path12.length - 1) !== 63 || !path12.includes(":")) {
|
|
911
912
|
return null;
|
|
912
913
|
}
|
|
913
|
-
const segments =
|
|
914
|
+
const segments = path12.split("/");
|
|
914
915
|
const results = [];
|
|
915
916
|
let basePath = "";
|
|
916
917
|
segments.forEach((segment) => {
|
|
@@ -1052,9 +1053,9 @@ var HonoRequest = class {
|
|
|
1052
1053
|
*/
|
|
1053
1054
|
path;
|
|
1054
1055
|
bodyCache = {};
|
|
1055
|
-
constructor(request,
|
|
1056
|
+
constructor(request, path12 = "/", matchResult = [[]]) {
|
|
1056
1057
|
this.raw = request;
|
|
1057
|
-
this.path =
|
|
1058
|
+
this.path = path12;
|
|
1058
1059
|
this.#matchResult = matchResult;
|
|
1059
1060
|
}
|
|
1060
1061
|
param(key) {
|
|
@@ -1826,8 +1827,8 @@ var Hono = class _Hono {
|
|
|
1826
1827
|
return this;
|
|
1827
1828
|
};
|
|
1828
1829
|
});
|
|
1829
|
-
this.on = (method,
|
|
1830
|
-
for (const p of [
|
|
1830
|
+
this.on = (method, path12, ...handlers) => {
|
|
1831
|
+
for (const p of [path12].flat()) {
|
|
1831
1832
|
this.#path = p;
|
|
1832
1833
|
for (const m of [method].flat()) {
|
|
1833
1834
|
const methodName = m.toUpperCase();
|
|
@@ -1885,8 +1886,8 @@ var Hono = class _Hono {
|
|
|
1885
1886
|
* app.route("/api", app2) // GET /api/user
|
|
1886
1887
|
* ```
|
|
1887
1888
|
*/
|
|
1888
|
-
route(
|
|
1889
|
-
const subApp = this.basePath(
|
|
1889
|
+
route(path12, app) {
|
|
1890
|
+
const subApp = this.basePath(path12);
|
|
1890
1891
|
app.routes.map((r) => {
|
|
1891
1892
|
let handler;
|
|
1892
1893
|
if (app.errorHandler === errorHandler) {
|
|
@@ -1912,9 +1913,9 @@ var Hono = class _Hono {
|
|
|
1912
1913
|
* const api = new Hono().basePath('/api')
|
|
1913
1914
|
* ```
|
|
1914
1915
|
*/
|
|
1915
|
-
basePath(
|
|
1916
|
+
basePath(path12) {
|
|
1916
1917
|
const subApp = this.#clone();
|
|
1917
|
-
subApp._basePath = mergePath(this._basePath,
|
|
1918
|
+
subApp._basePath = mergePath(this._basePath, path12);
|
|
1918
1919
|
return subApp;
|
|
1919
1920
|
}
|
|
1920
1921
|
/**
|
|
@@ -1988,7 +1989,7 @@ var Hono = class _Hono {
|
|
|
1988
1989
|
* })
|
|
1989
1990
|
* ```
|
|
1990
1991
|
*/
|
|
1991
|
-
mount(
|
|
1992
|
+
mount(path12, applicationHandler, options) {
|
|
1992
1993
|
let replaceRequest;
|
|
1993
1994
|
let optionHandler;
|
|
1994
1995
|
if (options) {
|
|
@@ -2015,7 +2016,7 @@ var Hono = class _Hono {
|
|
|
2015
2016
|
return [c.env, executionContext];
|
|
2016
2017
|
};
|
|
2017
2018
|
replaceRequest ||= (() => {
|
|
2018
|
-
const mergedPath = mergePath(this._basePath,
|
|
2019
|
+
const mergedPath = mergePath(this._basePath, path12);
|
|
2019
2020
|
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
|
|
2020
2021
|
return (request) => {
|
|
2021
2022
|
const url = new URL(request.url);
|
|
@@ -2030,18 +2031,18 @@ var Hono = class _Hono {
|
|
|
2030
2031
|
}
|
|
2031
2032
|
await next();
|
|
2032
2033
|
};
|
|
2033
|
-
this.#addRoute(METHOD_NAME_ALL, mergePath(
|
|
2034
|
+
this.#addRoute(METHOD_NAME_ALL, mergePath(path12, "*"), handler);
|
|
2034
2035
|
return this;
|
|
2035
2036
|
}
|
|
2036
|
-
#addRoute(method,
|
|
2037
|
-
|
|
2037
|
+
#addRoute(method, path12, handler, baseRoutePath) {
|
|
2038
|
+
path12 = mergePath(this._basePath, path12);
|
|
2038
2039
|
const r = {
|
|
2039
2040
|
basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
|
|
2040
|
-
path:
|
|
2041
|
+
path: path12,
|
|
2041
2042
|
method,
|
|
2042
2043
|
handler
|
|
2043
2044
|
};
|
|
2044
|
-
this.router.add(method,
|
|
2045
|
+
this.router.add(method, path12, [handler, r]);
|
|
2045
2046
|
this.routes.push(r);
|
|
2046
2047
|
}
|
|
2047
2048
|
#handleError(err, c) {
|
|
@@ -2054,10 +2055,10 @@ var Hono = class _Hono {
|
|
|
2054
2055
|
if (method === "HEAD") {
|
|
2055
2056
|
return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
|
|
2056
2057
|
}
|
|
2057
|
-
const
|
|
2058
|
-
const matchResult = this.router.match(method,
|
|
2058
|
+
const path12 = this.getPath(request, { env });
|
|
2059
|
+
const matchResult = this.router.match(method, path12);
|
|
2059
2060
|
const c = new Context(request, {
|
|
2060
|
-
path:
|
|
2061
|
+
path: path12,
|
|
2061
2062
|
matchResult,
|
|
2062
2063
|
env,
|
|
2063
2064
|
executionCtx,
|
|
@@ -2160,7 +2161,7 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
|
|
|
2160
2161
|
|
|
2161
2162
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
2162
2163
|
var emptyParam = [];
|
|
2163
|
-
function match(method,
|
|
2164
|
+
function match(method, path12) {
|
|
2164
2165
|
const matchers = this.buildAllMatchers();
|
|
2165
2166
|
const match2 = ((method2, path22) => {
|
|
2166
2167
|
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
|
@@ -2176,7 +2177,7 @@ function match(method, path11) {
|
|
|
2176
2177
|
return [matcher[1][index], match3];
|
|
2177
2178
|
});
|
|
2178
2179
|
this.match = match2;
|
|
2179
|
-
return match2(method,
|
|
2180
|
+
return match2(method, path12);
|
|
2180
2181
|
}
|
|
2181
2182
|
|
|
2182
2183
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
@@ -2293,14 +2294,14 @@ var Trie = class {
|
|
|
2293
2294
|
#index = 0;
|
|
2294
2295
|
// dynamic path -> [handler index, param assoc]; static paths are not registered
|
|
2295
2296
|
paths = createNullObject();
|
|
2296
|
-
insert(
|
|
2297
|
+
insert(path12, isStatic) {
|
|
2297
2298
|
if (isStatic) {
|
|
2298
|
-
this.#root.insert(
|
|
2299
|
+
this.#root.insert(path12.split(""), 0, [], this.#context, true);
|
|
2299
2300
|
return;
|
|
2300
2301
|
}
|
|
2301
2302
|
const paramAssoc = [];
|
|
2302
2303
|
const groups = [];
|
|
2303
|
-
let markedPath =
|
|
2304
|
+
let markedPath = path12;
|
|
2304
2305
|
for (let i = 0; ; ) {
|
|
2305
2306
|
let replaced = false;
|
|
2306
2307
|
markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
|
|
@@ -2325,7 +2326,7 @@ var Trie = class {
|
|
|
2325
2326
|
}
|
|
2326
2327
|
}
|
|
2327
2328
|
this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
|
|
2328
|
-
this.paths[
|
|
2329
|
+
this.paths[path12] = [this.#index++, paramAssoc];
|
|
2329
2330
|
}
|
|
2330
2331
|
buildRegExp() {
|
|
2331
2332
|
let regexp = this.#root.buildRegExpStr();
|
|
@@ -2352,17 +2353,17 @@ var Trie = class {
|
|
|
2352
2353
|
|
|
2353
2354
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
|
|
2354
2355
|
var wildcardRegExpCache = createNullObject();
|
|
2355
|
-
function buildWildcardRegExp(
|
|
2356
|
-
return wildcardRegExpCache[
|
|
2357
|
-
`^${
|
|
2356
|
+
function buildWildcardRegExp(path12) {
|
|
2357
|
+
return wildcardRegExpCache[path12] ??= new RegExp(
|
|
2358
|
+
`^${path12.replace(
|
|
2358
2359
|
/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g,
|
|
2359
2360
|
(match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`
|
|
2360
2361
|
)}$`
|
|
2361
2362
|
);
|
|
2362
2363
|
}
|
|
2363
|
-
function findMiddleware(middleware,
|
|
2364
|
+
function findMiddleware(middleware, path12) {
|
|
2364
2365
|
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
|
|
2365
|
-
if (buildWildcardRegExp(k).test(
|
|
2366
|
+
if (buildWildcardRegExp(k).test(path12)) {
|
|
2366
2367
|
return [...middleware[k]];
|
|
2367
2368
|
}
|
|
2368
2369
|
}
|
|
@@ -2378,14 +2379,14 @@ var RegExpRouter = class {
|
|
|
2378
2379
|
this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
|
|
2379
2380
|
this.#tries = { [METHOD_NAME_ALL]: new Trie() };
|
|
2380
2381
|
}
|
|
2381
|
-
#insertPath(method,
|
|
2382
|
+
#insertPath(method, path12) {
|
|
2382
2383
|
try {
|
|
2383
|
-
this.#tries[method].insert(
|
|
2384
|
+
this.#tries[method].insert(path12, !/\*|\/:/.test(path12));
|
|
2384
2385
|
} catch (e) {
|
|
2385
|
-
throw e === PATH_ERROR ? new UnsupportedPathError(
|
|
2386
|
+
throw e === PATH_ERROR ? new UnsupportedPathError(path12) : e;
|
|
2386
2387
|
}
|
|
2387
2388
|
}
|
|
2388
|
-
add(method,
|
|
2389
|
+
add(method, path12, handler) {
|
|
2389
2390
|
const middleware = this.#middleware;
|
|
2390
2391
|
const routes = this.#routes;
|
|
2391
2392
|
if (!middleware) {
|
|
@@ -2401,28 +2402,28 @@ var RegExpRouter = class {
|
|
|
2401
2402
|
}
|
|
2402
2403
|
}
|
|
2403
2404
|
}
|
|
2404
|
-
if (
|
|
2405
|
-
|
|
2405
|
+
if (path12 === "/*") {
|
|
2406
|
+
path12 = "*";
|
|
2406
2407
|
}
|
|
2407
2408
|
const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
|
|
2408
|
-
if (/\*$/.test(
|
|
2409
|
-
const re = buildWildcardRegExp(
|
|
2409
|
+
if (/\*$/.test(path12)) {
|
|
2410
|
+
const re = buildWildcardRegExp(path12);
|
|
2410
2411
|
for (const m of methods) {
|
|
2411
|
-
if (!middleware[m][
|
|
2412
|
-
this.#insertPath(m,
|
|
2413
|
-
middleware[m][
|
|
2412
|
+
if (!middleware[m][path12]) {
|
|
2413
|
+
this.#insertPath(m, path12);
|
|
2414
|
+
middleware[m][path12] = findMiddleware(middleware[m], path12) || findMiddleware(middleware[METHOD_NAME_ALL], path12) || [];
|
|
2414
2415
|
}
|
|
2415
2416
|
}
|
|
2416
2417
|
for (const handlerMap of [middleware, routes]) {
|
|
2417
2418
|
for (const m of methods) {
|
|
2418
2419
|
for (const p in handlerMap[m]) {
|
|
2419
|
-
re.test(p) && handlerMap[m][p].push([handler,
|
|
2420
|
+
re.test(p) && handlerMap[m][p].push([handler, path12]);
|
|
2420
2421
|
}
|
|
2421
2422
|
}
|
|
2422
2423
|
}
|
|
2423
2424
|
return;
|
|
2424
2425
|
}
|
|
2425
|
-
const paths = checkOptionalParameter(
|
|
2426
|
+
const paths = checkOptionalParameter(path12) || [path12];
|
|
2426
2427
|
for (const path22 of paths) {
|
|
2427
2428
|
for (const m of methods) {
|
|
2428
2429
|
if (!routes[m][path22]) {
|
|
@@ -2451,11 +2452,11 @@ var RegExpRouter = class {
|
|
|
2451
2452
|
const handlerData = [];
|
|
2452
2453
|
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
|
|
2453
2454
|
for (const r of [middleware, routes]) {
|
|
2454
|
-
for (const
|
|
2455
|
-
const handlers = r[
|
|
2456
|
-
const pathData = trie.paths[
|
|
2455
|
+
for (const path12 in r) {
|
|
2456
|
+
const handlers = r[path12];
|
|
2457
|
+
const pathData = trie.paths[path12];
|
|
2457
2458
|
if (!pathData) {
|
|
2458
|
-
staticMap[
|
|
2459
|
+
staticMap[path12] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
|
|
2459
2460
|
continue;
|
|
2460
2461
|
}
|
|
2461
2462
|
handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
|
|
@@ -2479,13 +2480,13 @@ var SmartRouter = class {
|
|
|
2479
2480
|
constructor(init) {
|
|
2480
2481
|
this.#routers = init.routers;
|
|
2481
2482
|
}
|
|
2482
|
-
add(method,
|
|
2483
|
+
add(method, path12, handler) {
|
|
2483
2484
|
if (!this.#routes) {
|
|
2484
2485
|
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
2485
2486
|
}
|
|
2486
|
-
this.#routes.push([method,
|
|
2487
|
+
this.#routes.push([method, path12, handler]);
|
|
2487
2488
|
}
|
|
2488
|
-
match(method,
|
|
2489
|
+
match(method, path12) {
|
|
2489
2490
|
if (!this.#routes) {
|
|
2490
2491
|
throw new Error("Fatal error");
|
|
2491
2492
|
}
|
|
@@ -2500,7 +2501,7 @@ var SmartRouter = class {
|
|
|
2500
2501
|
for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
|
|
2501
2502
|
router.add(...routes[i2]);
|
|
2502
2503
|
}
|
|
2503
|
-
res = router.match(method,
|
|
2504
|
+
res = router.match(method, path12);
|
|
2504
2505
|
} catch (e) {
|
|
2505
2506
|
if (e instanceof UnsupportedPathError) {
|
|
2506
2507
|
continue;
|
|
@@ -2535,9 +2536,9 @@ var Node2 = class _Node2 {
|
|
|
2535
2536
|
#patterns = [];
|
|
2536
2537
|
#pattern;
|
|
2537
2538
|
#params = emptyParams;
|
|
2538
|
-
insert(method,
|
|
2539
|
+
insert(method, path12, handler) {
|
|
2539
2540
|
let curNode = this;
|
|
2540
|
-
const parts = splitRoutingPath(
|
|
2541
|
+
const parts = splitRoutingPath(path12);
|
|
2541
2542
|
const possibleKeys = /* @__PURE__ */ new Set();
|
|
2542
2543
|
let i = 0;
|
|
2543
2544
|
for (const p of parts) {
|
|
@@ -2577,12 +2578,12 @@ var Node2 = class _Node2 {
|
|
|
2577
2578
|
}
|
|
2578
2579
|
}
|
|
2579
2580
|
}
|
|
2580
|
-
search(method,
|
|
2581
|
+
search(method, path12) {
|
|
2581
2582
|
const handlerSets = [];
|
|
2582
2583
|
this.#params = emptyParams;
|
|
2583
2584
|
const curNode = this;
|
|
2584
2585
|
let curNodes = [curNode];
|
|
2585
|
-
const parts = splitPath(
|
|
2586
|
+
const parts = splitPath(path12);
|
|
2586
2587
|
const curNodesQueue = [];
|
|
2587
2588
|
const len = parts.length;
|
|
2588
2589
|
let partOffsets = null;
|
|
@@ -2624,13 +2625,13 @@ var Node2 = class _Node2 {
|
|
|
2624
2625
|
if (matcher !== true) {
|
|
2625
2626
|
if (!partOffsets) {
|
|
2626
2627
|
partOffsets = [];
|
|
2627
|
-
let offset =
|
|
2628
|
+
let offset = path12[0] === "/" ? 1 : 0;
|
|
2628
2629
|
for (let p = 0; p < len; p++) {
|
|
2629
2630
|
partOffsets[p] = offset;
|
|
2630
2631
|
offset += parts[p].length + 1;
|
|
2631
2632
|
}
|
|
2632
2633
|
}
|
|
2633
|
-
const restPathString =
|
|
2634
|
+
const restPathString = path12.slice(partOffsets[i]);
|
|
2634
2635
|
const m = matcher.exec(restPathString);
|
|
2635
2636
|
if (m) {
|
|
2636
2637
|
params[name] = m[0];
|
|
@@ -2690,13 +2691,13 @@ var Node2 = class _Node2 {
|
|
|
2690
2691
|
var TrieRouter = class {
|
|
2691
2692
|
name = "TrieRouter";
|
|
2692
2693
|
#node = new Node2();
|
|
2693
|
-
add(method,
|
|
2694
|
-
for (const result of checkOptionalParameter(
|
|
2694
|
+
add(method, path12, handler) {
|
|
2695
|
+
for (const result of checkOptionalParameter(path12) || [path12]) {
|
|
2695
2696
|
this.#node.insert(method, result, handler);
|
|
2696
2697
|
}
|
|
2697
2698
|
}
|
|
2698
|
-
match(method,
|
|
2699
|
-
return this.#node.search(method,
|
|
2699
|
+
match(method, path12) {
|
|
2700
|
+
return this.#node.search(method, path12);
|
|
2700
2701
|
}
|
|
2701
2702
|
};
|
|
2702
2703
|
|
|
@@ -2902,6 +2903,11 @@ function parseSuffix(rest) {
|
|
|
2902
2903
|
if (!isValidRef(ref)) throw new TargetKeyError(`invalid base ref: ${ref}`);
|
|
2903
2904
|
return { kind: "base", ref };
|
|
2904
2905
|
}
|
|
2906
|
+
if (rest.startsWith("checkpoint:")) {
|
|
2907
|
+
const n = rest.slice("checkpoint:".length);
|
|
2908
|
+
if (!/^[1-9][0-9]{0,8}$/.test(n)) throw new TargetKeyError(`invalid checkpoint: ${n}`);
|
|
2909
|
+
return { kind: "checkpoint", id: Number(n) };
|
|
2910
|
+
}
|
|
2905
2911
|
if (rest.startsWith("range:")) {
|
|
2906
2912
|
const spec = rest.slice("range:".length);
|
|
2907
2913
|
const idx = spec.indexOf("..");
|
|
@@ -2917,7 +2923,7 @@ function parseTargetKey(key) {
|
|
|
2917
2923
|
if (typeof key !== "string" || !key) throw new TargetKeyError("empty target key");
|
|
2918
2924
|
if (key.startsWith("worktree:")) {
|
|
2919
2925
|
const rest = key.slice("worktree:".length);
|
|
2920
|
-
const m = /^(.+):(working|staged|all|commit:.+|range:.+|base
|
|
2926
|
+
const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+|checkpoint:[0-9]+)$/.exec(rest);
|
|
2921
2927
|
if (!m?.[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
|
|
2922
2928
|
const worktree = m[1];
|
|
2923
2929
|
if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
|
|
@@ -2942,6 +2948,9 @@ function formatTargetKey(t) {
|
|
|
2942
2948
|
case "base":
|
|
2943
2949
|
suffix = `base:${t.ref}`;
|
|
2944
2950
|
break;
|
|
2951
|
+
case "checkpoint":
|
|
2952
|
+
suffix = `checkpoint:${t.id}`;
|
|
2953
|
+
break;
|
|
2945
2954
|
}
|
|
2946
2955
|
return t.worktree ? `worktree:${t.worktree}:${suffix}` : suffix;
|
|
2947
2956
|
}
|
|
@@ -2992,6 +3001,17 @@ function moveBefore(list, id, before) {
|
|
|
2992
3001
|
return rest;
|
|
2993
3002
|
}
|
|
2994
3003
|
|
|
3004
|
+
// packages/shared/src/comments.ts
|
|
3005
|
+
function commentWorktree(c) {
|
|
3006
|
+
return tryParseTargetKey(c.targetKey)?.worktree;
|
|
3007
|
+
}
|
|
3008
|
+
function awaitsAgent(c) {
|
|
3009
|
+
return !c.exportedAt;
|
|
3010
|
+
}
|
|
3011
|
+
function awaitsReviewer(c) {
|
|
3012
|
+
return c.replies?.at(-1)?.author === "agent";
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2995
3015
|
// packages/server/src/errors.ts
|
|
2996
3016
|
var HttpError = class extends Error {
|
|
2997
3017
|
constructor(status, message, code) {
|
|
@@ -3075,6 +3095,14 @@ var GitError = class extends HttpError {
|
|
|
3075
3095
|
exitCode;
|
|
3076
3096
|
stderr;
|
|
3077
3097
|
};
|
|
3098
|
+
function snapshotVars(snapshot) {
|
|
3099
|
+
if (!snapshot) return {};
|
|
3100
|
+
return {
|
|
3101
|
+
GIT_OBJECT_DIRECTORY: snapshot.objects,
|
|
3102
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: snapshot.alternates,
|
|
3103
|
+
...snapshot.index ? { GIT_INDEX_FILE: snapshot.index } : {}
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3078
3106
|
function assertAllowedGitArgs(args) {
|
|
3079
3107
|
const sub = args[0];
|
|
3080
3108
|
if (!sub || !ALLOWED_SUBCOMMANDS.has(sub)) {
|
|
@@ -3120,7 +3148,8 @@ async function runGit(args, opts) {
|
|
|
3120
3148
|
LC_ALL: "C",
|
|
3121
3149
|
GIT_PAGER: "cat",
|
|
3122
3150
|
PAGER: "cat",
|
|
3123
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
3151
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
3152
|
+
...snapshotVars(opts.snapshot)
|
|
3124
3153
|
},
|
|
3125
3154
|
windowsHide: true
|
|
3126
3155
|
},
|
|
@@ -3212,6 +3241,14 @@ async function applyOnce(cwd, patch, opts) {
|
|
|
3212
3241
|
});
|
|
3213
3242
|
}
|
|
3214
3243
|
async function runGitWrite(args, opts) {
|
|
3244
|
+
return execWrite(args, opts, {});
|
|
3245
|
+
}
|
|
3246
|
+
async function runGitSnapshot(args, opts) {
|
|
3247
|
+
const allowed = args[0] === "add" && args[1] === "--all" && args.slice(2).every((a) => a === "--intent-to-add") || args[0] === "write-tree" && args[1] === "--missing-ok" && args.length === 2;
|
|
3248
|
+
if (!allowed) throw new GitError(`not a snapshot command: git ${args.join(" ")}`, null, "", 400, "git_subcommand_forbidden");
|
|
3249
|
+
return execWrite(args, opts, { GIT_OBJECT_DIRECTORY: opts.objects, GIT_INDEX_FILE: opts.index });
|
|
3250
|
+
}
|
|
3251
|
+
function execWrite(args, opts, extraEnv) {
|
|
3215
3252
|
for (const arg of args) if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
|
|
3216
3253
|
return new Promise((resolve, reject) => {
|
|
3217
3254
|
execFile(
|
|
@@ -3222,7 +3259,7 @@ async function runGitWrite(args, opts) {
|
|
|
3222
3259
|
timeout: opts.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
|
|
3223
3260
|
maxBuffer: 16 * 1024 * 1024,
|
|
3224
3261
|
encoding: "utf8",
|
|
3225
|
-
env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
|
|
3262
|
+
env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0", ...extraEnv },
|
|
3226
3263
|
windowsHide: true
|
|
3227
3264
|
},
|
|
3228
3265
|
(error, stdout, stderr) => {
|
|
@@ -3374,8 +3411,18 @@ async function getRepoInfo(ctx, defaultTarget) {
|
|
|
3374
3411
|
}
|
|
3375
3412
|
|
|
3376
3413
|
// packages/server/src/targets.ts
|
|
3414
|
+
import path4 from "path";
|
|
3415
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3416
|
+
|
|
3417
|
+
// packages/server/src/checkpoints.ts
|
|
3418
|
+
import path3 from "path";
|
|
3419
|
+
import { randomUUID } from "crypto";
|
|
3420
|
+
import { copyFile, mkdir as mkdir2, rm, stat as stat2, utimes } from "fs/promises";
|
|
3421
|
+
|
|
3422
|
+
// packages/server/src/state.ts
|
|
3423
|
+
import os from "os";
|
|
3377
3424
|
import path2 from "path";
|
|
3378
|
-
import { readFile } from "fs/promises";
|
|
3425
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "fs/promises";
|
|
3379
3426
|
|
|
3380
3427
|
// packages/server/src/hash.ts
|
|
3381
3428
|
import { createHash } from "crypto";
|
|
@@ -3391,6 +3438,278 @@ function hunkHash(lineContents) {
|
|
|
3391
3438
|
);
|
|
3392
3439
|
}
|
|
3393
3440
|
|
|
3441
|
+
// packages/server/src/state.ts
|
|
3442
|
+
function dataDir() {
|
|
3443
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
3444
|
+
const base = xdg?.trim() ? xdg : path2.join(os.homedir(), ".local", "share");
|
|
3445
|
+
return path2.join(base, "warden");
|
|
3446
|
+
}
|
|
3447
|
+
function repoHash(repoRoot) {
|
|
3448
|
+
return sha1(repoRoot).slice(0, 12);
|
|
3449
|
+
}
|
|
3450
|
+
function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
3451
|
+
return path2.join(baseDir, repoHash(repoRoot), "state.json");
|
|
3452
|
+
}
|
|
3453
|
+
function defaultPrefs() {
|
|
3454
|
+
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
|
|
3455
|
+
}
|
|
3456
|
+
function defaultState(repoRoot) {
|
|
3457
|
+
return { schemaVersion: 1, repoRoot, targets: {}, todos: [], checkpoints: [], prefs: defaultPrefs() };
|
|
3458
|
+
}
|
|
3459
|
+
function ensureTarget(state, key) {
|
|
3460
|
+
let t = state.targets[key];
|
|
3461
|
+
if (!t) {
|
|
3462
|
+
t = { viewed: {}, comments: [] };
|
|
3463
|
+
state.targets[key] = t;
|
|
3464
|
+
}
|
|
3465
|
+
return t;
|
|
3466
|
+
}
|
|
3467
|
+
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
3468
|
+
var usableCheckpoint = (v) => {
|
|
3469
|
+
const c = v;
|
|
3470
|
+
return !!c && typeof c === "object" && Number.isInteger(c.id) && typeof c.tree === "string" && (c.worktree === void 0 || typeof c.worktree === "string");
|
|
3471
|
+
};
|
|
3472
|
+
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
3473
|
+
function normalise(raw2, repoRoot) {
|
|
3474
|
+
const base = defaultState(repoRoot);
|
|
3475
|
+
if (!raw2 || typeof raw2 !== "object") return base;
|
|
3476
|
+
const r = raw2;
|
|
3477
|
+
if (r.schemaVersion !== 1) return base;
|
|
3478
|
+
const targets = {};
|
|
3479
|
+
for (const [k, v] of Object.entries(r.targets ?? {})) {
|
|
3480
|
+
if (!v || typeof v !== "object") continue;
|
|
3481
|
+
targets[k] = {
|
|
3482
|
+
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
3483
|
+
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
3484
|
+
...typeof v.head === "string" ? { head: v.head } : {}
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
migrateLocalViews(targets);
|
|
3488
|
+
return {
|
|
3489
|
+
schemaVersion: 1,
|
|
3490
|
+
repoRoot: r.repoRoot ?? repoRoot,
|
|
3491
|
+
targets,
|
|
3492
|
+
// Issues were folded into todos (a todo links comments now); the few there were are let go,
|
|
3493
|
+
// and the next write leaves them out of the file.
|
|
3494
|
+
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
3495
|
+
checkpoints: Array.isArray(r.checkpoints) ? r.checkpoints.filter(usableCheckpoint) : [],
|
|
3496
|
+
prefs: {
|
|
3497
|
+
...defaultPrefs(),
|
|
3498
|
+
...r.prefs ?? {},
|
|
3499
|
+
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
3500
|
+
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
3501
|
+
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
|
|
3502
|
+
ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
function migrateLocalViews(targets) {
|
|
3507
|
+
for (const key of Object.keys(targets).sort()) {
|
|
3508
|
+
const scope = commentScopeKey(key);
|
|
3509
|
+
if (scope === key) continue;
|
|
3510
|
+
const src = targets[key];
|
|
3511
|
+
delete targets[key];
|
|
3512
|
+
if (src.comments.length === 0) continue;
|
|
3513
|
+
let dst = targets[scope];
|
|
3514
|
+
if (!dst) {
|
|
3515
|
+
dst = { viewed: {}, comments: [] };
|
|
3516
|
+
targets[scope] = dst;
|
|
3517
|
+
}
|
|
3518
|
+
dst.comments.push(...src.comments);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
var LOCK_STALE_MS = 5e3;
|
|
3522
|
+
var LOCK_WAIT_MS = 5e3;
|
|
3523
|
+
async function sleep(ms) {
|
|
3524
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
3525
|
+
}
|
|
3526
|
+
var StateStore = class {
|
|
3527
|
+
constructor(file, repoRoot) {
|
|
3528
|
+
this.file = file;
|
|
3529
|
+
this.repoRoot = repoRoot;
|
|
3530
|
+
}
|
|
3531
|
+
file;
|
|
3532
|
+
repoRoot;
|
|
3533
|
+
queue = Promise.resolve();
|
|
3534
|
+
async load() {
|
|
3535
|
+
try {
|
|
3536
|
+
const text = await readFile(this.file, "utf8");
|
|
3537
|
+
return normalise(JSON.parse(text), this.repoRoot);
|
|
3538
|
+
} catch (e) {
|
|
3539
|
+
const err = e;
|
|
3540
|
+
if (err.code === "ENOENT") return defaultState(this.repoRoot);
|
|
3541
|
+
if (e instanceof SyntaxError) {
|
|
3542
|
+
try {
|
|
3543
|
+
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
3544
|
+
} catch {
|
|
3545
|
+
}
|
|
3546
|
+
return defaultState(this.repoRoot);
|
|
3547
|
+
}
|
|
3548
|
+
throw e;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
/** Apply a mutation under lock. The callback receives the freshest on-disk state. */
|
|
3552
|
+
update(fn) {
|
|
3553
|
+
const run2 = async () => {
|
|
3554
|
+
await mkdir(path2.dirname(this.file), { recursive: true });
|
|
3555
|
+
const release = await this.acquireLock();
|
|
3556
|
+
try {
|
|
3557
|
+
const state = await this.load();
|
|
3558
|
+
const result = await fn(state);
|
|
3559
|
+
await this.writeAtomic(state);
|
|
3560
|
+
return result;
|
|
3561
|
+
} finally {
|
|
3562
|
+
await release();
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
const p = this.queue.then(run2, run2);
|
|
3566
|
+
this.queue = p.catch(() => void 0);
|
|
3567
|
+
return p;
|
|
3568
|
+
}
|
|
3569
|
+
async writeAtomic(state) {
|
|
3570
|
+
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
3571
|
+
await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3572
|
+
await rename(tmp, this.file);
|
|
3573
|
+
}
|
|
3574
|
+
async acquireLock() {
|
|
3575
|
+
const lock = `${this.file}.lock`;
|
|
3576
|
+
const started = Date.now();
|
|
3577
|
+
for (; ; ) {
|
|
3578
|
+
try {
|
|
3579
|
+
const fh = await open(lock, "wx");
|
|
3580
|
+
await fh.writeFile(String(process.pid));
|
|
3581
|
+
await fh.close();
|
|
3582
|
+
return async () => {
|
|
3583
|
+
try {
|
|
3584
|
+
await unlink(lock);
|
|
3585
|
+
} catch {
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3588
|
+
} catch (e) {
|
|
3589
|
+
const err = e;
|
|
3590
|
+
if (err.code !== "EEXIST") throw e;
|
|
3591
|
+
try {
|
|
3592
|
+
const st = await stat(lock);
|
|
3593
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
3594
|
+
await unlink(lock).catch(() => void 0);
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
} catch {
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
if (Date.now() - started > LOCK_WAIT_MS) {
|
|
3601
|
+
throw new Error(`timed out waiting for state lock ${lock}`);
|
|
3602
|
+
}
|
|
3603
|
+
await sleep(20);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
function forgetWorktreeTargets(state, worktreePath) {
|
|
3609
|
+
const prefix = `worktree:${worktreePath}:`;
|
|
3610
|
+
const gone = /* @__PURE__ */ new Set();
|
|
3611
|
+
for (const key of Object.keys(state.targets)) {
|
|
3612
|
+
if (!key.startsWith(prefix)) continue;
|
|
3613
|
+
for (const c of state.targets[key].comments) gone.add(c.id);
|
|
3614
|
+
delete state.targets[key];
|
|
3615
|
+
}
|
|
3616
|
+
unlinkComments(state, gone);
|
|
3617
|
+
state.checkpoints = state.checkpoints.filter((c) => c.worktree !== worktreePath);
|
|
3618
|
+
}
|
|
3619
|
+
function unlinkComments(state, ids) {
|
|
3620
|
+
const gone = new Set(ids);
|
|
3621
|
+
if (gone.size === 0) return;
|
|
3622
|
+
for (const todo of state.todos) {
|
|
3623
|
+
if (!todo.commentIds?.some((id) => gone.has(id))) continue;
|
|
3624
|
+
const kept = todo.commentIds.filter((id) => !gone.has(id));
|
|
3625
|
+
if (kept.length) todo.commentIds = kept;
|
|
3626
|
+
else delete todo.commentIds;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// packages/server/src/checkpoints.ts
|
|
3631
|
+
function checkpointStore(stateFile) {
|
|
3632
|
+
return path3.join(path3.dirname(stateFile), "checkpoints");
|
|
3633
|
+
}
|
|
3634
|
+
async function repoObjects(cwd) {
|
|
3635
|
+
const r = await runGit(["rev-parse", "--git-path", "objects"], { cwd });
|
|
3636
|
+
return path3.resolve(cwd, r.stdout.trim());
|
|
3637
|
+
}
|
|
3638
|
+
async function repoIndex(cwd) {
|
|
3639
|
+
const r = await runGit(["rev-parse", "--git-path", "index"], { cwd });
|
|
3640
|
+
return path3.resolve(cwd, r.stdout.trim());
|
|
3641
|
+
}
|
|
3642
|
+
async function checkpointObjects(cwd, store) {
|
|
3643
|
+
return { objects: path3.join(store, "objects"), alternates: await repoObjects(cwd) };
|
|
3644
|
+
}
|
|
3645
|
+
async function withIndexCopy(cwd, store, fn) {
|
|
3646
|
+
const tmp = path3.join(store, "tmp");
|
|
3647
|
+
await mkdir2(path3.join(store, "objects"), { recursive: true });
|
|
3648
|
+
await mkdir2(tmp, { recursive: true });
|
|
3649
|
+
const [objects, index] = await Promise.all([checkpointObjects(cwd, store), repoIndex(cwd)]);
|
|
3650
|
+
const copy = path3.join(tmp, `index-${randomUUID()}`);
|
|
3651
|
+
try {
|
|
3652
|
+
const st = await stat2(index).catch(() => void 0);
|
|
3653
|
+
if (st) {
|
|
3654
|
+
await copyFile(index, copy);
|
|
3655
|
+
const earlier = Math.floor(st.mtimeMs / 1e3) - 1;
|
|
3656
|
+
await utimes(copy, earlier, earlier);
|
|
3657
|
+
}
|
|
3658
|
+
return await fn({ ...objects, index: copy });
|
|
3659
|
+
} finally {
|
|
3660
|
+
await rm(copy, { force: true });
|
|
3661
|
+
await rm(`${copy}.lock`, { force: true });
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
async function snapshotWorktree(cwd, store) {
|
|
3665
|
+
return withIndexCopy(cwd, store, async (snapshot) => {
|
|
3666
|
+
await runGitSnapshot(["add", "--all"], { cwd, ...snapshot });
|
|
3667
|
+
const r = await runGitSnapshot(["write-tree", "--missing-ok"], { cwd, ...snapshot });
|
|
3668
|
+
return r.stdout.trim();
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
async function withCheckpointIndex(cwd, store, fn) {
|
|
3672
|
+
return withIndexCopy(cwd, store, async (snapshot) => {
|
|
3673
|
+
await runGitSnapshot(["add", "--all", "--intent-to-add"], { cwd, ...snapshot });
|
|
3674
|
+
return fn(snapshot);
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
var MAX_CHECKPOINTS = 20;
|
|
3678
|
+
var sameWorktree = (c, worktree) => c.worktree === worktree;
|
|
3679
|
+
function checkpointsOf(state, worktree) {
|
|
3680
|
+
return state.checkpoints.filter((c) => sameWorktree(c, worktree));
|
|
3681
|
+
}
|
|
3682
|
+
function findCheckpoint(state, worktree, id) {
|
|
3683
|
+
return state.checkpoints.find((c) => sameWorktree(c, worktree) && c.id === id);
|
|
3684
|
+
}
|
|
3685
|
+
function checkpointKey(c) {
|
|
3686
|
+
return formatTargetKey({ kind: "checkpoint", id: c.id, ...c.worktree ? { worktree: c.worktree } : {} });
|
|
3687
|
+
}
|
|
3688
|
+
function forgetCheckpoint(state, c) {
|
|
3689
|
+
state.checkpoints = state.checkpoints.filter((x) => x !== c);
|
|
3690
|
+
const key = checkpointKey(c);
|
|
3691
|
+
const gone = new Set((state.targets[key]?.comments ?? []).map((x) => x.id));
|
|
3692
|
+
delete state.targets[key];
|
|
3693
|
+
unlinkComments(state, gone);
|
|
3694
|
+
}
|
|
3695
|
+
function addCheckpoint(state, worktree, tree, head, handoff = false) {
|
|
3696
|
+
const mine = checkpointsOf(state, worktree);
|
|
3697
|
+
const id = mine.reduce((n, c) => Math.max(n, c.id), 0) + 1;
|
|
3698
|
+
const checkpoint = { id, ...worktree ? { worktree } : {}, tree, head, createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...handoff ? { handoff } : {} };
|
|
3699
|
+
state.checkpoints.push(checkpoint);
|
|
3700
|
+
for (const old of mine.slice(0, Math.max(0, mine.length + 1 - MAX_CHECKPOINTS))) forgetCheckpoint(state, old);
|
|
3701
|
+
return checkpoint;
|
|
3702
|
+
}
|
|
3703
|
+
async function takeCheckpoint(store, cwd, worktree, opts = {}) {
|
|
3704
|
+
const tree = await snapshotWorktree(cwd, checkpointStore(store.file));
|
|
3705
|
+
const head = await revParse(cwd, "HEAD") ?? "";
|
|
3706
|
+
return store.update((s) => {
|
|
3707
|
+
const newest = checkpointsOf(s, worktree).at(-1);
|
|
3708
|
+
if (newest?.tree === tree) return { checkpoint: newest, unchanged: true };
|
|
3709
|
+
return { checkpoint: addCheckpoint(s, worktree, tree, head, opts.handoff), unchanged: false };
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3394
3713
|
// packages/server/src/diffparse.ts
|
|
3395
3714
|
function unquotePath(p) {
|
|
3396
3715
|
if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
|
|
@@ -3632,6 +3951,17 @@ function markDebugLines(diff, newSide, oldSide) {
|
|
|
3632
3951
|
}
|
|
3633
3952
|
|
|
3634
3953
|
// packages/server/src/targets.ts
|
|
3954
|
+
function checkpointOf(ctx) {
|
|
3955
|
+
if (!ctx.checkpoint) throw new Error(`checkpoint of ${ctx.key} was not looked up`);
|
|
3956
|
+
return ctx.checkpoint;
|
|
3957
|
+
}
|
|
3958
|
+
async function objectsFor(ctx) {
|
|
3959
|
+
return ctx.target.kind === "checkpoint" ? { snapshot: await checkpointObjects(ctx.cwd, checkpointOf(ctx).store) } : {};
|
|
3960
|
+
}
|
|
3961
|
+
async function runDiff(ctx, args) {
|
|
3962
|
+
if (ctx.target.kind !== "checkpoint") return (await runGit(args, { cwd: ctx.cwd })).stdout;
|
|
3963
|
+
return withCheckpointIndex(ctx.cwd, checkpointOf(ctx).store, async (snapshot) => (await runGit(args, { cwd: ctx.cwd, snapshot })).stdout);
|
|
3964
|
+
}
|
|
3635
3965
|
var DIFF_BASE_ARGS = ["diff", "--no-color", "--no-ext-diff", "-U3", "-M", "--find-renames"];
|
|
3636
3966
|
function resolveTargetContext(repo, worktrees, key) {
|
|
3637
3967
|
let target;
|
|
@@ -3685,6 +4015,8 @@ async function diffArgs(ctx) {
|
|
|
3685
4015
|
}
|
|
3686
4016
|
case "base":
|
|
3687
4017
|
return [...DIFF_BASE_ARGS, await baseSha(ctx, t.ref)];
|
|
4018
|
+
case "checkpoint":
|
|
4019
|
+
return [...DIFF_BASE_ARGS, checkpointOf(ctx).tree];
|
|
3688
4020
|
}
|
|
3689
4021
|
}
|
|
3690
4022
|
function includesUntracked(t) {
|
|
@@ -3726,8 +4058,7 @@ async function mapLimit(items, limit, fn) {
|
|
|
3726
4058
|
}
|
|
3727
4059
|
async function listTargetDiffs(ctx) {
|
|
3728
4060
|
const args = await diffArgs(ctx);
|
|
3729
|
-
const
|
|
3730
|
-
const files = parseUnifiedDiff(r.stdout);
|
|
4061
|
+
const files = parseUnifiedDiff(await runDiff(ctx, args));
|
|
3731
4062
|
if (includesUntracked(ctx.target)) {
|
|
3732
4063
|
const untracked = await listUntracked(ctx.cwd);
|
|
3733
4064
|
const extra = await mapLimit(untracked, 8, (f) => untrackedDiff(ctx.cwd, f).catch(() => void 0));
|
|
@@ -3749,8 +4080,7 @@ async function getFileDiff(ctx, filePath, hints = {}) {
|
|
|
3749
4080
|
const args = await diffArgs(ctx);
|
|
3750
4081
|
const pathspec = [literal(filePath)];
|
|
3751
4082
|
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
|
|
3752
|
-
const
|
|
3753
|
-
const files = parseUnifiedDiff(r.stdout);
|
|
4083
|
+
const files = parseUnifiedDiff(await runDiff(ctx, [...args, "--", ...pathspec]));
|
|
3754
4084
|
return files.find((f) => f.path === filePath) ?? files[0];
|
|
3755
4085
|
}
|
|
3756
4086
|
async function refForSide(ctx, side) {
|
|
@@ -3770,6 +4100,8 @@ async function refForSide(ctx, side) {
|
|
|
3770
4100
|
return await mergeBase(ctx.cwd, t.base, t.head) ?? t.base;
|
|
3771
4101
|
case "base":
|
|
3772
4102
|
return side === "new" ? void 0 : baseSha(ctx, t.ref);
|
|
4103
|
+
case "checkpoint":
|
|
4104
|
+
return side === "new" ? void 0 : checkpointOf(ctx).tree;
|
|
3773
4105
|
}
|
|
3774
4106
|
}
|
|
3775
4107
|
async function getFullFile(ctx, filePath, side) {
|
|
@@ -3777,14 +4109,14 @@ async function getFullFile(ctx, filePath, side) {
|
|
|
3777
4109
|
const ref = await refForSide(ctx, side);
|
|
3778
4110
|
if (ref === void 0) {
|
|
3779
4111
|
try {
|
|
3780
|
-
const buf = await
|
|
4112
|
+
const buf = await readFile2(path4.join(ctx.cwd, filePath));
|
|
3781
4113
|
return buf.toString("utf8");
|
|
3782
4114
|
} catch {
|
|
3783
4115
|
return null;
|
|
3784
4116
|
}
|
|
3785
4117
|
}
|
|
3786
4118
|
try {
|
|
3787
|
-
const r = await runGit(["show", `${ref}:${filePath}`], { cwd: ctx.cwd });
|
|
4119
|
+
const r = await runGit(["show", `${ref}:${filePath}`], { cwd: ctx.cwd, ...await objectsFor(ctx) });
|
|
3788
4120
|
return r.stdout;
|
|
3789
4121
|
} catch (e) {
|
|
3790
4122
|
if (e instanceof HttpError && e.code === "git_failed") return null;
|
|
@@ -3801,9 +4133,10 @@ async function pathsWithMarkers(ctx, side, paths) {
|
|
|
3801
4133
|
else if (ref === ":0") args.push("--cached");
|
|
3802
4134
|
else args.push(ref);
|
|
3803
4135
|
const prefix = ref === void 0 || ref === ":0" ? "" : `${ref}:`;
|
|
4136
|
+
const objects = await objectsFor(ctx);
|
|
3804
4137
|
for (let i = 0; i < paths.length; i += GREP_CHUNK) {
|
|
3805
4138
|
const chunk = paths.slice(i, i + GREP_CHUNK).map(literal);
|
|
3806
|
-
const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1] });
|
|
4139
|
+
const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1], ...objects });
|
|
3807
4140
|
for (const p of r.stdout.split("\0")) if (p) found.add(p.startsWith(prefix) ? p.slice(prefix.length) : p);
|
|
3808
4141
|
}
|
|
3809
4142
|
return found;
|
|
@@ -4076,15 +4409,15 @@ function buildStagePatch(diff, mode, selection, opts = {}) {
|
|
|
4076
4409
|
}
|
|
4077
4410
|
|
|
4078
4411
|
// packages/server/src/worktrees.ts
|
|
4079
|
-
import
|
|
4080
|
-
import { readdir, realpath as realpath2, stat } from "fs/promises";
|
|
4412
|
+
import path5 from "path";
|
|
4413
|
+
import { readdir, realpath as realpath2, stat as stat3 } from "fs/promises";
|
|
4081
4414
|
function slotPath(ctx, slot) {
|
|
4082
|
-
return
|
|
4415
|
+
return path5.join(path5.dirname(ctx.commonRoot), `${path5.basename(ctx.commonRoot)}-${slot}`);
|
|
4083
4416
|
}
|
|
4084
4417
|
function slotOf(ctx, p) {
|
|
4085
|
-
if (
|
|
4086
|
-
const prefix = `${
|
|
4087
|
-
const name =
|
|
4418
|
+
if (path5.dirname(p) !== path5.dirname(ctx.commonRoot)) return void 0;
|
|
4419
|
+
const prefix = `${path5.basename(ctx.commonRoot)}-`;
|
|
4420
|
+
const name = path5.basename(p);
|
|
4088
4421
|
const n = name.startsWith(prefix) ? name.slice(prefix.length) : "";
|
|
4089
4422
|
return /^[1-9]\d*$/.test(n) ? Number(n) : void 0;
|
|
4090
4423
|
}
|
|
@@ -4169,7 +4502,7 @@ async function assertBranchName(ctx, name) {
|
|
|
4169
4502
|
}
|
|
4170
4503
|
}
|
|
4171
4504
|
async function dirUsable(p) {
|
|
4172
|
-
const st = await
|
|
4505
|
+
const st = await stat3(p).catch((e) => {
|
|
4173
4506
|
if (e.code === "ENOENT") return void 0;
|
|
4174
4507
|
throw e;
|
|
4175
4508
|
});
|
|
@@ -4247,8 +4580,8 @@ async function checkoutWorktree(ctx, req) {
|
|
|
4247
4580
|
if (e instanceof GitError && /already (checked out|used by worktree)/.test(e.stderr)) throw new HttpError(409, e.message, "branch_in_use");
|
|
4248
4581
|
throw e;
|
|
4249
4582
|
}
|
|
4250
|
-
const
|
|
4251
|
-
const worktree = (await listWorktrees(ctx)).find((w) => w.path ===
|
|
4583
|
+
const real2 = await realpath2(target.path).catch(() => target.path);
|
|
4584
|
+
const worktree = (await listWorktrees(ctx)).find((w) => w.path === real2);
|
|
4252
4585
|
if (!worktree) throw new HttpError(500, `git checked ${branch} out at ${target.path} but does not list it`, "git_failed");
|
|
4253
4586
|
return { worktree, slot: target.slot, reused: target.reuse };
|
|
4254
4587
|
}
|
|
@@ -4266,7 +4599,7 @@ async function dropBranch(ctx, wt, asked) {
|
|
|
4266
4599
|
}
|
|
4267
4600
|
async function releaseWorktree(ctx, req) {
|
|
4268
4601
|
const p = req.path.trim();
|
|
4269
|
-
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path ===
|
|
4602
|
+
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4270
4603
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4271
4604
|
if (wt.isMain) throw badRequest("the main worktree cannot be released", "main_worktree");
|
|
4272
4605
|
if (wt.bare || slotOf(ctx, wt.path) === void 0) throw badRequest(`${wt.path} is not a slot; remove it instead`, "not_a_slot");
|
|
@@ -4282,7 +4615,7 @@ async function releaseWorktree(ctx, req) {
|
|
|
4282
4615
|
}
|
|
4283
4616
|
async function removeWorktree(ctx, req) {
|
|
4284
4617
|
const p = req.path.trim();
|
|
4285
|
-
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path ===
|
|
4618
|
+
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4286
4619
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4287
4620
|
if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
|
|
4288
4621
|
const args = ["worktree", "remove"];
|
|
@@ -4302,181 +4635,6 @@ async function removeWorktree(ctx, req) {
|
|
|
4302
4635
|
return dropBranch(ctx, wt, req.deleteBranch);
|
|
4303
4636
|
}
|
|
4304
4637
|
|
|
4305
|
-
// packages/server/src/state.ts
|
|
4306
|
-
import os from "os";
|
|
4307
|
-
import path4 from "path";
|
|
4308
|
-
import { mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "fs/promises";
|
|
4309
|
-
function dataDir() {
|
|
4310
|
-
const xdg = process.env.XDG_DATA_HOME;
|
|
4311
|
-
const base = xdg?.trim() ? xdg : path4.join(os.homedir(), ".local", "share");
|
|
4312
|
-
return path4.join(base, "warden");
|
|
4313
|
-
}
|
|
4314
|
-
function repoHash(repoRoot) {
|
|
4315
|
-
return sha1(repoRoot).slice(0, 12);
|
|
4316
|
-
}
|
|
4317
|
-
function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
4318
|
-
return path4.join(baseDir, repoHash(repoRoot), "state.json");
|
|
4319
|
-
}
|
|
4320
|
-
function defaultPrefs() {
|
|
4321
|
-
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
|
|
4322
|
-
}
|
|
4323
|
-
function defaultState(repoRoot) {
|
|
4324
|
-
return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
|
|
4325
|
-
}
|
|
4326
|
-
function ensureTarget(state, key) {
|
|
4327
|
-
let t = state.targets[key];
|
|
4328
|
-
if (!t) {
|
|
4329
|
-
t = { viewed: {}, comments: [] };
|
|
4330
|
-
state.targets[key] = t;
|
|
4331
|
-
}
|
|
4332
|
-
return t;
|
|
4333
|
-
}
|
|
4334
|
-
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
4335
|
-
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
4336
|
-
function normalise(raw2, repoRoot) {
|
|
4337
|
-
const base = defaultState(repoRoot);
|
|
4338
|
-
if (!raw2 || typeof raw2 !== "object") return base;
|
|
4339
|
-
const r = raw2;
|
|
4340
|
-
if (r.schemaVersion !== 1) return base;
|
|
4341
|
-
const targets = {};
|
|
4342
|
-
for (const [k, v] of Object.entries(r.targets ?? {})) {
|
|
4343
|
-
if (!v || typeof v !== "object") continue;
|
|
4344
|
-
targets[k] = {
|
|
4345
|
-
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
4346
|
-
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
4347
|
-
...typeof v.head === "string" ? { head: v.head } : {}
|
|
4348
|
-
};
|
|
4349
|
-
}
|
|
4350
|
-
migrateLocalViews(targets);
|
|
4351
|
-
return {
|
|
4352
|
-
schemaVersion: 1,
|
|
4353
|
-
repoRoot: r.repoRoot ?? repoRoot,
|
|
4354
|
-
targets,
|
|
4355
|
-
issues: Array.isArray(r.issues) ? r.issues.filter(usable) : [],
|
|
4356
|
-
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
4357
|
-
prefs: {
|
|
4358
|
-
...defaultPrefs(),
|
|
4359
|
-
...r.prefs ?? {},
|
|
4360
|
-
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
4361
|
-
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
4362
|
-
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
|
|
4363
|
-
ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
|
|
4364
|
-
}
|
|
4365
|
-
};
|
|
4366
|
-
}
|
|
4367
|
-
function migrateLocalViews(targets) {
|
|
4368
|
-
for (const key of Object.keys(targets).sort()) {
|
|
4369
|
-
const scope = commentScopeKey(key);
|
|
4370
|
-
if (scope === key) continue;
|
|
4371
|
-
const src = targets[key];
|
|
4372
|
-
delete targets[key];
|
|
4373
|
-
if (src.comments.length === 0) continue;
|
|
4374
|
-
let dst = targets[scope];
|
|
4375
|
-
if (!dst) {
|
|
4376
|
-
dst = { viewed: {}, comments: [] };
|
|
4377
|
-
targets[scope] = dst;
|
|
4378
|
-
}
|
|
4379
|
-
dst.comments.push(...src.comments);
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
var LOCK_STALE_MS = 5e3;
|
|
4383
|
-
var LOCK_WAIT_MS = 5e3;
|
|
4384
|
-
async function sleep(ms) {
|
|
4385
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
4386
|
-
}
|
|
4387
|
-
var StateStore = class {
|
|
4388
|
-
constructor(file, repoRoot) {
|
|
4389
|
-
this.file = file;
|
|
4390
|
-
this.repoRoot = repoRoot;
|
|
4391
|
-
}
|
|
4392
|
-
file;
|
|
4393
|
-
repoRoot;
|
|
4394
|
-
queue = Promise.resolve();
|
|
4395
|
-
async load() {
|
|
4396
|
-
try {
|
|
4397
|
-
const text = await readFile2(this.file, "utf8");
|
|
4398
|
-
return normalise(JSON.parse(text), this.repoRoot);
|
|
4399
|
-
} catch (e) {
|
|
4400
|
-
const err = e;
|
|
4401
|
-
if (err.code === "ENOENT") return defaultState(this.repoRoot);
|
|
4402
|
-
if (e instanceof SyntaxError) {
|
|
4403
|
-
try {
|
|
4404
|
-
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
4405
|
-
} catch {
|
|
4406
|
-
}
|
|
4407
|
-
return defaultState(this.repoRoot);
|
|
4408
|
-
}
|
|
4409
|
-
throw e;
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
/** Apply a mutation under lock. The callback receives the freshest on-disk state. */
|
|
4413
|
-
update(fn) {
|
|
4414
|
-
const run2 = async () => {
|
|
4415
|
-
await mkdir(path4.dirname(this.file), { recursive: true });
|
|
4416
|
-
const release = await this.acquireLock();
|
|
4417
|
-
try {
|
|
4418
|
-
const state = await this.load();
|
|
4419
|
-
const result = await fn(state);
|
|
4420
|
-
await this.writeAtomic(state);
|
|
4421
|
-
return result;
|
|
4422
|
-
} finally {
|
|
4423
|
-
await release();
|
|
4424
|
-
}
|
|
4425
|
-
};
|
|
4426
|
-
const p = this.queue.then(run2, run2);
|
|
4427
|
-
this.queue = p.catch(() => void 0);
|
|
4428
|
-
return p;
|
|
4429
|
-
}
|
|
4430
|
-
async writeAtomic(state) {
|
|
4431
|
-
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
4432
|
-
await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
4433
|
-
await rename(tmp, this.file);
|
|
4434
|
-
}
|
|
4435
|
-
async acquireLock() {
|
|
4436
|
-
const lock = `${this.file}.lock`;
|
|
4437
|
-
const started = Date.now();
|
|
4438
|
-
for (; ; ) {
|
|
4439
|
-
try {
|
|
4440
|
-
const fh = await open(lock, "wx");
|
|
4441
|
-
await fh.writeFile(String(process.pid));
|
|
4442
|
-
await fh.close();
|
|
4443
|
-
return async () => {
|
|
4444
|
-
try {
|
|
4445
|
-
await unlink(lock);
|
|
4446
|
-
} catch {
|
|
4447
|
-
}
|
|
4448
|
-
};
|
|
4449
|
-
} catch (e) {
|
|
4450
|
-
const err = e;
|
|
4451
|
-
if (err.code !== "EEXIST") throw e;
|
|
4452
|
-
try {
|
|
4453
|
-
const st = await stat2(lock);
|
|
4454
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
4455
|
-
await unlink(lock).catch(() => void 0);
|
|
4456
|
-
continue;
|
|
4457
|
-
}
|
|
4458
|
-
} catch {
|
|
4459
|
-
continue;
|
|
4460
|
-
}
|
|
4461
|
-
if (Date.now() - started > LOCK_WAIT_MS) {
|
|
4462
|
-
throw new Error(`timed out waiting for state lock ${lock}`);
|
|
4463
|
-
}
|
|
4464
|
-
await sleep(20);
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
}
|
|
4468
|
-
};
|
|
4469
|
-
function forgetWorktreeTargets(state, worktreePath) {
|
|
4470
|
-
const prefix = `worktree:${worktreePath}:`;
|
|
4471
|
-
const gone = /* @__PURE__ */ new Set();
|
|
4472
|
-
for (const key of Object.keys(state.targets)) {
|
|
4473
|
-
if (!key.startsWith(prefix)) continue;
|
|
4474
|
-
for (const c of state.targets[key].comments) gone.add(c.id);
|
|
4475
|
-
delete state.targets[key];
|
|
4476
|
-
}
|
|
4477
|
-
if (gone.size) for (const issue of state.issues) issue.commentIds = issue.commentIds.filter((id) => !gone.has(id));
|
|
4478
|
-
}
|
|
4479
|
-
|
|
4480
4638
|
// packages/server/src/export.ts
|
|
4481
4639
|
var LANG_BY_EXT = {
|
|
4482
4640
|
ts: "ts",
|
|
@@ -4524,36 +4682,123 @@ function langForPath(filePath) {
|
|
|
4524
4682
|
if (!m?.[1]) return "";
|
|
4525
4683
|
return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
|
|
4526
4684
|
}
|
|
4685
|
+
function shortId(id) {
|
|
4686
|
+
return id.slice(0, 8);
|
|
4687
|
+
}
|
|
4688
|
+
var quote = (text) => text.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
|
|
4527
4689
|
function formatCommentSection(c) {
|
|
4528
4690
|
const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
|
|
4529
4691
|
const width = String(c.endLine).length;
|
|
4530
4692
|
const code = c.codeSnippet.map((line, i) => `${String(c.startLine + i).padStart(width, " ")} | ${line}`).join("\n");
|
|
4531
4693
|
const lang = langForPath(c.filePath);
|
|
4532
|
-
const body = c.body.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
|
|
4533
4694
|
const fence = code.includes("```") ? "````" : "```";
|
|
4534
4695
|
const orphanNote = c.status === "orphaned" ? " [orphaned: original location no longer exists]" : "";
|
|
4535
|
-
|
|
4696
|
+
const parts = [`## ${c.filePath}:${range} (${c.side}) [id: ${shortId(c.id)}]${orphanNote}`, `${fence}${lang}`, code, fence, quote(c.body)];
|
|
4697
|
+
for (const r of c.replies ?? []) parts.push(`${r.author === "agent" ? "Agent" : "Reviewer"} replied:
|
|
4698
|
+
${quote(r.body)}`);
|
|
4699
|
+
return parts.join("\n");
|
|
4536
4700
|
}
|
|
4537
|
-
function
|
|
4701
|
+
function replyFooter(replyCommand) {
|
|
4702
|
+
return `When you have dealt with a comment, answer it with \`${replyCommand} reply <id> "<what you changed, or why you did not>"\` so the reviewer sees your answer beside it.`;
|
|
4703
|
+
}
|
|
4704
|
+
function formatCommentsExport({ repoRoot, comments, replyCommand }) {
|
|
4538
4705
|
const targets = [...new Set(comments.map((c) => c.targetKey))];
|
|
4539
4706
|
const head = ["# Review comments", `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
|
|
4540
4707
|
const sections = comments.map(formatCommentSection);
|
|
4541
|
-
|
|
4708
|
+
const tail = replyCommand && comments.length > 0 ? [replyFooter(replyCommand)] : [];
|
|
4709
|
+
return [head.join("\n"), ...sections, ...tail].join("\n\n") + "\n";
|
|
4542
4710
|
}
|
|
4543
|
-
function
|
|
4544
|
-
const
|
|
4545
|
-
const
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4711
|
+
function formatTodoExport({ todo, comments, replyCommand }) {
|
|
4712
|
+
const body = todo.body.replace(/\r\n/g, "\n").trim();
|
|
4713
|
+
const parts = [todo.title, ...body ? [body] : [], ...comments.map(formatCommentSection)];
|
|
4714
|
+
if (replyCommand && comments.length > 0) parts.push(replyFooter(replyCommand));
|
|
4715
|
+
return parts.join("\n\n");
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// packages/server/src/feedback.ts
|
|
4719
|
+
import { realpath as realpath3 } from "fs/promises";
|
|
4720
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4721
|
+
async function real(p) {
|
|
4722
|
+
try {
|
|
4723
|
+
return await realpath3(p);
|
|
4724
|
+
} catch {
|
|
4725
|
+
return p;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
function worktreeOf(c, mainRoot) {
|
|
4729
|
+
return commentWorktree(c) ?? mainRoot;
|
|
4730
|
+
}
|
|
4731
|
+
async function commentsIn(state, worktreeRoot, mainRoot) {
|
|
4732
|
+
const want = await real(worktreeRoot);
|
|
4733
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4734
|
+
const out = [];
|
|
4735
|
+
for (const t of Object.values(state.targets)) {
|
|
4736
|
+
for (const c of t.comments) {
|
|
4737
|
+
const wt = worktreeOf(c, mainRoot);
|
|
4738
|
+
let p = resolved.get(wt);
|
|
4739
|
+
if (!p) {
|
|
4740
|
+
p = real(wt);
|
|
4741
|
+
resolved.set(wt, p);
|
|
4742
|
+
}
|
|
4743
|
+
if (await p === want) out.push(c);
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
return out;
|
|
4747
|
+
}
|
|
4748
|
+
async function takeFeedback(store, opts) {
|
|
4749
|
+
if (opts.peek) return (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
|
|
4750
|
+
return store.update(async (s) => {
|
|
4751
|
+
const ids = new Set((await commentsIn(s, opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent).map((c) => c.id));
|
|
4752
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4753
|
+
const taken = [];
|
|
4754
|
+
for (const t of Object.values(s.targets)) {
|
|
4755
|
+
t.comments = t.comments.map((c) => {
|
|
4756
|
+
if (!ids.has(c.id)) return c;
|
|
4757
|
+
const next = { ...c, exportedAt: now, updatedAt: now, status: c.status === "active" ? "exported" : c.status };
|
|
4758
|
+
taken.push(next);
|
|
4759
|
+
return next;
|
|
4760
|
+
});
|
|
4761
|
+
}
|
|
4762
|
+
return taken;
|
|
4763
|
+
});
|
|
4764
|
+
}
|
|
4765
|
+
function locate(state, ref) {
|
|
4766
|
+
const needle = ref.trim().toLowerCase();
|
|
4767
|
+
if (!needle) throw badRequest("a comment id is required", "bad_comment_id");
|
|
4768
|
+
const hits = [];
|
|
4769
|
+
for (const t of Object.values(state.targets)) {
|
|
4770
|
+
t.comments.forEach((c, index) => {
|
|
4771
|
+
if (c.id === needle) hits.unshift({ list: t.comments, index });
|
|
4772
|
+
else if (c.id.startsWith(needle)) hits.push({ list: t.comments, index });
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
const exact = hits[0] && hits[0].list[hits[0].index].id === needle;
|
|
4776
|
+
if (hits.length === 0) throw notFound(`no comment with id ${ref}; it may have been resolved or deleted`, "unknown_comment");
|
|
4777
|
+
if (!exact && hits.length > 1) throw badRequest(`id ${ref} matches ${hits.length} comments; give more of it`, "ambiguous_comment");
|
|
4778
|
+
return hits[0];
|
|
4779
|
+
}
|
|
4780
|
+
async function addReply(store, ref, author, body) {
|
|
4781
|
+
if (!body.trim()) throw badRequest("reply must not be empty", "empty_reply");
|
|
4782
|
+
return store.update((s) => {
|
|
4783
|
+
const { list, index } = locate(s, ref);
|
|
4784
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4785
|
+
const reply2 = { id: randomUUID2(), author, body: body.trim(), at: now };
|
|
4786
|
+
const prev = list[index];
|
|
4787
|
+
const next = { ...prev, replies: [...prev.replies ?? [], reply2], updatedAt: now };
|
|
4788
|
+
if (author === "reviewer") {
|
|
4789
|
+
delete next.exportedAt;
|
|
4790
|
+
if (next.status === "exported") next.status = "active";
|
|
4791
|
+
}
|
|
4792
|
+
list[index] = next;
|
|
4793
|
+
return next;
|
|
4794
|
+
});
|
|
4550
4795
|
}
|
|
4551
4796
|
|
|
4552
4797
|
// packages/server/src/nvim.ts
|
|
4553
4798
|
import os2 from "os";
|
|
4554
|
-
import
|
|
4799
|
+
import path6 from "path";
|
|
4555
4800
|
import { execFile as execFile2 } from "child_process";
|
|
4556
|
-
import { readdir as readdir2, stat as
|
|
4801
|
+
import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
4557
4802
|
var QUERY_TIMEOUT_MS = 500;
|
|
4558
4803
|
var CACHE_TTL_MS = 1e4;
|
|
4559
4804
|
function execNvim(args, timeoutMs) {
|
|
@@ -4582,14 +4827,14 @@ function socketDirs(env = process.env) {
|
|
|
4582
4827
|
}
|
|
4583
4828
|
}
|
|
4584
4829
|
const tmp = env.TMPDIR || "/tmp";
|
|
4585
|
-
if (user) dirs.push(
|
|
4586
|
-
if (env.NVIM_LISTEN_ADDRESS) dirs.push(
|
|
4830
|
+
if (user) dirs.push(path6.join(tmp, `nvim.${user}`));
|
|
4831
|
+
if (env.NVIM_LISTEN_ADDRESS) dirs.push(path6.dirname(env.NVIM_LISTEN_ADDRESS));
|
|
4587
4832
|
return [...new Set(dirs)];
|
|
4588
4833
|
}
|
|
4589
4834
|
var SOCKET_NAME = /^nvim\.(\d+)\.\d+$/;
|
|
4590
4835
|
async function isSocket(p) {
|
|
4591
4836
|
try {
|
|
4592
|
-
return (await
|
|
4837
|
+
return (await stat4(p)).isSocket();
|
|
4593
4838
|
} catch {
|
|
4594
4839
|
return false;
|
|
4595
4840
|
}
|
|
@@ -4604,7 +4849,7 @@ async function findSockets(dirs = socketDirs()) {
|
|
|
4604
4849
|
return;
|
|
4605
4850
|
}
|
|
4606
4851
|
for (const ent of entries) {
|
|
4607
|
-
const full =
|
|
4852
|
+
const full = path6.join(dir, ent.name);
|
|
4608
4853
|
const m = SOCKET_NAME.exec(ent.name);
|
|
4609
4854
|
if (m && (ent.isSocket() || await isSocket(full))) {
|
|
4610
4855
|
found.set(full, Number(m[1]));
|
|
@@ -4670,7 +4915,7 @@ var NvimService = class {
|
|
|
4670
4915
|
|
|
4671
4916
|
// packages/server/src/tmux.ts
|
|
4672
4917
|
import { execFile as execFile3 } from "child_process";
|
|
4673
|
-
import { realpath as
|
|
4918
|
+
import { realpath as realpath4 } from "fs/promises";
|
|
4674
4919
|
var runTmux = (args) => new Promise((resolve, reject) => {
|
|
4675
4920
|
execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
4676
4921
|
if (!error) return resolve(stdout);
|
|
@@ -4686,29 +4931,29 @@ var TmuxService = class {
|
|
|
4686
4931
|
}
|
|
4687
4932
|
run;
|
|
4688
4933
|
async sessions(mainRoot) {
|
|
4689
|
-
const root = await
|
|
4934
|
+
const root = await realpath4(mainRoot);
|
|
4690
4935
|
const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]);
|
|
4691
4936
|
const sessions = [];
|
|
4692
4937
|
for (const line of output.trimEnd().split("\n")) {
|
|
4693
4938
|
const [id, name, ...parts] = line.split(" ");
|
|
4694
|
-
const
|
|
4695
|
-
if (!id || !/^\$\d+$/.test(id) || !name || !
|
|
4696
|
-
if (await
|
|
4939
|
+
const path12 = parts.join(" ");
|
|
4940
|
+
if (!id || !/^\$\d+$/.test(id) || !name || !path12) continue;
|
|
4941
|
+
if (await realpath4(path12).catch(() => null) === root) sessions.push({ id, name, path: path12 });
|
|
4697
4942
|
}
|
|
4698
4943
|
return sessions;
|
|
4699
4944
|
}
|
|
4700
4945
|
async open(mainRoot, worktreePath, sessionId) {
|
|
4701
4946
|
const session = (await this.sessions(mainRoot)).find((s) => s.id === sessionId);
|
|
4702
4947
|
if (!session) throw new HttpError(409, "\u4E3B\u4ED3\u5E93\u5BF9\u5E94\u7684 tmux session \u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "tmux_session_missing");
|
|
4703
|
-
const directory = await
|
|
4948
|
+
const directory = await realpath4(worktreePath);
|
|
4704
4949
|
const window = (await this.run(["new-window", "-d", "-P", "-F", "#{window_id}", "-t", `${session.id}:`, "-c", directory.replaceAll("#", "##")])).trim();
|
|
4705
4950
|
return { session: session.name, window };
|
|
4706
4951
|
}
|
|
4707
4952
|
};
|
|
4708
4953
|
|
|
4709
4954
|
// packages/server/src/watcher.ts
|
|
4710
|
-
import
|
|
4711
|
-
import { stat as
|
|
4955
|
+
import path7 from "path";
|
|
4956
|
+
import { stat as stat5 } from "fs/promises";
|
|
4712
4957
|
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
4713
4958
|
function statusPaths(out) {
|
|
4714
4959
|
const parts = out.split("\0");
|
|
@@ -4804,7 +5049,7 @@ var RepoWatcher = class {
|
|
|
4804
5049
|
const stats = await Promise.all(
|
|
4805
5050
|
statusPaths(statusRes.stdout).map(async (rel) => {
|
|
4806
5051
|
try {
|
|
4807
|
-
const st = await
|
|
5052
|
+
const st = await stat5(path7.join(this.root, rel));
|
|
4808
5053
|
return `${rel} ${st.mtimeMs} ${st.size}`;
|
|
4809
5054
|
} catch {
|
|
4810
5055
|
return `${rel} -`;
|
|
@@ -4816,8 +5061,8 @@ var RepoWatcher = class {
|
|
|
4816
5061
|
};
|
|
4817
5062
|
|
|
4818
5063
|
// packages/server/src/static.ts
|
|
4819
|
-
import
|
|
4820
|
-
import { readFile as readFile3, stat as
|
|
5064
|
+
import path8 from "path";
|
|
5065
|
+
import { readFile as readFile3, stat as stat6 } from "fs/promises";
|
|
4821
5066
|
var MIME = {
|
|
4822
5067
|
".html": "text/html; charset=utf-8",
|
|
4823
5068
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -4834,20 +5079,20 @@ var MIME = {
|
|
|
4834
5079
|
".txt": "text/plain; charset=utf-8"
|
|
4835
5080
|
};
|
|
4836
5081
|
async function serveStaticFile(c, webDir) {
|
|
4837
|
-
const root =
|
|
5082
|
+
const root = path8.resolve(webDir);
|
|
4838
5083
|
let rel;
|
|
4839
5084
|
try {
|
|
4840
5085
|
rel = decodeURIComponent(c.req.path);
|
|
4841
5086
|
} catch {
|
|
4842
5087
|
return c.text("Bad Request", 400);
|
|
4843
5088
|
}
|
|
4844
|
-
let target =
|
|
4845
|
-
if (target !== root && !target.startsWith(root +
|
|
5089
|
+
let target = path8.resolve(root, "." + rel);
|
|
5090
|
+
if (target !== root && !target.startsWith(root + path8.sep)) return c.text("Forbidden", 403);
|
|
4846
5091
|
try {
|
|
4847
|
-
const st = await
|
|
4848
|
-
if (st.isDirectory()) target =
|
|
5092
|
+
const st = await stat6(target);
|
|
5093
|
+
if (st.isDirectory()) target = path8.join(target, "index.html");
|
|
4849
5094
|
} catch {
|
|
4850
|
-
target =
|
|
5095
|
+
target = path8.join(root, "index.html");
|
|
4851
5096
|
}
|
|
4852
5097
|
let body;
|
|
4853
5098
|
try {
|
|
@@ -4855,7 +5100,7 @@ async function serveStaticFile(c, webDir) {
|
|
|
4855
5100
|
} catch {
|
|
4856
5101
|
return c.text("Not Found", 404);
|
|
4857
5102
|
}
|
|
4858
|
-
const ext =
|
|
5103
|
+
const ext = path8.extname(target).toLowerCase();
|
|
4859
5104
|
const isAsset = rel.startsWith("/assets/");
|
|
4860
5105
|
return new Response(new Uint8Array(body), {
|
|
4861
5106
|
status: 200,
|
|
@@ -4868,6 +5113,7 @@ async function serveStaticFile(c, webDir) {
|
|
|
4868
5113
|
|
|
4869
5114
|
// packages/server/src/app.ts
|
|
4870
5115
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
5116
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
4871
5117
|
var LISTING_HINT_TTL_MS = 5 * 6e4;
|
|
4872
5118
|
function createApp(opts) {
|
|
4873
5119
|
const { repo, store } = opts;
|
|
@@ -4891,7 +5137,12 @@ function createApp(opts) {
|
|
|
4891
5137
|
worktreeCache = { at: Date.now(), list };
|
|
4892
5138
|
return list;
|
|
4893
5139
|
};
|
|
4894
|
-
const
|
|
5140
|
+
const checkpoints = checkpointStore(store.file);
|
|
5141
|
+
const stateStamp = () => stat7(store.file).then(
|
|
5142
|
+
(st) => st.mtimeMs,
|
|
5143
|
+
() => 0
|
|
5144
|
+
);
|
|
5145
|
+
const worktreeCtx = async (key) => {
|
|
4895
5146
|
let list = await worktrees();
|
|
4896
5147
|
try {
|
|
4897
5148
|
return resolveTargetContext(repo, list, key);
|
|
@@ -4903,6 +5154,13 @@ function createApp(opts) {
|
|
|
4903
5154
|
throw e;
|
|
4904
5155
|
}
|
|
4905
5156
|
};
|
|
5157
|
+
const targetCtx = async (key) => {
|
|
5158
|
+
const ctx = await worktreeCtx(key);
|
|
5159
|
+
if (ctx.target.kind !== "checkpoint") return ctx;
|
|
5160
|
+
const cp = findCheckpoint(await store.load(), ctx.target.worktree, ctx.target.id);
|
|
5161
|
+
if (!cp) throw notFound(`no checkpoint #${ctx.target.id} in this worktree`, "unknown_checkpoint");
|
|
5162
|
+
return { ...ctx, checkpoint: { tree: cp.tree, store: checkpoints } };
|
|
5163
|
+
};
|
|
4906
5164
|
const fileDiffWithHints = async (ctx, filePath, explicit) => {
|
|
4907
5165
|
const cached = listings.get(ctx.key);
|
|
4908
5166
|
const hint = cached && Date.now() - cached.at < LISTING_HINT_TTL_MS ? cached.files.find((f) => f.path === filePath) : void 0;
|
|
@@ -4911,6 +5169,14 @@ function createApp(opts) {
|
|
|
4911
5169
|
untracked: explicit?.untracked ?? hint?.untracked
|
|
4912
5170
|
});
|
|
4913
5171
|
};
|
|
5172
|
+
const checkpointHandoff = (comments) => {
|
|
5173
|
+
const worktrees2 = new Set(comments.map((x) => tryParseTargetKey(x.targetKey)?.worktree));
|
|
5174
|
+
for (const wt of worktrees2) {
|
|
5175
|
+
takeCheckpoint(store, wt ?? repo.root, wt, { handoff: true }).catch((e) => {
|
|
5176
|
+
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.root}: ${e instanceof Error ? e.message : e}`);
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
};
|
|
4914
5180
|
const findComment = (state, id) => {
|
|
4915
5181
|
for (const [targetKey, t] of Object.entries(state.targets)) {
|
|
4916
5182
|
const index = t.comments.findIndex((c) => c.id === id);
|
|
@@ -4918,6 +5184,13 @@ function createApp(opts) {
|
|
|
4918
5184
|
}
|
|
4919
5185
|
return void 0;
|
|
4920
5186
|
};
|
|
5187
|
+
const linkable = (state, raw2) => {
|
|
5188
|
+
if (!Array.isArray(raw2) || raw2.some((x) => typeof x !== "string")) throw badRequest("commentIds must be a list of ids");
|
|
5189
|
+
const ids = [...new Set(raw2)];
|
|
5190
|
+
const missing = ids.find((x) => !findComment(state, x));
|
|
5191
|
+
if (missing) throw notFound(`no comment with id ${missing}`, "unknown_comment");
|
|
5192
|
+
return ids;
|
|
5193
|
+
};
|
|
4921
5194
|
app.onError((err, c) => {
|
|
4922
5195
|
if (err instanceof HttpError) {
|
|
4923
5196
|
return c.json({ error: err.message, code: err.code }, err.status);
|
|
@@ -4925,10 +5198,20 @@ function createApp(opts) {
|
|
|
4925
5198
|
console.error(err);
|
|
4926
5199
|
return c.json({ error: err instanceof Error ? err.message : String(err), code: "internal" }, 500);
|
|
4927
5200
|
});
|
|
5201
|
+
app.use("*", async (c, next) => {
|
|
5202
|
+
const host = new URL(c.req.url).hostname;
|
|
5203
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
5204
|
+
throw new HttpError(403, `requests for host ${host} are refused; open warden at http://127.0.0.1 or http://localhost`, "bad_host");
|
|
5205
|
+
}
|
|
5206
|
+
await next();
|
|
5207
|
+
});
|
|
4928
5208
|
const api = new Hono2();
|
|
4929
5209
|
api.use("*", async (c, next) => {
|
|
4930
|
-
if (c.req.method !== "GET" && c.req.method !== "HEAD"
|
|
4931
|
-
|
|
5210
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
5211
|
+
const origin = c.req.header("origin");
|
|
5212
|
+
if (c.req.header("sec-fetch-site") === "cross-site" || origin && origin !== new URL(c.req.url).origin) {
|
|
5213
|
+
throw new HttpError(403, "cross-site requests are refused", "cross_site");
|
|
5214
|
+
}
|
|
4932
5215
|
}
|
|
4933
5216
|
await next();
|
|
4934
5217
|
});
|
|
@@ -4939,7 +5222,7 @@ function createApp(opts) {
|
|
|
4939
5222
|
worktreeCache = { at: Date.now(), list: info.worktrees };
|
|
4940
5223
|
const last = state.prefs.lastTarget;
|
|
4941
5224
|
const target = last ? tryParseTargetKey(last) : void 0;
|
|
4942
|
-
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
|
|
5225
|
+
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree)) && (target.kind !== "checkpoint" || !!findCheckpoint(state, target.worktree, target.id));
|
|
4943
5226
|
if (last && usable2) info.defaultTarget = last;
|
|
4944
5227
|
else if (last) {
|
|
4945
5228
|
await store.update((s) => {
|
|
@@ -5038,6 +5321,27 @@ function createApp(opts) {
|
|
|
5038
5321
|
changedNotices.get(key)?.delete(body.path);
|
|
5039
5322
|
return c.json({ viewed });
|
|
5040
5323
|
});
|
|
5324
|
+
api.get("/targets/:key/checkpoints", async (c) => {
|
|
5325
|
+
const ctx = await worktreeCtx(c.req.param("key"));
|
|
5326
|
+
const res = { checkpoints: checkpointsOf(await store.load(), ctx.target.worktree) };
|
|
5327
|
+
return c.json(res);
|
|
5328
|
+
});
|
|
5329
|
+
api.post("/targets/:key/checkpoints", async (c) => {
|
|
5330
|
+
const ctx = await worktreeCtx(c.req.param("key"));
|
|
5331
|
+
const { checkpoint, unchanged } = await takeCheckpoint(store, ctx.cwd, ctx.target.worktree);
|
|
5332
|
+
const res = { checkpoint, unchanged, targetKey: checkpointKey(checkpoint) };
|
|
5333
|
+
return c.json(res, unchanged ? 200 : 201);
|
|
5334
|
+
});
|
|
5335
|
+
api.delete("/targets/:key/checkpoints/:id", async (c) => {
|
|
5336
|
+
const ctx = await worktreeCtx(c.req.param("key"));
|
|
5337
|
+
const id = Number(c.req.param("id"));
|
|
5338
|
+
await store.update((s) => {
|
|
5339
|
+
const cp = Number.isInteger(id) ? findCheckpoint(s, ctx.target.worktree, id) : void 0;
|
|
5340
|
+
if (!cp) throw notFound(`no checkpoint #${c.req.param("id")} in this worktree`, "unknown_checkpoint");
|
|
5341
|
+
forgetCheckpoint(s, cp);
|
|
5342
|
+
});
|
|
5343
|
+
return c.json({ ok: true });
|
|
5344
|
+
});
|
|
5041
5345
|
api.post("/targets/:key/stage", async (c) => {
|
|
5042
5346
|
const key = c.req.param("key");
|
|
5043
5347
|
const ctx = await targetCtx(key);
|
|
@@ -5079,7 +5383,7 @@ function createApp(opts) {
|
|
|
5079
5383
|
if (!anchored) throw badRequest("selection is not fully visible in the diff on that side (must be inside one hunk)", "bad_selection");
|
|
5080
5384
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5081
5385
|
const comment = {
|
|
5082
|
-
id:
|
|
5386
|
+
id: randomUUID3(),
|
|
5083
5387
|
targetKey: key,
|
|
5084
5388
|
filePath: body.filePath,
|
|
5085
5389
|
side: body.side,
|
|
@@ -5150,12 +5454,19 @@ function createApp(opts) {
|
|
|
5150
5454
|
if (!t) return false;
|
|
5151
5455
|
const before = t.comments.length;
|
|
5152
5456
|
t.comments = t.comments.filter((x) => x.id !== id);
|
|
5153
|
-
|
|
5457
|
+
unlinkComments(s, [id]);
|
|
5154
5458
|
return t.comments.length !== before;
|
|
5155
5459
|
});
|
|
5156
5460
|
if (!removed) throw notFound("comment not found");
|
|
5157
5461
|
return c.json({ ok: true });
|
|
5158
5462
|
});
|
|
5463
|
+
api.post("/targets/:key/comments/:id/replies", async (c) => {
|
|
5464
|
+
const body = await c.req.json();
|
|
5465
|
+
if (typeof body.body !== "string") throw badRequest("body is required");
|
|
5466
|
+
const found = findComment(await store.load(), c.req.param("id"));
|
|
5467
|
+
if (!found || found.targetKey !== commentScopeKey(c.req.param("key"))) throw notFound("comment not found");
|
|
5468
|
+
return c.json(await addReply(store, found.comment.id, "reviewer", body.body), 201);
|
|
5469
|
+
});
|
|
5159
5470
|
api.post("/targets/:key/comments/reanchor", async (c) => {
|
|
5160
5471
|
const key = c.req.param("key");
|
|
5161
5472
|
const ctx = await targetCtx(key);
|
|
@@ -5227,7 +5538,7 @@ function createApp(opts) {
|
|
|
5227
5538
|
const patch = located.get(cm.id);
|
|
5228
5539
|
if (!patch) {
|
|
5229
5540
|
if (!comments.some((x) => x.id === cm.id)) return [cm];
|
|
5230
|
-
if (committed) {
|
|
5541
|
+
if (committed && !cm.replies?.length) {
|
|
5231
5542
|
dropped.push(cm.id);
|
|
5232
5543
|
return [];
|
|
5233
5544
|
}
|
|
@@ -5237,9 +5548,7 @@ function createApp(opts) {
|
|
|
5237
5548
|
const unchanged = cm.targetKey === next.targetKey && cm.status === next.status && cm.startLine === next.startLine && cm.endLine === next.endLine && cm.anchor.hunkHash === next.anchor.hunkHash;
|
|
5238
5549
|
return [unchanged ? cm : { ...next, updatedAt: now }];
|
|
5239
5550
|
});
|
|
5240
|
-
|
|
5241
|
-
for (const issue of s.issues) issue.commentIds = issue.commentIds.filter((x) => x !== id);
|
|
5242
|
-
}
|
|
5551
|
+
unlinkComments(s, dropped);
|
|
5243
5552
|
t.head = head;
|
|
5244
5553
|
return t.comments;
|
|
5245
5554
|
});
|
|
@@ -5250,6 +5559,7 @@ function createApp(opts) {
|
|
|
5250
5559
|
const body = await c.req.json();
|
|
5251
5560
|
if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
|
|
5252
5561
|
const ids = body.commentIds.filter((x) => typeof x === "string");
|
|
5562
|
+
let handed = [];
|
|
5253
5563
|
const res = await store.update((s) => {
|
|
5254
5564
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5255
5565
|
const selected = [];
|
|
@@ -5261,94 +5571,14 @@ function createApp(opts) {
|
|
|
5261
5571
|
s.targets[found.targetKey].comments[found.index] = next;
|
|
5262
5572
|
selected.push(next);
|
|
5263
5573
|
}
|
|
5574
|
+
handed = selected;
|
|
5264
5575
|
return {
|
|
5265
|
-
text: formatCommentsExport({ repoRoot: repo.root, comments: selected }),
|
|
5576
|
+
text: formatCommentsExport({ repoRoot: repo.root, comments: selected, replyCommand: opts.replyCommand }),
|
|
5266
5577
|
count: selected.length,
|
|
5267
5578
|
commentIds: selected.map((x) => x.id)
|
|
5268
5579
|
};
|
|
5269
5580
|
});
|
|
5270
|
-
|
|
5271
|
-
});
|
|
5272
|
-
api.get("/issues", async (c) => {
|
|
5273
|
-
const s = await store.load();
|
|
5274
|
-
return c.json({ issues: s.issues });
|
|
5275
|
-
});
|
|
5276
|
-
api.post("/issues", async (c) => {
|
|
5277
|
-
const body = await c.req.json();
|
|
5278
|
-
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5279
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5280
|
-
const issue = {
|
|
5281
|
-
id: randomUUID(),
|
|
5282
|
-
title: body.title.trim(),
|
|
5283
|
-
body: typeof body.body === "string" ? body.body : "",
|
|
5284
|
-
status: body.status === "closed" ? "closed" : "open",
|
|
5285
|
-
commentIds: Array.isArray(body.commentIds) ? body.commentIds.filter((x) => typeof x === "string") : [],
|
|
5286
|
-
createdAt: now,
|
|
5287
|
-
updatedAt: now
|
|
5288
|
-
};
|
|
5289
|
-
await store.update((s) => {
|
|
5290
|
-
s.issues = insertAfter(s.issues, issue, typeof body.after === "string" ? body.after : void 0);
|
|
5291
|
-
});
|
|
5292
|
-
return c.json(issue, 201);
|
|
5293
|
-
});
|
|
5294
|
-
api.post("/issues/:id/move", async (c) => {
|
|
5295
|
-
const id = c.req.param("id");
|
|
5296
|
-
const body = await c.req.json();
|
|
5297
|
-
if (body.before !== null && typeof body.before !== "string") throw badRequest("before must be an id or null");
|
|
5298
|
-
const issues = await store.update((s) => {
|
|
5299
|
-
const next = moveBefore(s.issues, id, body.before);
|
|
5300
|
-
if (!next) throw notFound("issue not found");
|
|
5301
|
-
s.issues = next;
|
|
5302
|
-
return s.issues;
|
|
5303
|
-
});
|
|
5304
|
-
return c.json({ issues });
|
|
5305
|
-
});
|
|
5306
|
-
api.patch("/issues/:id", async (c) => {
|
|
5307
|
-
const id = c.req.param("id");
|
|
5308
|
-
const body = await c.req.json();
|
|
5309
|
-
const updated = await store.update((s) => {
|
|
5310
|
-
const issue = s.issues.find((i) => i.id === id);
|
|
5311
|
-
if (!issue) throw notFound("issue not found");
|
|
5312
|
-
if (typeof body.title === "string" && body.title.trim()) issue.title = body.title.trim();
|
|
5313
|
-
if (typeof body.body === "string") issue.body = body.body;
|
|
5314
|
-
if (body.status === "open" || body.status === "closed") issue.status = body.status;
|
|
5315
|
-
if (Array.isArray(body.commentIds)) issue.commentIds = [...new Set(body.commentIds.filter((x) => typeof x === "string"))];
|
|
5316
|
-
issue.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5317
|
-
return issue;
|
|
5318
|
-
});
|
|
5319
|
-
return c.json(updated);
|
|
5320
|
-
});
|
|
5321
|
-
api.delete("/issues/:id", async (c) => {
|
|
5322
|
-
const id = c.req.param("id");
|
|
5323
|
-
const removed = await store.update((s) => {
|
|
5324
|
-
const before = s.issues.length;
|
|
5325
|
-
s.issues = s.issues.filter((i) => i.id !== id);
|
|
5326
|
-
return s.issues.length !== before;
|
|
5327
|
-
});
|
|
5328
|
-
if (!removed) throw notFound("issue not found");
|
|
5329
|
-
return c.json({ ok: true });
|
|
5330
|
-
});
|
|
5331
|
-
api.post("/issues/:id/export", async (c) => {
|
|
5332
|
-
const id = c.req.param("id");
|
|
5333
|
-
const res = await store.update((s) => {
|
|
5334
|
-
const issue = s.issues.find((i) => i.id === id);
|
|
5335
|
-
if (!issue) throw notFound("issue not found");
|
|
5336
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5337
|
-
const comments = [];
|
|
5338
|
-
for (const cid of issue.commentIds) {
|
|
5339
|
-
const found = findComment(s, cid);
|
|
5340
|
-
if (!found) continue;
|
|
5341
|
-
const next = { ...found.comment, exportedAt: now, updatedAt: now };
|
|
5342
|
-
if (next.status === "active") next.status = "exported";
|
|
5343
|
-
s.targets[found.targetKey].comments[found.index] = next;
|
|
5344
|
-
comments.push(next);
|
|
5345
|
-
}
|
|
5346
|
-
return {
|
|
5347
|
-
text: formatIssueExport({ repoRoot: repo.root, issue, comments }),
|
|
5348
|
-
count: comments.length,
|
|
5349
|
-
commentIds: comments.map((x) => x.id)
|
|
5350
|
-
};
|
|
5351
|
-
});
|
|
5581
|
+
checkpointHandoff(handed);
|
|
5352
5582
|
return c.json(res);
|
|
5353
5583
|
});
|
|
5354
5584
|
const knownRoot = async (rootParam) => {
|
|
@@ -5372,7 +5602,7 @@ function createApp(opts) {
|
|
|
5372
5602
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5373
5603
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5374
5604
|
const todo = {
|
|
5375
|
-
id:
|
|
5605
|
+
id: randomUUID3(),
|
|
5376
5606
|
branch,
|
|
5377
5607
|
title: body.title.trim(),
|
|
5378
5608
|
body: typeof body.body === "string" ? body.body : "",
|
|
@@ -5381,6 +5611,10 @@ function createApp(opts) {
|
|
|
5381
5611
|
updatedAt: now
|
|
5382
5612
|
};
|
|
5383
5613
|
await store.update((s) => {
|
|
5614
|
+
if (body.commentIds !== void 0) {
|
|
5615
|
+
const ids = linkable(s, body.commentIds);
|
|
5616
|
+
if (ids.length) todo.commentIds = ids;
|
|
5617
|
+
}
|
|
5384
5618
|
s.todos = insertAfter(s.todos, todo, typeof body.after === "string" ? body.after : void 0);
|
|
5385
5619
|
});
|
|
5386
5620
|
return c.json(todo, 201);
|
|
@@ -5407,11 +5641,40 @@ function createApp(opts) {
|
|
|
5407
5641
|
if (typeof body.title === "string" && body.title.trim()) todo.title = body.title.trim();
|
|
5408
5642
|
if (typeof body.body === "string") todo.body = body.body;
|
|
5409
5643
|
if (body.status === "open" || body.status === "done") todo.status = body.status;
|
|
5644
|
+
if (body.commentIds !== void 0) {
|
|
5645
|
+
const ids = linkable(s, body.commentIds);
|
|
5646
|
+
if (ids.length) todo.commentIds = ids;
|
|
5647
|
+
else delete todo.commentIds;
|
|
5648
|
+
}
|
|
5410
5649
|
todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5411
5650
|
return todo;
|
|
5412
5651
|
});
|
|
5413
5652
|
return c.json(updated);
|
|
5414
5653
|
});
|
|
5654
|
+
api.post("/todos/:id/export", async (c) => {
|
|
5655
|
+
const id = c.req.param("id");
|
|
5656
|
+
const handed = [];
|
|
5657
|
+
const res = await store.update((s) => {
|
|
5658
|
+
const todo = s.todos.find((t) => t.id === id);
|
|
5659
|
+
if (!todo) throw notFound("todo not found");
|
|
5660
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5661
|
+
for (const cid of todo.commentIds ?? []) {
|
|
5662
|
+
const found = findComment(s, cid);
|
|
5663
|
+
if (!found) continue;
|
|
5664
|
+
const next = { ...found.comment, exportedAt: now, updatedAt: now };
|
|
5665
|
+
if (next.status === "active") next.status = "exported";
|
|
5666
|
+
s.targets[found.targetKey].comments[found.index] = next;
|
|
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);
|
|
5676
|
+
return c.json(res);
|
|
5677
|
+
});
|
|
5415
5678
|
api.delete("/todos/:id", async (c) => {
|
|
5416
5679
|
const id = c.req.param("id");
|
|
5417
5680
|
const removed = await store.update((s) => {
|
|
@@ -5490,8 +5753,31 @@ function createApp(opts) {
|
|
|
5490
5753
|
return c.json(await tmux.open(repo.commonRoot, wt.path, body.sessionId), 201);
|
|
5491
5754
|
});
|
|
5492
5755
|
api.get("/worktrees", async (c) => {
|
|
5493
|
-
const list = await listWorktreesDetailed(repo);
|
|
5756
|
+
const [list, state] = await Promise.all([listWorktreesDetailed(repo), store.load()]);
|
|
5494
5757
|
const [branches, newSlot] = await Promise.all([listBranches(repo, list), nextSlot(repo, list)]);
|
|
5758
|
+
const review = /* @__PURE__ */ new Map();
|
|
5759
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5760
|
+
for (const t of Object.values(state.targets)) {
|
|
5761
|
+
for (const cm of t.comments) {
|
|
5762
|
+
const at = commentWorktree(cm) ?? repo.root;
|
|
5763
|
+
const r = review.get(at) ?? { toAgent: 0, toReviewer: 0 };
|
|
5764
|
+
const seen = latest.get(at) ?? { agent: "", reviewer: "" };
|
|
5765
|
+
if (awaitsAgent(cm)) {
|
|
5766
|
+
r.toAgent++;
|
|
5767
|
+
if (cm.updatedAt >= seen.agent) [seen.agent, r.toAgentTarget] = [cm.updatedAt, cm.targetKey];
|
|
5768
|
+
}
|
|
5769
|
+
if (awaitsReviewer(cm)) {
|
|
5770
|
+
r.toReviewer++;
|
|
5771
|
+
if (cm.updatedAt >= seen.reviewer) [seen.reviewer, r.toReviewerTarget] = [cm.updatedAt, cm.targetKey];
|
|
5772
|
+
}
|
|
5773
|
+
review.set(at, r);
|
|
5774
|
+
latest.set(at, seen);
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
for (const wt of list) {
|
|
5778
|
+
const r = review.get(wt.path);
|
|
5779
|
+
if (r && (r.toAgent || r.toReviewer)) wt.review = r;
|
|
5780
|
+
}
|
|
5495
5781
|
const res = { worktrees: list, branches, newSlot };
|
|
5496
5782
|
return c.json(res);
|
|
5497
5783
|
});
|
|
@@ -5538,7 +5824,19 @@ function createApp(opts) {
|
|
|
5538
5824
|
const event = { type: "changed", ...change };
|
|
5539
5825
|
void enqueue(() => stream2.writeSSE({ event: "changed", data: JSON.stringify(event) }));
|
|
5540
5826
|
});
|
|
5541
|
-
|
|
5827
|
+
let stamp = await stateStamp();
|
|
5828
|
+
const statePoll = setInterval(() => {
|
|
5829
|
+
void stateStamp().then((next) => {
|
|
5830
|
+
if (next === stamp) return;
|
|
5831
|
+
stamp = next;
|
|
5832
|
+
const event = { type: "state", at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5833
|
+
void enqueue(() => stream2.writeSSE({ event: "state", data: JSON.stringify(event) }));
|
|
5834
|
+
});
|
|
5835
|
+
}, opts.watchIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
5836
|
+
stream2.onAbort(() => {
|
|
5837
|
+
clearInterval(statePoll);
|
|
5838
|
+
release();
|
|
5839
|
+
});
|
|
5542
5840
|
try {
|
|
5543
5841
|
while (!stream2.aborted && !stream2.closed) {
|
|
5544
5842
|
await stream2.sleep(SSE_HEARTBEAT_MS);
|
|
@@ -5546,6 +5844,7 @@ function createApp(opts) {
|
|
|
5546
5844
|
await enqueue(() => stream2.write(": ping\n\n"));
|
|
5547
5845
|
}
|
|
5548
5846
|
} finally {
|
|
5847
|
+
clearInterval(statePoll);
|
|
5549
5848
|
release();
|
|
5550
5849
|
}
|
|
5551
5850
|
});
|
|
@@ -5574,7 +5873,7 @@ function createApp(opts) {
|
|
|
5574
5873
|
api.post("/nvim/open", async (c) => {
|
|
5575
5874
|
const body = await c.req.json();
|
|
5576
5875
|
if (!body.socket || !body.absPath) throw badRequest("socket and absPath are required");
|
|
5577
|
-
if (!
|
|
5876
|
+
if (!path9.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
|
|
5578
5877
|
const scan = await nvim.scan();
|
|
5579
5878
|
if (!scan.instances.some((i) => i.socket === body.socket)) {
|
|
5580
5879
|
const fresh = await nvim.scan(true);
|
|
@@ -5634,14 +5933,14 @@ function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
|
5634
5933
|
}
|
|
5635
5934
|
|
|
5636
5935
|
// packages/server/src/update.ts
|
|
5637
|
-
import
|
|
5638
|
-
import { mkdir as
|
|
5936
|
+
import path10 from "path";
|
|
5937
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
5639
5938
|
var PACKAGE = "@xbghc/warden";
|
|
5640
5939
|
var LATEST_URL = "https://registry.npmjs.org/@xbghc%2fwarden/latest";
|
|
5641
5940
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
5642
5941
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
5643
5942
|
function updateCacheFile(baseDir = dataDir()) {
|
|
5644
|
-
return
|
|
5943
|
+
return path10.join(baseDir, "update-check.json");
|
|
5645
5944
|
}
|
|
5646
5945
|
function updateCheckEnabled(env = process.env) {
|
|
5647
5946
|
return !env.WARDEN_NO_UPDATE_CHECK && !env.NO_UPDATE_NOTIFIER && !env.CI;
|
|
@@ -5675,7 +5974,7 @@ async function readCache(file, now) {
|
|
|
5675
5974
|
}
|
|
5676
5975
|
async function writeCache(file, cache) {
|
|
5677
5976
|
try {
|
|
5678
|
-
await
|
|
5977
|
+
await mkdir3(path10.dirname(file), { recursive: true });
|
|
5679
5978
|
await writeFile2(file, JSON.stringify(cache));
|
|
5680
5979
|
} catch {
|
|
5681
5980
|
}
|
|
@@ -5720,8 +6019,8 @@ async function startServer(opts) {
|
|
|
5720
6019
|
const repo = await resolveRepo(opts.repoPath);
|
|
5721
6020
|
const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
|
|
5722
6021
|
const store = new StateStore(stateFile, repo.commonRoot);
|
|
5723
|
-
const instanceToken =
|
|
5724
|
-
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update });
|
|
6022
|
+
const instanceToken = randomUUID4();
|
|
6023
|
+
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update, replyCommand: opts.replyCommand });
|
|
5725
6024
|
const listen = (p) => new Promise((resolve, reject) => {
|
|
5726
6025
|
const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
|
|
5727
6026
|
s.once("error", reject);
|
|
@@ -5763,8 +6062,68 @@ async function startServer(opts) {
|
|
|
5763
6062
|
};
|
|
5764
6063
|
}
|
|
5765
6064
|
|
|
6065
|
+
// bin/agent.ts
|
|
6066
|
+
var AGENT_COMMANDS = ["feedback", "reply"];
|
|
6067
|
+
function agentUsage(cmd) {
|
|
6068
|
+
return `Commands for the agent, run inside the worktree under review:
|
|
6069
|
+
${cmd} feedback [--peek] Print the review comments not yet handed over, and mark them handed
|
|
6070
|
+
over (--peek leaves them as they are)
|
|
6071
|
+
${cmd} reply <id> <message> Answer a comment; <id> is the one in its heading. A message of "-",
|
|
6072
|
+
or none with stdin piped, is read from stdin
|
|
6073
|
+
`;
|
|
6074
|
+
}
|
|
6075
|
+
async function readStdin() {
|
|
6076
|
+
const chunks = [];
|
|
6077
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6078
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
6079
|
+
}
|
|
6080
|
+
async function openStore() {
|
|
6081
|
+
const repo = await resolveRepo(process.cwd());
|
|
6082
|
+
return { store: new StateStore(stateFilePath(repo.commonRoot), repo.commonRoot), root: repo.root, commonRoot: repo.commonRoot };
|
|
6083
|
+
}
|
|
6084
|
+
async function feedback(args, replyCommand) {
|
|
6085
|
+
const unknown = args.find((a) => a !== "--peek");
|
|
6086
|
+
if (unknown) throw new Error(`unknown argument: ${unknown}`);
|
|
6087
|
+
const { store, root, commonRoot } = await openStore();
|
|
6088
|
+
const peek = args.includes("--peek");
|
|
6089
|
+
const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek });
|
|
6090
|
+
if (comments.length === 0) {
|
|
6091
|
+
console.log("No review comments are waiting for you.");
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
if (!peek) {
|
|
6095
|
+
await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
|
|
6096
|
+
console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
|
|
6097
|
+
});
|
|
6098
|
+
}
|
|
6099
|
+
process.stdout.write(formatCommentsExport({ repoRoot: root, comments, replyCommand }));
|
|
6100
|
+
}
|
|
6101
|
+
async function reply(args) {
|
|
6102
|
+
const [id, ...words] = args;
|
|
6103
|
+
if (!id) throw new Error("reply needs a comment id");
|
|
6104
|
+
let body = words.join(" ");
|
|
6105
|
+
if (body === "-" || !body && !process.stdin.isTTY) body = await readStdin();
|
|
6106
|
+
if (!body.trim()) throw new Error("reply needs a message");
|
|
6107
|
+
const { store } = await openStore();
|
|
6108
|
+
const c = await addReply(store, id, "agent", body);
|
|
6109
|
+
const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
|
|
6110
|
+
console.log(`Replied to ${shortId(c.id)} (${c.filePath}:${range}).`);
|
|
6111
|
+
}
|
|
6112
|
+
async function runAgentCommand(argv, replyCommand) {
|
|
6113
|
+
const [cmd, ...rest] = argv;
|
|
6114
|
+
if (!cmd || !AGENT_COMMANDS.includes(cmd)) return void 0;
|
|
6115
|
+
try {
|
|
6116
|
+
if (cmd === "feedback") await feedback(rest, replyCommand);
|
|
6117
|
+
else await reply(rest);
|
|
6118
|
+
return 0;
|
|
6119
|
+
} catch (e) {
|
|
6120
|
+
console.error(`warden ${cmd}: ${e instanceof HttpError || e instanceof Error ? e.message : String(e)}`);
|
|
6121
|
+
return 1;
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
|
|
5766
6125
|
// bin/cli.ts
|
|
5767
|
-
var VERSION = true ? "0.
|
|
6126
|
+
var VERSION = true ? "0.16.0" : "dev";
|
|
5768
6127
|
function parseArgs(argv) {
|
|
5769
6128
|
const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
|
|
5770
6129
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -5783,14 +6142,17 @@ function parseArgs(argv) {
|
|
|
5783
6142
|
} else if (a === "--help" || a === "-h") args.help = true;
|
|
5784
6143
|
else if (a === "--version" || a === "-v") args.version = true;
|
|
5785
6144
|
else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
|
|
5786
|
-
else args.repoPath =
|
|
6145
|
+
else args.repoPath = path11.resolve(a);
|
|
5787
6146
|
}
|
|
5788
6147
|
return args;
|
|
5789
6148
|
}
|
|
6149
|
+
var viaNpx = fileURLToPath(import.meta.url).includes(`${path11.sep}_npx${path11.sep}`);
|
|
6150
|
+
var selfCommand = viaNpx ? "npx @xbghc/warden" : "warden";
|
|
5790
6151
|
function usage() {
|
|
5791
6152
|
return `warden \u2014 local git diff review UI
|
|
5792
6153
|
|
|
5793
6154
|
Usage: warden [repoPath] [options]
|
|
6155
|
+
warden <command> [args]
|
|
5794
6156
|
|
|
5795
6157
|
Options:
|
|
5796
6158
|
--port <n>, -p <n> Listen on a fixed port (default: first free port from 4100, and under WSL
|
|
@@ -5800,6 +6162,9 @@ Options:
|
|
|
5800
6162
|
(also: WARDEN_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1, CI)
|
|
5801
6163
|
-h, --help Show this help
|
|
5802
6164
|
-v, --version Print version
|
|
6165
|
+
|
|
6166
|
+
${agentUsage("warden")}
|
|
6167
|
+
A repository in a directory named after a command is given as ./<name>.
|
|
5803
6168
|
`;
|
|
5804
6169
|
}
|
|
5805
6170
|
async function openBrowser(url) {
|
|
@@ -5828,20 +6193,22 @@ async function openBrowser(url) {
|
|
|
5828
6193
|
return false;
|
|
5829
6194
|
}
|
|
5830
6195
|
function resolveWebDir() {
|
|
5831
|
-
const here =
|
|
6196
|
+
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
5832
6197
|
const candidates = [
|
|
5833
6198
|
process.env.WARDEN_WEB_DIR,
|
|
5834
|
-
|
|
6199
|
+
path11.join(here, "web"),
|
|
5835
6200
|
// dist/cli.js -> dist/web
|
|
5836
|
-
|
|
6201
|
+
path11.join(here, "..", "dist", "web")
|
|
5837
6202
|
// bin/cli.ts (tsx dev) -> dist/web
|
|
5838
6203
|
].filter((p) => !!p);
|
|
5839
6204
|
for (const dir of candidates) {
|
|
5840
|
-
if (existsSync2(
|
|
6205
|
+
if (existsSync2(path11.join(dir, "index.html"))) return dir;
|
|
5841
6206
|
}
|
|
5842
6207
|
return void 0;
|
|
5843
6208
|
}
|
|
5844
6209
|
async function main() {
|
|
6210
|
+
const code = await runAgentCommand(process.argv.slice(2), selfCommand);
|
|
6211
|
+
if (code !== void 0) process.exit(code);
|
|
5845
6212
|
let args;
|
|
5846
6213
|
try {
|
|
5847
6214
|
args = parseArgs(process.argv.slice(2));
|
|
@@ -5862,12 +6229,11 @@ async function main() {
|
|
|
5862
6229
|
if (!webDir) {
|
|
5863
6230
|
console.error("warning: built web assets not found (dist/web). Only the /api endpoints will be served.");
|
|
5864
6231
|
}
|
|
5865
|
-
const viaNpx = fileURLToPath(import.meta.url).includes(`${path10.sep}_npx${path10.sep}`);
|
|
5866
6232
|
const checking = args.updateCheck && VERSION !== "dev" && updateCheckEnabled();
|
|
5867
6233
|
const update = checking ? checkForUpdate({ current: VERSION, npx: viaNpx }) : Promise.resolve(null);
|
|
5868
6234
|
let server;
|
|
5869
6235
|
try {
|
|
5870
|
-
server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update });
|
|
6236
|
+
server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update, replyCommand: selfCommand });
|
|
5871
6237
|
} catch (e) {
|
|
5872
6238
|
console.error(`warden: ${e.message}`);
|
|
5873
6239
|
process.exit(1);
|