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