weifuwu 0.24.2 → 0.25.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 +225 -0
- package/dist/compile.d.ts +7 -12
- package/dist/iii/client.d.ts +1 -1
- package/dist/iii/register-worker.d.ts +0 -1
- package/dist/iii/types.d.ts +19 -33
- package/dist/iii/ws.d.ts +0 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1347 -765
- package/dist/mcp.d.ts +34 -0
- package/dist/module-server.d.ts +10 -0
- package/dist/notifier/client.d.ts +2 -0
- package/dist/notifier/index.d.ts +2 -0
- package/dist/notifier/types.d.ts +105 -0
- package/dist/router.d.ts +30 -0
- package/dist/server-registry.d.ts +12 -0
- package/dist/stream.d.ts +0 -5
- package/dist/test-utils.d.ts +80 -2
- package/dist/user/types.d.ts +21 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -65,7 +65,8 @@ function isBundled() {
|
|
|
65
65
|
return true ? true : false;
|
|
66
66
|
}
|
|
67
67
|
function isDev() {
|
|
68
|
-
|
|
68
|
+
const env2 = process.env.NODE_ENV;
|
|
69
|
+
return env2 !== "production" && env2 !== "test";
|
|
69
70
|
}
|
|
70
71
|
function isProd() {
|
|
71
72
|
return process.env.NODE_ENV === "production";
|
|
@@ -293,20 +294,20 @@ function serve(handler, options) {
|
|
|
293
294
|
process.off("SIGINT", shutdownHandler);
|
|
294
295
|
shutdownHandler = null;
|
|
295
296
|
}
|
|
296
|
-
return new Promise((
|
|
297
|
+
return new Promise((resolve16) => {
|
|
297
298
|
if (!server.listening) {
|
|
298
|
-
|
|
299
|
+
resolve16();
|
|
299
300
|
return;
|
|
300
301
|
}
|
|
301
302
|
server.close();
|
|
302
303
|
server.closeIdleConnections();
|
|
303
304
|
const timer = setTimeout(() => {
|
|
304
305
|
server.closeAllConnections();
|
|
305
|
-
|
|
306
|
+
resolve16();
|
|
306
307
|
}, timeoutMs);
|
|
307
308
|
server.on("close", () => {
|
|
308
309
|
clearTimeout(timer);
|
|
309
|
-
|
|
310
|
+
resolve16();
|
|
310
311
|
});
|
|
311
312
|
});
|
|
312
313
|
},
|
|
@@ -466,6 +467,8 @@ var Router = class _Router {
|
|
|
466
467
|
_hasWildcard = false;
|
|
467
468
|
_hub;
|
|
468
469
|
_wss;
|
|
470
|
+
/** Track which ctx fields have been injected so far (for dependency checking). */
|
|
471
|
+
_ctxFields = /* @__PURE__ */ new Set();
|
|
469
472
|
get wss() {
|
|
470
473
|
if (!this._wss) this._wss = new WebSocketServer({ noServer: true });
|
|
471
474
|
return this._wss;
|
|
@@ -489,17 +492,47 @@ var Router = class _Router {
|
|
|
489
492
|
node = getOrCreateChild(node, segment, createTrieNode, false);
|
|
490
493
|
}
|
|
491
494
|
node.pathMws.push(arg2);
|
|
495
|
+
this._checkMiddlewareMeta(arg2, `${arg1}`);
|
|
492
496
|
}
|
|
493
497
|
} else if (typeof arg1 === "function") {
|
|
494
498
|
this.globalMws.push(arg1);
|
|
499
|
+
this._checkMiddlewareMeta(arg1, "global");
|
|
495
500
|
} else if (typeof arg1 === "object" && arg1 !== null && "middleware" in arg1 && // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
496
501
|
typeof arg1.middleware === "function" && arg1 instanceof _Router) {
|
|
497
502
|
const mod = arg1;
|
|
498
|
-
|
|
503
|
+
const mw = mod.middleware();
|
|
504
|
+
this.globalMws.push(mw);
|
|
505
|
+
this._checkMiddlewareMeta(mw, "global (auto-registered)");
|
|
499
506
|
this._mountRouter("/", mod);
|
|
500
507
|
}
|
|
501
508
|
return this;
|
|
502
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Check a middleware's dependency metadata and emit warnings if
|
|
512
|
+
* required fields haven't been injected yet.
|
|
513
|
+
* Attach __meta to a middleware function:
|
|
514
|
+
*
|
|
515
|
+
* ```ts
|
|
516
|
+
* mw.__meta = { injects: ['sql'], depends: ['session'] }
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
_checkMiddlewareMeta(mw, location) {
|
|
520
|
+
const meta = mw.__meta ?? mw.middleware?.().__meta;
|
|
521
|
+
if (!meta) return;
|
|
522
|
+
for (const dep of meta.depends) {
|
|
523
|
+
if (!this._ctxFields.has(dep)) {
|
|
524
|
+
console.warn(
|
|
525
|
+
`[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.
|
|
526
|
+
Register the provider before this middleware:
|
|
527
|
+
app.use(${dep}()) // add before this middleware
|
|
528
|
+
Current ctx fields: [${[...this._ctxFields].join(", ")}]`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const field of meta.injects) {
|
|
533
|
+
this._ctxFields.add(field);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
503
536
|
// Route registration — returns Router<T> unchanged.
|
|
504
537
|
// Route-level middleware and handlers get Context<T>.
|
|
505
538
|
get(path2, ...args) {
|
|
@@ -1487,7 +1520,7 @@ function upload(options) {
|
|
|
1487
1520
|
}
|
|
1488
1521
|
|
|
1489
1522
|
// rate-limit.ts
|
|
1490
|
-
function defaultKey(_req,
|
|
1523
|
+
function defaultKey(_req, _ctx2) {
|
|
1491
1524
|
const forwarded = _req.headers.get("x-forwarded-for");
|
|
1492
1525
|
if (forwarded) return forwarded.split(",")[0].trim();
|
|
1493
1526
|
const realIp = _req.headers.get("x-real-ip");
|
|
@@ -1567,6 +1600,7 @@ function rateLimit(options) {
|
|
|
1567
1600
|
const res = await next(req, ctx);
|
|
1568
1601
|
return addRateLimitHeaders(res, max, remaining, reset);
|
|
1569
1602
|
};
|
|
1603
|
+
mw.__meta = { injects: [], depends: [] };
|
|
1570
1604
|
mw.close = () => {
|
|
1571
1605
|
if (interval) clearInterval(interval);
|
|
1572
1606
|
hits.clear();
|
|
@@ -1750,6 +1784,7 @@ function createSSEStream(iterable, opts) {
|
|
|
1750
1784
|
}
|
|
1751
1785
|
|
|
1752
1786
|
// test-utils.ts
|
|
1787
|
+
import { WebSocket as WSWebSocket } from "ws";
|
|
1753
1788
|
var TestResponseImpl = class {
|
|
1754
1789
|
response;
|
|
1755
1790
|
constructor(response) {
|
|
@@ -1845,9 +1880,22 @@ var TestRequest = class {
|
|
|
1845
1880
|
};
|
|
1846
1881
|
var TestApp = class {
|
|
1847
1882
|
router;
|
|
1883
|
+
wsServer = null;
|
|
1884
|
+
wsConnections = [];
|
|
1848
1885
|
constructor() {
|
|
1849
1886
|
this.router = new Router();
|
|
1850
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Register a WebSocket handler.
|
|
1890
|
+
*/
|
|
1891
|
+
ws(path2, handler) {
|
|
1892
|
+
this.router.ws(path2, handler);
|
|
1893
|
+
return this;
|
|
1894
|
+
}
|
|
1895
|
+
/** Get the raw Router (for advanced use). */
|
|
1896
|
+
get _router() {
|
|
1897
|
+
return this.router;
|
|
1898
|
+
}
|
|
1851
1899
|
/** Add global middleware */
|
|
1852
1900
|
use(mw) {
|
|
1853
1901
|
this.router.use(mw);
|
|
@@ -1907,6 +1955,195 @@ var TestApp = class {
|
|
|
1907
1955
|
handler() {
|
|
1908
1956
|
return this.router.handler();
|
|
1909
1957
|
}
|
|
1958
|
+
/** Start building a WebSocket connection to the given path. */
|
|
1959
|
+
wsReq(path2) {
|
|
1960
|
+
return new TestWSRequest(this, path2);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Internal: ensure HTTP server is running for WebSocket connections.
|
|
1964
|
+
* Starts on a random port.
|
|
1965
|
+
*/
|
|
1966
|
+
/* @internal */
|
|
1967
|
+
async _ensureServer() {
|
|
1968
|
+
if (this.wsServer) {
|
|
1969
|
+
return `http://localhost:${this.wsServer.port}`;
|
|
1970
|
+
}
|
|
1971
|
+
const wsHandler = this.router.websocketHandler();
|
|
1972
|
+
if (!wsHandler) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
"No WebSocket routes registered. Use app.ws(path, handler) before calling wsReq()."
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
this.wsServer = serve(this.router.handler(), {
|
|
1978
|
+
websocket: wsHandler
|
|
1979
|
+
});
|
|
1980
|
+
await this.wsServer.ready;
|
|
1981
|
+
return `http://localhost:${this.wsServer.port}`;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Internal: register a WS connection for cleanup.
|
|
1985
|
+
*/
|
|
1986
|
+
/* @internal */
|
|
1987
|
+
_trackConnection(conn) {
|
|
1988
|
+
this.wsConnections.push(conn);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Cleanup all WebSocket connections and stop the server.
|
|
1992
|
+
*/
|
|
1993
|
+
async close() {
|
|
1994
|
+
for (const conn of this.wsConnections) {
|
|
1995
|
+
try {
|
|
1996
|
+
conn.close();
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.wsConnections = [];
|
|
2001
|
+
if (this.wsServer) {
|
|
2002
|
+
this.wsServer.stop();
|
|
2003
|
+
this.wsServer = null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
var TestWSRequest = class {
|
|
2008
|
+
app;
|
|
2009
|
+
path;
|
|
2010
|
+
_timeout = 5e3;
|
|
2011
|
+
constructor(app, path2) {
|
|
2012
|
+
this.app = app;
|
|
2013
|
+
this.path = path2;
|
|
2014
|
+
}
|
|
2015
|
+
/** Set the timeout for operations (default: 5000ms). */
|
|
2016
|
+
timeout(ms) {
|
|
2017
|
+
this._timeout = ms;
|
|
2018
|
+
return this;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Connect to the WebSocket endpoint.
|
|
2022
|
+
* Starts a real HTTP server (random port) if not already running.
|
|
2023
|
+
*/
|
|
2024
|
+
async connect() {
|
|
2025
|
+
const baseUrl = await this.app._ensureServer();
|
|
2026
|
+
const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
|
|
2027
|
+
const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
|
|
2028
|
+
return new Promise((resolve16, reject) => {
|
|
2029
|
+
const timer = setTimeout(() => {
|
|
2030
|
+
reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
|
|
2031
|
+
ws.close();
|
|
2032
|
+
}, this._timeout);
|
|
2033
|
+
ws.on("open", () => {
|
|
2034
|
+
clearTimeout(timer);
|
|
2035
|
+
const conn = new TestWSConnection(ws, this._timeout);
|
|
2036
|
+
this.app._trackConnection(conn);
|
|
2037
|
+
resolve16(conn);
|
|
2038
|
+
});
|
|
2039
|
+
ws.on("error", (err) => {
|
|
2040
|
+
clearTimeout(timer);
|
|
2041
|
+
reject(new Error(`WebSocket connection error: ${err.message}`));
|
|
2042
|
+
});
|
|
2043
|
+
ws.on("unexpected-response", (_req, res) => {
|
|
2044
|
+
clearTimeout(timer);
|
|
2045
|
+
let body = "";
|
|
2046
|
+
res.on("data", (chunk) => {
|
|
2047
|
+
body += chunk.toString();
|
|
2048
|
+
});
|
|
2049
|
+
res.on("end", () => {
|
|
2050
|
+
reject(new Error(`WebSocket upgrade rejected (${res.statusCode}): ${body.slice(0, 200)}`));
|
|
2051
|
+
});
|
|
2052
|
+
});
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
var TestWSConnection = class {
|
|
2057
|
+
ws;
|
|
2058
|
+
_timeout;
|
|
2059
|
+
messageQueue = [];
|
|
2060
|
+
resolveQueue = [];
|
|
2061
|
+
_closed = false;
|
|
2062
|
+
constructor(ws, timeout = 5e3) {
|
|
2063
|
+
this.ws = ws;
|
|
2064
|
+
this._timeout = timeout;
|
|
2065
|
+
ws.on("message", (data) => {
|
|
2066
|
+
const str = data.toString();
|
|
2067
|
+
if (this.resolveQueue.length > 0) {
|
|
2068
|
+
const resolve16 = this.resolveQueue.shift();
|
|
2069
|
+
resolve16(str);
|
|
2070
|
+
} else {
|
|
2071
|
+
this.messageQueue.push(str);
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
ws.on("close", () => {
|
|
2075
|
+
this._closed = true;
|
|
2076
|
+
for (const _r of this.resolveQueue) {
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
/** Send a text message. */
|
|
2081
|
+
send(data) {
|
|
2082
|
+
this.ws.send(data);
|
|
2083
|
+
}
|
|
2084
|
+
/** Send a JSON message. */
|
|
2085
|
+
json(data) {
|
|
2086
|
+
this.ws.send(JSON.stringify(data));
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Wait for the next message. Returns the raw text.
|
|
2090
|
+
* Throws on timeout or if the connection is closed.
|
|
2091
|
+
*/
|
|
2092
|
+
async receive(timeout) {
|
|
2093
|
+
if (this.messageQueue.length > 0) {
|
|
2094
|
+
return this.messageQueue.shift();
|
|
2095
|
+
}
|
|
2096
|
+
if (this._closed) {
|
|
2097
|
+
throw new Error("WebSocket connection closed");
|
|
2098
|
+
}
|
|
2099
|
+
return new Promise((resolve16, reject) => {
|
|
2100
|
+
const timer = setTimeout(() => {
|
|
2101
|
+
const idx = this.resolveQueue.indexOf(resolve16);
|
|
2102
|
+
if (idx !== -1) this.resolveQueue.splice(idx, 1);
|
|
2103
|
+
reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
|
|
2104
|
+
}, timeout ?? this._timeout);
|
|
2105
|
+
this.resolveQueue.push((msg) => {
|
|
2106
|
+
clearTimeout(timer);
|
|
2107
|
+
resolve16(msg);
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
/** Wait for the next message and parse as JSON. */
|
|
2112
|
+
async receiveJson() {
|
|
2113
|
+
const msg = await this.receive();
|
|
2114
|
+
return JSON.parse(msg);
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Assert that no message is received within the given silence period.
|
|
2118
|
+
* Useful for verifying that something did NOT happen.
|
|
2119
|
+
*/
|
|
2120
|
+
async expectSilent(ms) {
|
|
2121
|
+
return new Promise((resolve16, reject) => {
|
|
2122
|
+
if (this.messageQueue.length > 0) {
|
|
2123
|
+
reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const timer = setTimeout(() => resolve16(), ms);
|
|
2127
|
+
const origPush = this.resolveQueue.push.bind(this.resolveQueue);
|
|
2128
|
+
this.resolveQueue.push = (_fn) => {
|
|
2129
|
+
clearTimeout(timer);
|
|
2130
|
+
reject(new Error("Expected silence but received a message"));
|
|
2131
|
+
return 0;
|
|
2132
|
+
};
|
|
2133
|
+
setTimeout(() => {
|
|
2134
|
+
this.resolveQueue.push = origPush;
|
|
2135
|
+
}, ms + 10).unref();
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/** Close the connection. */
|
|
2139
|
+
close() {
|
|
2140
|
+
this._closed = true;
|
|
2141
|
+
this.ws.close();
|
|
2142
|
+
}
|
|
2143
|
+
/** Whether the connection is closed. */
|
|
2144
|
+
get closed() {
|
|
2145
|
+
return this._closed;
|
|
2146
|
+
}
|
|
1910
2147
|
};
|
|
1911
2148
|
function testApp() {
|
|
1912
2149
|
return new TestApp();
|
|
@@ -2506,6 +2743,7 @@ function aiProvider(options) {
|
|
|
2506
2743
|
ctx.ai = provider;
|
|
2507
2744
|
return next(req, ctx);
|
|
2508
2745
|
};
|
|
2746
|
+
mw.__meta = { injects: ["ai"], depends: [] };
|
|
2509
2747
|
return Object.assign(mw, provider);
|
|
2510
2748
|
}
|
|
2511
2749
|
|
|
@@ -3115,6 +3353,7 @@ function postgres(opts) {
|
|
|
3115
3353
|
ctx.sql = sql2;
|
|
3116
3354
|
return next(req, ctx);
|
|
3117
3355
|
});
|
|
3356
|
+
mw.__meta = { injects: ["sql"], depends: [] };
|
|
3118
3357
|
mw.sql = sql2;
|
|
3119
3358
|
mw.table = ((tableOrSchema, builders) => {
|
|
3120
3359
|
if (typeof tableOrSchema === "string") {
|
|
@@ -3191,7 +3430,7 @@ var PgModule = class {
|
|
|
3191
3430
|
};
|
|
3192
3431
|
|
|
3193
3432
|
// user/client.ts
|
|
3194
|
-
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
3433
|
+
import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
|
|
3195
3434
|
import jwt2 from "jsonwebtoken";
|
|
3196
3435
|
import { z as z2 } from "zod";
|
|
3197
3436
|
|
|
@@ -3317,7 +3556,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3317
3556
|
{ status: 400, headers: { "Content-Type": "text/html; charset=utf-8" } }
|
|
3318
3557
|
);
|
|
3319
3558
|
}
|
|
3320
|
-
async function authorizeHandler(req,
|
|
3559
|
+
async function authorizeHandler(req, _ctx2) {
|
|
3321
3560
|
const url = new URL(req.url);
|
|
3322
3561
|
const clientId = url.searchParams.get("client_id") || "";
|
|
3323
3562
|
const redirectUri = url.searchParams.get("redirect_uri") || "";
|
|
@@ -3738,6 +3977,10 @@ var LoginSchema = z2.object({
|
|
|
3738
3977
|
email: z2.string().email(),
|
|
3739
3978
|
password: z2.string().min(1)
|
|
3740
3979
|
});
|
|
3980
|
+
var CreateApiKeySchema = z2.object({
|
|
3981
|
+
name: z2.string().min(1),
|
|
3982
|
+
scopes: z2.array(z2.string()).optional()
|
|
3983
|
+
});
|
|
3741
3984
|
function escapeIdent2(s) {
|
|
3742
3985
|
return `"${s.replace(/"/g, '""')}"`;
|
|
3743
3986
|
}
|
|
@@ -3784,6 +4027,7 @@ function user(options) {
|
|
|
3784
4027
|
const secret = options.jwtSecret;
|
|
3785
4028
|
const expiresIn = options.expiresIn ?? "24h";
|
|
3786
4029
|
const oauth2Enabled = options.oauth2?.server ?? false;
|
|
4030
|
+
const apiKeysEnabled = options.apiKeys ?? false;
|
|
3787
4031
|
const base = hasDb ? new PgModule(pg) : null;
|
|
3788
4032
|
const users = hasDb ? pg.table(table, {
|
|
3789
4033
|
id: serial("id").primaryKey(),
|
|
@@ -3821,6 +4065,28 @@ function user(options) {
|
|
|
3821
4065
|
ON "_auth_providers"(user_id)
|
|
3822
4066
|
`);
|
|
3823
4067
|
}
|
|
4068
|
+
if (apiKeysEnabled) {
|
|
4069
|
+
await _pg.sql.unsafe(`
|
|
4070
|
+
CREATE TABLE IF NOT EXISTS "_api_keys" (
|
|
4071
|
+
id SERIAL PRIMARY KEY,
|
|
4072
|
+
user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
|
|
4073
|
+
name TEXT NOT NULL,
|
|
4074
|
+
key_prefix TEXT NOT NULL,
|
|
4075
|
+
key_hash TEXT NOT NULL,
|
|
4076
|
+
scopes TEXT[] DEFAULT '{}',
|
|
4077
|
+
last_used_at TIMESTAMPTZ,
|
|
4078
|
+
expires_at TIMESTAMPTZ,
|
|
4079
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
4080
|
+
revoked BOOLEAN DEFAULT false
|
|
4081
|
+
)
|
|
4082
|
+
`);
|
|
4083
|
+
await _pg.sql.unsafe(`
|
|
4084
|
+
CREATE INDEX IF NOT EXISTS "_api_keys_user_idx" ON "_api_keys"(user_id)
|
|
4085
|
+
`);
|
|
4086
|
+
await _pg.sql.unsafe(`
|
|
4087
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "_api_keys_hash_idx" ON "_api_keys"(key_hash)
|
|
4088
|
+
`);
|
|
4089
|
+
}
|
|
3824
4090
|
if (!oauth2Enabled) return;
|
|
3825
4091
|
const clients3 = _pg.table("_oauth2_clients", {
|
|
3826
4092
|
id: serial("id").primaryKey(),
|
|
@@ -3921,8 +4187,79 @@ function user(options) {
|
|
|
3921
4187
|
return null;
|
|
3922
4188
|
}
|
|
3923
4189
|
}
|
|
4190
|
+
function hashApiKey(key) {
|
|
4191
|
+
return createHash("sha256").update(key).digest("hex");
|
|
4192
|
+
}
|
|
4193
|
+
function generateApiKey() {
|
|
4194
|
+
const random = randomBytes(32).toString("hex");
|
|
4195
|
+
return `sk_live_${random}`;
|
|
4196
|
+
}
|
|
4197
|
+
async function createApiKey(userId2, name, scopes) {
|
|
4198
|
+
if (!hasDb) throw new Error("user(): pg required for API key management");
|
|
4199
|
+
const key = generateApiKey();
|
|
4200
|
+
const keyHash = hashApiKey(key);
|
|
4201
|
+
const prefix = key.slice(0, 12) + "..." + key.slice(-4);
|
|
4202
|
+
const [row] = await _pg.sql.unsafe(
|
|
4203
|
+
`INSERT INTO "_api_keys" (user_id, name, key_prefix, key_hash, scopes)
|
|
4204
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
|
4205
|
+
[userId2, name, prefix, keyHash, scopes ?? []]
|
|
4206
|
+
);
|
|
4207
|
+
return { id: row.id, key };
|
|
4208
|
+
}
|
|
4209
|
+
async function listApiKeys(userId2) {
|
|
4210
|
+
if (!hasDb) return [];
|
|
4211
|
+
const rows = await _pg.sql.unsafe(
|
|
4212
|
+
`SELECT id, name, key_prefix, scopes, last_used_at, created_at, revoked
|
|
4213
|
+
FROM "_api_keys" WHERE user_id = $1 ORDER BY created_at DESC`,
|
|
4214
|
+
[userId2]
|
|
4215
|
+
);
|
|
4216
|
+
return rows.map((r2) => ({
|
|
4217
|
+
id: r2.id,
|
|
4218
|
+
name: r2.name,
|
|
4219
|
+
prefix: r2.key_prefix,
|
|
4220
|
+
scopes: Array.isArray(r2.scopes) ? r2.scopes : [],
|
|
4221
|
+
last_used_at: r2.last_used_at ? new Date(r2.last_used_at).toISOString() : null,
|
|
4222
|
+
created_at: new Date(r2.created_at).toISOString(),
|
|
4223
|
+
revoked: !!r2.revoked
|
|
4224
|
+
}));
|
|
4225
|
+
}
|
|
4226
|
+
async function revokeApiKey(userId2, keyId) {
|
|
4227
|
+
if (!hasDb) throw new Error("user(): pg required for API key management");
|
|
4228
|
+
await _pg.sql.unsafe(`UPDATE "_api_keys" SET revoked = true WHERE id = $1 AND user_id = $2`, [
|
|
4229
|
+
keyId,
|
|
4230
|
+
userId2
|
|
4231
|
+
]);
|
|
4232
|
+
}
|
|
4233
|
+
async function verifyApiKey(key) {
|
|
4234
|
+
if (!hasDb || !apiKeysEnabled) return null;
|
|
4235
|
+
const keyHash = hashApiKey(key);
|
|
4236
|
+
const [row] = await _pg.sql.unsafe(
|
|
4237
|
+
`SELECT id, user_id, scopes, revoked, expires_at
|
|
4238
|
+
FROM "_api_keys" WHERE key_hash = $1 LIMIT 1`,
|
|
4239
|
+
[keyHash]
|
|
4240
|
+
);
|
|
4241
|
+
if (!row) return null;
|
|
4242
|
+
if (row.revoked) return null;
|
|
4243
|
+
if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) return null;
|
|
4244
|
+
await _pg.sql.unsafe(
|
|
4245
|
+
`UPDATE "_api_keys" SET last_used_at = NOW() WHERE id = $1 AND last_used_at IS NULL OR last_used_at < NOW() - interval '1 minute'`,
|
|
4246
|
+
[row.id]
|
|
4247
|
+
).catch(() => {
|
|
4248
|
+
});
|
|
4249
|
+
return {
|
|
4250
|
+
userId: row.user_id,
|
|
4251
|
+
scopes: Array.isArray(row.scopes) ? row.scopes : []
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
async function tryApiKeyAuth(token) {
|
|
4255
|
+
if (!apiKeysEnabled || !hasDb) return null;
|
|
4256
|
+
if (!token.startsWith("sk_")) return null;
|
|
4257
|
+
return verifyApiKey(token);
|
|
4258
|
+
}
|
|
3924
4259
|
const headerName = options.header ?? "Authorization";
|
|
3925
4260
|
async function resolveUser(req, ctx) {
|
|
4261
|
+
const _ctx2 = ctx;
|
|
4262
|
+
if (_ctx2.user) return _ctx2.user;
|
|
3926
4263
|
const s = ctx;
|
|
3927
4264
|
const sessionUserId = s.session?.userId;
|
|
3928
4265
|
if (sessionUserId !== void 0 && sessionUserId !== null) {
|
|
@@ -3998,6 +4335,17 @@ function user(options) {
|
|
|
3998
4335
|
return null;
|
|
3999
4336
|
}
|
|
4000
4337
|
}
|
|
4338
|
+
if (token.startsWith("sk_")) {
|
|
4339
|
+
const result = await tryApiKeyAuth(token);
|
|
4340
|
+
if (result) {
|
|
4341
|
+
if (hasDb) {
|
|
4342
|
+
const row = await findById(result.userId);
|
|
4343
|
+
if (row) return { ...stripPassword(row), _apiKeyScopes: result.scopes };
|
|
4344
|
+
}
|
|
4345
|
+
return { id: result.userId, _apiKeyScopes: result.scopes };
|
|
4346
|
+
}
|
|
4347
|
+
return null;
|
|
4348
|
+
}
|
|
4001
4349
|
if (secret && hasDb) {
|
|
4002
4350
|
try {
|
|
4003
4351
|
const payload = jwt2.verify(token, secret);
|
|
@@ -4083,6 +4431,33 @@ function user(options) {
|
|
|
4083
4431
|
}
|
|
4084
4432
|
});
|
|
4085
4433
|
}
|
|
4434
|
+
if (apiKeysEnabled) {
|
|
4435
|
+
r.get("/api-keys", middleware(), async (_req, ctx) => {
|
|
4436
|
+
const keys = await listApiKeys(ctx.user.id);
|
|
4437
|
+
return Response.json(keys);
|
|
4438
|
+
});
|
|
4439
|
+
r.post("/api-keys", middleware(), async (req, ctx) => {
|
|
4440
|
+
try {
|
|
4441
|
+
const body = await parseBody2(req);
|
|
4442
|
+
const { name, scopes } = CreateApiKeySchema.parse(body);
|
|
4443
|
+
const result = await createApiKey(ctx.user.id, name, scopes);
|
|
4444
|
+
return Response.json(result, { status: 201 });
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
if (err instanceof z2.ZodError) {
|
|
4447
|
+
return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
|
|
4448
|
+
}
|
|
4449
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
4450
|
+
}
|
|
4451
|
+
});
|
|
4452
|
+
r.delete("/api-keys/:id", middleware(), async (req, ctx) => {
|
|
4453
|
+
const keyId = parseInt(ctx.params.id, 10);
|
|
4454
|
+
if (isNaN(keyId)) {
|
|
4455
|
+
return Response.json({ error: "Invalid key ID" }, { status: 400 });
|
|
4456
|
+
}
|
|
4457
|
+
await revokeApiKey(ctx.user.id, keyId);
|
|
4458
|
+
return Response.json({ ok: true });
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4086
4461
|
if (oauth2) {
|
|
4087
4462
|
r.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
|
|
4088
4463
|
r.post("/oauth/consent", (req) => oauth2.consentHandler(req));
|
|
@@ -4127,6 +4502,21 @@ function user(options) {
|
|
|
4127
4502
|
mod.revokeClient = oauth2 ? (clientId) => oauth2.revokeClient(clientId) : async () => {
|
|
4128
4503
|
throw new Error("OAuth2 server is not enabled");
|
|
4129
4504
|
};
|
|
4505
|
+
mod.createApiKey = hasDb && apiKeysEnabled ? createApiKey : async () => {
|
|
4506
|
+
throw new Error(
|
|
4507
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4508
|
+
);
|
|
4509
|
+
};
|
|
4510
|
+
mod.listApiKeys = hasDb && apiKeysEnabled ? listApiKeys : async () => {
|
|
4511
|
+
throw new Error(
|
|
4512
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4513
|
+
);
|
|
4514
|
+
};
|
|
4515
|
+
mod.revokeApiKey = hasDb && apiKeysEnabled ? revokeApiKey : async () => {
|
|
4516
|
+
throw new Error(
|
|
4517
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4518
|
+
);
|
|
4519
|
+
};
|
|
4130
4520
|
mod.close = hasDb ? () => base.close() : async () => {
|
|
4131
4521
|
};
|
|
4132
4522
|
return mod;
|
|
@@ -4143,6 +4533,7 @@ function redis(opts) {
|
|
|
4143
4533
|
ctx.redis = client;
|
|
4144
4534
|
return next(req, ctx);
|
|
4145
4535
|
});
|
|
4536
|
+
mw.__meta = { injects: ["redis"], depends: [] };
|
|
4146
4537
|
mw.redis = client;
|
|
4147
4538
|
mw.close = () => client.quit();
|
|
4148
4539
|
return mw;
|
|
@@ -5590,23 +5981,23 @@ function buildGraphQLHandler(sql2) {
|
|
|
5590
5981
|
});
|
|
5591
5982
|
return Response.json(result, { status: result.errors ? 400 : 200 });
|
|
5592
5983
|
});
|
|
5593
|
-
r.get("/", async (req,
|
|
5984
|
+
r.get("/", async (req, _ctx2) => {
|
|
5594
5985
|
const url = new URL(req.url);
|
|
5595
5986
|
if (url.searchParams.has("query")) {
|
|
5596
|
-
return handleGET(req,
|
|
5987
|
+
return handleGET(req, _ctx2);
|
|
5597
5988
|
}
|
|
5598
5989
|
return new Response("GraphQL endpoint. Send POST /graphql with { query, variables }", {
|
|
5599
5990
|
status: 200,
|
|
5600
5991
|
headers: { "Content-Type": "text/plain" }
|
|
5601
5992
|
});
|
|
5602
5993
|
});
|
|
5603
|
-
async function handleGET(req,
|
|
5994
|
+
async function handleGET(req, _ctx2) {
|
|
5604
5995
|
const tables = await sql2`
|
|
5605
5996
|
SELECT * FROM "_user_tables"
|
|
5606
|
-
WHERE tenant_id = ${
|
|
5997
|
+
WHERE tenant_id = ${_ctx2.tenant.id}
|
|
5607
5998
|
ORDER BY created_at ASC
|
|
5608
5999
|
`;
|
|
5609
|
-
const buildCtx = { sql: sql2, tenantId:
|
|
6000
|
+
const buildCtx = { sql: sql2, tenantId: _ctx2.tenant.id, tables, typeCache: /* @__PURE__ */ new Map() };
|
|
5610
6001
|
const schema = new GraphQLSchema({
|
|
5611
6002
|
query: new GraphQLObjectType({
|
|
5612
6003
|
name: "Query",
|
|
@@ -5626,7 +6017,7 @@ function buildGraphQLHandler(sql2) {
|
|
|
5626
6017
|
source: query,
|
|
5627
6018
|
variableValues: variables,
|
|
5628
6019
|
operationName: url.searchParams.get("operationName") || void 0,
|
|
5629
|
-
contextValue:
|
|
6020
|
+
contextValue: _ctx2
|
|
5630
6021
|
});
|
|
5631
6022
|
return Response.json(result, { status: result.errors ? 400 : 200 });
|
|
5632
6023
|
}
|
|
@@ -6653,14 +7044,14 @@ function forkApp(opts) {
|
|
|
6653
7044
|
return { child, port: opts.port };
|
|
6654
7045
|
}
|
|
6655
7046
|
function stopProcess(mp, timeout = 1e4) {
|
|
6656
|
-
return new Promise((
|
|
7047
|
+
return new Promise((resolve16) => {
|
|
6657
7048
|
const timer = setTimeout(() => {
|
|
6658
7049
|
mp.child.kill("SIGKILL");
|
|
6659
|
-
|
|
7050
|
+
resolve16();
|
|
6660
7051
|
}, timeout);
|
|
6661
7052
|
mp.child.on("exit", () => {
|
|
6662
7053
|
clearTimeout(timer);
|
|
6663
|
-
|
|
7054
|
+
resolve16();
|
|
6664
7055
|
});
|
|
6665
7056
|
mp.child.kill("SIGTERM");
|
|
6666
7057
|
});
|
|
@@ -6992,22 +7383,168 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
|
6992
7383
|
|
|
6993
7384
|
// ssr.ts
|
|
6994
7385
|
import { createElement as createElement3 } from "react";
|
|
6995
|
-
import { createHash as
|
|
6996
|
-
import { existsSync as
|
|
7386
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7387
|
+
import { existsSync as existsSync6, readdirSync } from "node:fs";
|
|
6997
7388
|
import { readdir, stat } from "node:fs/promises";
|
|
6998
|
-
import { dirname as
|
|
7389
|
+
import { dirname as dirname4, join as join5, resolve as resolve8, relative as relative3 } from "node:path";
|
|
6999
7390
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
7000
7391
|
|
|
7001
7392
|
// compile.ts
|
|
7002
|
-
import * as
|
|
7003
|
-
import { existsSync, mkdirSync, readFileSync as
|
|
7004
|
-
import { join as join2, resolve as
|
|
7393
|
+
import * as esbuild2 from "esbuild";
|
|
7394
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
7395
|
+
import { join as join2, resolve as resolve4, dirname as dirname2 } from "node:path";
|
|
7005
7396
|
import { pathToFileURL } from "node:url";
|
|
7006
|
-
import { createHash } from "node:crypto";
|
|
7397
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7398
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
7399
|
+
|
|
7400
|
+
// server-registry.ts
|
|
7401
|
+
import * as esbuild from "esbuild";
|
|
7402
|
+
import { existsSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
7403
|
+
import { resolve as resolve3, dirname } from "node:path";
|
|
7007
7404
|
import vm from "node:vm";
|
|
7008
7405
|
import { createRequire } from "node:module";
|
|
7009
|
-
var _cjsRequire = createRequire(import.meta.url);
|
|
7010
7406
|
var _userRequire = null;
|
|
7407
|
+
function getUserRequire() {
|
|
7408
|
+
if (!_userRequire) {
|
|
7409
|
+
try {
|
|
7410
|
+
_userRequire = createRequire(resolve3(process.cwd(), "package.json"));
|
|
7411
|
+
} catch {
|
|
7412
|
+
_userRequire = createRequire(import.meta.url);
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
return _userRequire;
|
|
7416
|
+
}
|
|
7417
|
+
var _alias = null;
|
|
7418
|
+
function resolveAliases() {
|
|
7419
|
+
if (_alias) return _alias;
|
|
7420
|
+
const configFiles = ["tsconfig.json", "jsconfig.json"];
|
|
7421
|
+
for (const file of configFiles) {
|
|
7422
|
+
const p = resolve3(file);
|
|
7423
|
+
if (existsSync(p)) {
|
|
7424
|
+
try {
|
|
7425
|
+
const config = JSON.parse(readFileSync2(p, "utf-8"));
|
|
7426
|
+
const paths = config.compilerOptions?.paths;
|
|
7427
|
+
if (paths) {
|
|
7428
|
+
const alias = {};
|
|
7429
|
+
for (const [key, values] of Object.entries(paths)) {
|
|
7430
|
+
const cleanKey = key.replace("/*", "");
|
|
7431
|
+
const val = values[0]?.replace("/*", "");
|
|
7432
|
+
if (val) alias[cleanKey] = resolve3(dirname(p), val);
|
|
7433
|
+
}
|
|
7434
|
+
_alias = alias;
|
|
7435
|
+
return alias;
|
|
7436
|
+
}
|
|
7437
|
+
} catch {
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
_alias = {};
|
|
7442
|
+
return {};
|
|
7443
|
+
}
|
|
7444
|
+
function applyAlias(id2, _moduleDir) {
|
|
7445
|
+
const aliases = resolveAliases();
|
|
7446
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
7447
|
+
if (id2.startsWith(prefix)) {
|
|
7448
|
+
const rest = id2.slice(prefix.length);
|
|
7449
|
+
return target + rest;
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
return null;
|
|
7453
|
+
}
|
|
7454
|
+
var exts = [".tsx", ".ts", ".jsx", ".js"];
|
|
7455
|
+
function tryResolve(base) {
|
|
7456
|
+
if (existsSync(base)) {
|
|
7457
|
+
const stat3 = statSync(base);
|
|
7458
|
+
if (stat3.isFile()) return base;
|
|
7459
|
+
if (stat3.isDirectory()) {
|
|
7460
|
+
for (const ext of exts) {
|
|
7461
|
+
const p = resolve3(base, `index${ext}`);
|
|
7462
|
+
if (existsSync(p)) return p;
|
|
7463
|
+
}
|
|
7464
|
+
return null;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
for (const ext of exts) {
|
|
7468
|
+
const p = base + ext;
|
|
7469
|
+
if (existsSync(p)) return p;
|
|
7470
|
+
}
|
|
7471
|
+
return null;
|
|
7472
|
+
}
|
|
7473
|
+
var registry = /* @__PURE__ */ new Map();
|
|
7474
|
+
var _ctx = vm.createContext(Object.create(globalThis));
|
|
7475
|
+
function transformToCjs(absPath, source) {
|
|
7476
|
+
const isTsx = absPath.endsWith(".tsx");
|
|
7477
|
+
const result = esbuild.transformSync(source, {
|
|
7478
|
+
loader: isTsx ? "tsx" : "ts",
|
|
7479
|
+
format: "cjs",
|
|
7480
|
+
jsx: isTsx ? "automatic" : void 0,
|
|
7481
|
+
jsxImportSource: isTsx ? "react" : void 0,
|
|
7482
|
+
sourcemap: false
|
|
7483
|
+
});
|
|
7484
|
+
return result.code;
|
|
7485
|
+
}
|
|
7486
|
+
function makeRequire(modulePath) {
|
|
7487
|
+
const moduleDir = dirname(modulePath);
|
|
7488
|
+
return (id2) => {
|
|
7489
|
+
if (id2.startsWith(".")) {
|
|
7490
|
+
const base = resolve3(moduleDir, id2);
|
|
7491
|
+
const file = tryResolve(base);
|
|
7492
|
+
if (!file) {
|
|
7493
|
+
throw new Error(
|
|
7494
|
+
`[server-registry] Cannot resolve '${id2}' from '${modulePath}'. Tried: ${[base, ...exts.map((e) => base + e)].filter((p) => !p.endsWith(base)).join(", ")}`
|
|
7495
|
+
);
|
|
7496
|
+
}
|
|
7497
|
+
return getServerModule(file);
|
|
7498
|
+
}
|
|
7499
|
+
const aliased = applyAlias(id2, moduleDir);
|
|
7500
|
+
if (aliased) {
|
|
7501
|
+
const file = tryResolve(aliased);
|
|
7502
|
+
if (file) return getServerModule(file);
|
|
7503
|
+
}
|
|
7504
|
+
return getUserRequire()(id2);
|
|
7505
|
+
};
|
|
7506
|
+
}
|
|
7507
|
+
function evaluateModule(code, modulePath) {
|
|
7508
|
+
const mod = { exports: {} };
|
|
7509
|
+
const require2 = makeRequire(modulePath);
|
|
7510
|
+
const _dirname = dirname(modulePath);
|
|
7511
|
+
const _filename = modulePath;
|
|
7512
|
+
const wrapped = `(function(require,module,exports,__dirname,__filename){
|
|
7513
|
+
${code}
|
|
7514
|
+
})`;
|
|
7515
|
+
try {
|
|
7516
|
+
new vm.Script(wrapped).runInContext(_ctx)(require2, mod, mod.exports, _dirname, _filename);
|
|
7517
|
+
} catch (err) {
|
|
7518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7519
|
+
const cause = err instanceof Error ? err : void 0;
|
|
7520
|
+
throw new Error(
|
|
7521
|
+
`[server-registry] Error evaluating '${modulePath}': ${msg}`,
|
|
7522
|
+
cause ? { cause } : void 0
|
|
7523
|
+
);
|
|
7524
|
+
}
|
|
7525
|
+
return mod.exports;
|
|
7526
|
+
}
|
|
7527
|
+
function getServerModule(absPath) {
|
|
7528
|
+
const normalized = resolve3(absPath);
|
|
7529
|
+
if (registry.has(normalized)) return registry.get(normalized).exports;
|
|
7530
|
+
const source = readFileSync2(normalized, "utf-8");
|
|
7531
|
+
const code = transformToCjs(normalized, source);
|
|
7532
|
+
const exports = evaluateModule(code, normalized);
|
|
7533
|
+
registry.set(normalized, { exports });
|
|
7534
|
+
return exports;
|
|
7535
|
+
}
|
|
7536
|
+
function clearServerModule(absPath) {
|
|
7537
|
+
if (absPath) {
|
|
7538
|
+
const normalized = resolve3(absPath);
|
|
7539
|
+
registry.delete(normalized);
|
|
7540
|
+
} else {
|
|
7541
|
+
registry.clear();
|
|
7542
|
+
_alias = null;
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7546
|
+
// compile.ts
|
|
7547
|
+
var _userRequire2 = null;
|
|
7011
7548
|
var OUT_DIR = ".weifuwu/ssr";
|
|
7012
7549
|
var cache = /* @__PURE__ */ new Map();
|
|
7013
7550
|
var externals = [
|
|
@@ -7020,48 +7557,49 @@ var externals = [
|
|
|
7020
7557
|
"@graphql-tools/schema",
|
|
7021
7558
|
"ai"
|
|
7022
7559
|
];
|
|
7023
|
-
var
|
|
7024
|
-
function
|
|
7025
|
-
if (
|
|
7560
|
+
var _alias2 = null;
|
|
7561
|
+
function resolveAliases2() {
|
|
7562
|
+
if (_alias2) return _alias2;
|
|
7026
7563
|
const configFiles = ["tsconfig.json", "jsconfig.json"];
|
|
7027
7564
|
for (const file of configFiles) {
|
|
7028
|
-
const p =
|
|
7029
|
-
if (
|
|
7565
|
+
const p = resolve4(file);
|
|
7566
|
+
if (existsSync2(p)) {
|
|
7030
7567
|
try {
|
|
7031
|
-
const config = JSON.parse(
|
|
7568
|
+
const config = JSON.parse(readFileSync3(p, "utf-8"));
|
|
7032
7569
|
const paths = config.compilerOptions?.paths;
|
|
7033
7570
|
if (paths) {
|
|
7034
7571
|
const alias = {};
|
|
7035
7572
|
for (const [key, values] of Object.entries(paths)) {
|
|
7036
7573
|
const cleanKey = key.replace("/*", "");
|
|
7037
7574
|
const val = values[0]?.replace("/*", "");
|
|
7038
|
-
if (val) alias[cleanKey] =
|
|
7575
|
+
if (val) alias[cleanKey] = resolve4(dirname2(p), val);
|
|
7039
7576
|
}
|
|
7040
|
-
|
|
7577
|
+
_alias2 = alias;
|
|
7041
7578
|
return alias;
|
|
7042
7579
|
}
|
|
7043
7580
|
} catch {
|
|
7044
7581
|
}
|
|
7045
7582
|
}
|
|
7046
7583
|
}
|
|
7047
|
-
|
|
7584
|
+
_alias2 = {};
|
|
7048
7585
|
return {};
|
|
7049
7586
|
}
|
|
7050
7587
|
function id(s) {
|
|
7051
|
-
return
|
|
7588
|
+
return createHash2("md5").update(s).digest("hex").slice(0, 8);
|
|
7052
7589
|
}
|
|
7053
7590
|
function clearCompileCache() {
|
|
7054
7591
|
cache.clear();
|
|
7055
|
-
|
|
7592
|
+
clearServerModule();
|
|
7593
|
+
_alias2 = null;
|
|
7056
7594
|
}
|
|
7057
7595
|
async function compileTsx(path2) {
|
|
7058
|
-
const absPath =
|
|
7596
|
+
const absPath = resolve4(path2);
|
|
7059
7597
|
if (cache.has(absPath)) return cache.get(absPath);
|
|
7060
|
-
const outDir =
|
|
7598
|
+
const outDir = resolve4(OUT_DIR);
|
|
7061
7599
|
mkdirSync(outDir, { recursive: true });
|
|
7062
7600
|
const hash = id(absPath);
|
|
7063
7601
|
const outPath = join2(outDir, hash + ".js");
|
|
7064
|
-
await
|
|
7602
|
+
await esbuild2.build({
|
|
7065
7603
|
entryPoints: { [hash]: absPath },
|
|
7066
7604
|
outdir: outDir,
|
|
7067
7605
|
format: "esm",
|
|
@@ -7070,7 +7608,7 @@ async function compileTsx(path2) {
|
|
|
7070
7608
|
jsxImportSource: "react",
|
|
7071
7609
|
bundle: true,
|
|
7072
7610
|
external: externals,
|
|
7073
|
-
alias:
|
|
7611
|
+
alias: resolveAliases2(),
|
|
7074
7612
|
write: true,
|
|
7075
7613
|
allowOverwrite: true
|
|
7076
7614
|
});
|
|
@@ -7078,42 +7616,20 @@ async function compileTsx(path2) {
|
|
|
7078
7616
|
cache.set(absPath, mod);
|
|
7079
7617
|
return mod;
|
|
7080
7618
|
}
|
|
7081
|
-
function
|
|
7082
|
-
const
|
|
7083
|
-
const mod =
|
|
7084
|
-
ctx.require = (name) => _cjsRequire(name);
|
|
7085
|
-
ctx.module = mod;
|
|
7086
|
-
ctx.exports = mod.exports;
|
|
7087
|
-
new vm.Script(code).runInContext(ctx);
|
|
7088
|
-
return mod.exports;
|
|
7089
|
-
}
|
|
7090
|
-
async function compileTsxDev(path2) {
|
|
7091
|
-
const absPath = resolve3(path2);
|
|
7092
|
-
if (cache.has(absPath)) return cache.get(absPath);
|
|
7093
|
-
const result = await esbuild.build({
|
|
7094
|
-
entryPoints: { [id(absPath)]: absPath },
|
|
7095
|
-
format: "cjs",
|
|
7096
|
-
platform: "node",
|
|
7097
|
-
jsx: "automatic",
|
|
7098
|
-
jsxImportSource: "react",
|
|
7099
|
-
bundle: true,
|
|
7100
|
-
external: externals,
|
|
7101
|
-
alias: resolveAliases(),
|
|
7102
|
-
write: false
|
|
7103
|
-
});
|
|
7104
|
-
const code = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
7105
|
-
const mod = loadSSRModule(code);
|
|
7619
|
+
function compileTsxDev(path2) {
|
|
7620
|
+
const absPath = resolve4(path2);
|
|
7621
|
+
const mod = getServerModule(absPath);
|
|
7106
7622
|
cache.set(absPath, mod);
|
|
7107
7623
|
return mod;
|
|
7108
7624
|
}
|
|
7109
7625
|
function compile(path2) {
|
|
7110
|
-
return isDev() ? compileTsxDev(path2) : compileTsx(path2);
|
|
7626
|
+
return isDev() ? Promise.resolve(compileTsxDev(path2)) : compileTsx(path2);
|
|
7111
7627
|
}
|
|
7112
7628
|
var vendorBundle = null;
|
|
7113
7629
|
var vendorHash = "";
|
|
7114
7630
|
async function compileVendorBundle() {
|
|
7115
7631
|
if (vendorBundle) return vendorBundle;
|
|
7116
|
-
if (!
|
|
7632
|
+
if (!_userRequire2) _userRequire2 = createRequire2(join2(process.cwd(), "package.json"));
|
|
7117
7633
|
const modules = {
|
|
7118
7634
|
react: [],
|
|
7119
7635
|
"react-dom": ["react"],
|
|
@@ -7121,13 +7637,13 @@ async function compileVendorBundle() {
|
|
|
7121
7637
|
"react/jsx-runtime": ["react"]
|
|
7122
7638
|
};
|
|
7123
7639
|
for (const request of Object.keys(modules)) {
|
|
7124
|
-
const mod =
|
|
7640
|
+
const mod = _userRequire2(request);
|
|
7125
7641
|
const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
|
|
7126
7642
|
modules[request] = keys;
|
|
7127
7643
|
}
|
|
7128
7644
|
const baseDir = import.meta.dirname ?? __dirname;
|
|
7129
|
-
const reactAbsPath = isBundled() ?
|
|
7130
|
-
const reactSrc =
|
|
7645
|
+
const reactAbsPath = isBundled() ? resolve4(baseDir, "react.js") : resolve4(baseDir, "react.ts");
|
|
7646
|
+
const reactSrc = readFileSync3(reactAbsPath, "utf-8");
|
|
7131
7647
|
const wfwKeys = [];
|
|
7132
7648
|
if (reactAbsPath.endsWith(".ts")) {
|
|
7133
7649
|
for (const line of reactSrc.split("\n")) {
|
|
@@ -7158,7 +7674,7 @@ async function compileVendorBundle() {
|
|
|
7158
7674
|
const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
|
|
7159
7675
|
if (uidWfw.length > 0)
|
|
7160
7676
|
stmts.push(`export { ${uidWfw.join(", ")} } from ${JSON.stringify(reactAbsPath)};`);
|
|
7161
|
-
const result = await
|
|
7677
|
+
const result = await esbuild2.build({
|
|
7162
7678
|
stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
|
|
7163
7679
|
format: "esm",
|
|
7164
7680
|
bundle: true,
|
|
@@ -7170,95 +7686,6 @@ async function compileVendorBundle() {
|
|
|
7170
7686
|
vendorHash = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
|
|
7171
7687
|
return vendorBundle;
|
|
7172
7688
|
}
|
|
7173
|
-
async function compileBrowser(path2, outDir) {
|
|
7174
|
-
const absPath = resolve3(path2);
|
|
7175
|
-
const h = id(absPath);
|
|
7176
|
-
outDir = outDir ?? resolve3(OUT_DIR);
|
|
7177
|
-
const outPath = join2(outDir, h + ".js");
|
|
7178
|
-
if (!isDev() && existsSync(outPath)) return h;
|
|
7179
|
-
mkdirSync(outDir, { recursive: true });
|
|
7180
|
-
const wfwDir = resolve3(import.meta.dirname ?? __dirname);
|
|
7181
|
-
const plugin = {
|
|
7182
|
-
name: "wfw-external",
|
|
7183
|
-
setup(build2) {
|
|
7184
|
-
build2.onResolve({ filter: /./ }, (args) => {
|
|
7185
|
-
if (args.kind === "entry-point") return;
|
|
7186
|
-
const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
|
|
7187
|
-
if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
|
|
7188
|
-
const rel = abs.slice(wfwDir.length + 1);
|
|
7189
|
-
if (rel.includes("/")) return;
|
|
7190
|
-
return { path: "weifuwu/react", external: true };
|
|
7191
|
-
}
|
|
7192
|
-
});
|
|
7193
|
-
}
|
|
7194
|
-
};
|
|
7195
|
-
await esbuild.build({
|
|
7196
|
-
entryPoints: { [h]: absPath },
|
|
7197
|
-
outdir: outDir,
|
|
7198
|
-
format: "esm",
|
|
7199
|
-
platform: "browser",
|
|
7200
|
-
jsx: "automatic",
|
|
7201
|
-
jsxImportSource: "react",
|
|
7202
|
-
bundle: true,
|
|
7203
|
-
external: [
|
|
7204
|
-
"react",
|
|
7205
|
-
"react-dom",
|
|
7206
|
-
"react-dom/client",
|
|
7207
|
-
"react/jsx-runtime",
|
|
7208
|
-
"weifuwu",
|
|
7209
|
-
"weifuwu/react"
|
|
7210
|
-
],
|
|
7211
|
-
plugins: [plugin],
|
|
7212
|
-
write: true,
|
|
7213
|
-
allowOverwrite: true
|
|
7214
|
-
});
|
|
7215
|
-
return h;
|
|
7216
|
-
}
|
|
7217
|
-
async function compileHotComponent(path2) {
|
|
7218
|
-
const absPath = resolve3(path2);
|
|
7219
|
-
const h = id(absPath);
|
|
7220
|
-
const stdin = `import C from ${JSON.stringify(absPath)};
|
|
7221
|
-
(window.__WFW_REFRESH||function(){})(C)`;
|
|
7222
|
-
const wfwDir = resolve3(import.meta.dirname ?? __dirname);
|
|
7223
|
-
const plugin = {
|
|
7224
|
-
name: "wfw-external",
|
|
7225
|
-
setup(build2) {
|
|
7226
|
-
build2.onResolve({ filter: /./ }, (args) => {
|
|
7227
|
-
if (args.kind === "entry-point") return;
|
|
7228
|
-
const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
|
|
7229
|
-
if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
|
|
7230
|
-
const rel = abs.slice(wfwDir.length + 1);
|
|
7231
|
-
if (rel.includes("/")) return;
|
|
7232
|
-
return { path: "weifuwu/react", external: true };
|
|
7233
|
-
}
|
|
7234
|
-
});
|
|
7235
|
-
}
|
|
7236
|
-
};
|
|
7237
|
-
const result = await esbuild.build({
|
|
7238
|
-
stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
|
|
7239
|
-
format: "esm",
|
|
7240
|
-
platform: "browser",
|
|
7241
|
-
jsx: "automatic",
|
|
7242
|
-
jsxImportSource: "react",
|
|
7243
|
-
bundle: true,
|
|
7244
|
-
external: [
|
|
7245
|
-
"react",
|
|
7246
|
-
"react-dom",
|
|
7247
|
-
"react-dom/client",
|
|
7248
|
-
"react/jsx-runtime",
|
|
7249
|
-
"weifuwu",
|
|
7250
|
-
"weifuwu/react"
|
|
7251
|
-
],
|
|
7252
|
-
plugins: [plugin],
|
|
7253
|
-
write: false
|
|
7254
|
-
});
|
|
7255
|
-
let code = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
7256
|
-
if (code.includes("__require") && (code.includes('"react"') || code.includes("'react'"))) {
|
|
7257
|
-
code = `import * as __r from 'react';
|
|
7258
|
-
` + code.replace(/__require\(["']react["']\)/g, "__r");
|
|
7259
|
-
}
|
|
7260
|
-
return { hash: h, code };
|
|
7261
|
-
}
|
|
7262
7689
|
|
|
7263
7690
|
// stream.ts
|
|
7264
7691
|
import { TextDecoder as TextDecoder2, TextEncoder as TextEncoder2 } from "node:util";
|
|
@@ -7276,6 +7703,8 @@ function getPublicEnv2() {
|
|
|
7276
7703
|
function buildHeadPayload(opts) {
|
|
7277
7704
|
const { ctx, base, tailwind } = opts;
|
|
7278
7705
|
let result = "";
|
|
7706
|
+
result += `<script>window.__wfw={_cache:{},_k:function(u){return u.split('?')[0]},h:async function(u){var k=this._k(u);if(this._cache[k])return this._cache[k];var m=await import(u);this._cache[k]=m;return m},_update:function(u,mod){var k=this._k(u);this._cache[k]=mod}}</script>
|
|
7707
|
+
`;
|
|
7279
7708
|
const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
|
|
7280
7709
|
result += `<script type="importmap">{
|
|
7281
7710
|
"imports": {
|
|
@@ -7360,26 +7789,37 @@ function streamResponse(reactStream, opts, hydrationScript) {
|
|
|
7360
7789
|
if (built) bodyScripts += built;
|
|
7361
7790
|
if (opts.isDev) {
|
|
7362
7791
|
const wsUrl = `${opts.base}/__weifuwu/livereload`;
|
|
7363
|
-
const hbUrl = `${opts.base}/__wfw/h/`;
|
|
7364
7792
|
bodyScripts += `
|
|
7365
7793
|
<script>
|
|
7366
7794
|
(function(){
|
|
7367
7795
|
var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');
|
|
7368
7796
|
var t=0;
|
|
7797
|
+
var _w=window;
|
|
7369
7798
|
ws.onmessage=function(e){
|
|
7370
7799
|
try{
|
|
7371
7800
|
var m=JSON.parse(e.data);
|
|
7372
|
-
if(m.type==='
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7801
|
+
if(m.type==='update'&&m.url&&m.code){
|
|
7802
|
+
var blob=new Blob([m.code],{type:'application/javascript'});
|
|
7803
|
+
var blobUrl=URL.createObjectURL(blob);
|
|
7804
|
+
import(blobUrl).then(function(mod){
|
|
7805
|
+
if(_w.__wfw) _w.__wfw._update(m.url,mod);
|
|
7806
|
+
// Re-import page module so it re-evaluates its __wfw.h() imports
|
|
7807
|
+
var pageUrl=_w.__WFW_PAGE_URL;
|
|
7808
|
+
if(pageUrl&&_w.__WFW_REFRESH){
|
|
7809
|
+
import(pageUrl.split('?')[0]+'?t='+Date.now()).then(function(pageMod){
|
|
7810
|
+
if(pageMod.default) _w.__WFW_REFRESH(pageMod.default);
|
|
7811
|
+
if(m.css){
|
|
7812
|
+
var s=document.querySelector('style[data-lr]')||function(){
|
|
7813
|
+
var x=document.createElement('style');
|
|
7814
|
+
x.setAttribute('data-lr','');
|
|
7815
|
+
document.head.appendChild(x);
|
|
7816
|
+
return x
|
|
7817
|
+
}();
|
|
7818
|
+
s.textContent=m.css
|
|
7819
|
+
}
|
|
7820
|
+
});
|
|
7821
|
+
}else{location.reload()}
|
|
7822
|
+
}).catch(function(){location.reload()});
|
|
7383
7823
|
return
|
|
7384
7824
|
}
|
|
7385
7825
|
if(m.type==='css'){
|
|
@@ -7429,13 +7869,13 @@ ws.onclose=function(){
|
|
|
7429
7869
|
var ssrEntries = /* @__PURE__ */ new Map();
|
|
7430
7870
|
|
|
7431
7871
|
// tailwind.ts
|
|
7432
|
-
import { createHash as
|
|
7433
|
-
import { existsSync as
|
|
7434
|
-
import { join as join3, relative, resolve as
|
|
7872
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
7873
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
7874
|
+
import { join as join3, relative, resolve as resolve5 } from "node:path";
|
|
7435
7875
|
var extraSources = /* @__PURE__ */ new Set();
|
|
7436
7876
|
var cssCache = /* @__PURE__ */ new Map();
|
|
7437
7877
|
function tailwindContext(dir) {
|
|
7438
|
-
const cssDir =
|
|
7878
|
+
const cssDir = resolve5(dir);
|
|
7439
7879
|
const cssPath = join3(cssDir, "app", "globals.css");
|
|
7440
7880
|
return async (req, ctx, next) => {
|
|
7441
7881
|
if (!cssCache.has(cssPath)) {
|
|
@@ -7449,10 +7889,10 @@ function tailwindContext(dir) {
|
|
|
7449
7889
|
};
|
|
7450
7890
|
}
|
|
7451
7891
|
function tailwindRouter(dir) {
|
|
7452
|
-
const cssDir =
|
|
7892
|
+
const cssDir = resolve5(dir);
|
|
7453
7893
|
const cssPath = join3(cssDir, "app", "globals.css");
|
|
7454
7894
|
const r = new Router();
|
|
7455
|
-
r.get("/__wfw/style/:hash.css", async (_req,
|
|
7895
|
+
r.get("/__wfw/style/:hash.css", async (_req, _ctx2) => {
|
|
7456
7896
|
if (!cssCache.has(cssPath)) {
|
|
7457
7897
|
await compileTailwindCss(cssPath, cssDir);
|
|
7458
7898
|
}
|
|
@@ -7466,13 +7906,13 @@ function tailwindRouter(dir) {
|
|
|
7466
7906
|
}
|
|
7467
7907
|
async function compileTailwindCss(cssPath, cssDir) {
|
|
7468
7908
|
try {
|
|
7469
|
-
if (!
|
|
7909
|
+
if (!existsSync3(cssPath)) {
|
|
7470
7910
|
mkdirSync2(cssDir, { recursive: true });
|
|
7471
7911
|
writeFileSync(cssPath, '@import "tailwindcss"\n', "utf-8");
|
|
7472
7912
|
}
|
|
7473
7913
|
const { default: tailwindPlugin } = await import("@tailwindcss/postcss");
|
|
7474
7914
|
const { default: postcss } = await import("postcss");
|
|
7475
|
-
let src =
|
|
7915
|
+
let src = readFileSync4(cssPath, "utf-8");
|
|
7476
7916
|
src = `@source "./";
|
|
7477
7917
|
${src}`;
|
|
7478
7918
|
for (const srcDir of extraSources) {
|
|
@@ -7481,7 +7921,7 @@ ${src}`;
|
|
|
7481
7921
|
${src}`;
|
|
7482
7922
|
}
|
|
7483
7923
|
const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
|
|
7484
|
-
const hash =
|
|
7924
|
+
const hash = createHash3("md5").update(result.css).digest("hex").slice(0, 8);
|
|
7485
7925
|
cssCache.set(cssPath, { css: result.css, hash });
|
|
7486
7926
|
return result.css;
|
|
7487
7927
|
} catch (err) {
|
|
@@ -7492,22 +7932,151 @@ ${src}`;
|
|
|
7492
7932
|
|
|
7493
7933
|
// live.ts
|
|
7494
7934
|
import chokidar from "chokidar";
|
|
7495
|
-
import { existsSync as
|
|
7496
|
-
import {
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7935
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
7936
|
+
import { join as join4, resolve as resolve7 } from "node:path";
|
|
7937
|
+
|
|
7938
|
+
// module-server.ts
|
|
7939
|
+
import * as esbuild3 from "esbuild";
|
|
7940
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
7941
|
+
import { resolve as resolve6, dirname as dirname3, relative as relative2 } from "node:path";
|
|
7942
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7943
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
7944
|
+
var hashCache = /* @__PURE__ */ new Map();
|
|
7945
|
+
function clearModuleCache(filePath) {
|
|
7946
|
+
if (filePath) {
|
|
7947
|
+
const abs = resolve6(filePath);
|
|
7948
|
+
for (const key of moduleCache.keys()) {
|
|
7949
|
+
if (key.endsWith(abs)) moduleCache.delete(key);
|
|
7950
|
+
}
|
|
7951
|
+
hashCache.delete(abs);
|
|
7952
|
+
} else {
|
|
7953
|
+
moduleCache.clear();
|
|
7954
|
+
hashCache.clear();
|
|
7508
7955
|
}
|
|
7509
|
-
hotBundleCache.set(hash, code);
|
|
7510
7956
|
}
|
|
7957
|
+
var _importRoots = [];
|
|
7958
|
+
function _setImportRoots(roots) {
|
|
7959
|
+
_importRoots = roots;
|
|
7960
|
+
}
|
|
7961
|
+
function fileHash(absPath) {
|
|
7962
|
+
const cached = hashCache.get(absPath);
|
|
7963
|
+
if (cached) return cached;
|
|
7964
|
+
try {
|
|
7965
|
+
const content = readFileSync5(absPath);
|
|
7966
|
+
const h = createHash4("md5").update(content).digest("hex").slice(0, 8);
|
|
7967
|
+
hashCache.set(absPath, h);
|
|
7968
|
+
return h;
|
|
7969
|
+
} catch {
|
|
7970
|
+
return "00000000";
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
function rewriteImports(code, absPath, mountPath) {
|
|
7974
|
+
const prefix = mountPath ? `${mountPath}/__wfw/m` : "/__wfw/m";
|
|
7975
|
+
let varCounter = 0;
|
|
7976
|
+
return code.replace(
|
|
7977
|
+
/^(import|export)\s+(.+?)\s+from\s+['"]([^'"]+)['"];?\s*$/gm,
|
|
7978
|
+
(_match, keyword, clause, modPath) => {
|
|
7979
|
+
if (!modPath.startsWith(".")) return _match;
|
|
7980
|
+
const isReexport = keyword === "export";
|
|
7981
|
+
const imports = clause.replace(/^type\s+/, "");
|
|
7982
|
+
const resolved = resolve6(dirname3(absPath), modPath);
|
|
7983
|
+
for (const root of _importRoots) {
|
|
7984
|
+
const rel = relative2(root, resolved);
|
|
7985
|
+
if (!rel.startsWith("..") && !rel.startsWith("/")) {
|
|
7986
|
+
const v = fileHash(resolved);
|
|
7987
|
+
const url = `${prefix}/${rel}?v=${v}`;
|
|
7988
|
+
const defaultMatch = imports.match(/^\s*(\w[\w$]*)\s*$/);
|
|
7989
|
+
const namedMatch = imports.match(/^\s*\{\s*([\w$,\s]+)\s*\}\s*$/);
|
|
7990
|
+
const mixedMatch = imports.match(/^\s*(\w[\w$]*)\s*,\s*\{\s*([\w$,\s]+)\s*\}\s*$/);
|
|
7991
|
+
if (defaultMatch) {
|
|
7992
|
+
const name = defaultMatch[1];
|
|
7993
|
+
if (isReexport) {
|
|
7994
|
+
return `const { default: ${name} } = await __wfw.h("${url}");
|
|
7995
|
+
export { ${name} as default }`;
|
|
7996
|
+
}
|
|
7997
|
+
return `const { default: ${name} } = await __wfw.h("${url}");`;
|
|
7998
|
+
}
|
|
7999
|
+
if (namedMatch) {
|
|
8000
|
+
const names = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8001
|
+
if (isReexport) {
|
|
8002
|
+
const tmp = `__wfw$${varCounter++}`;
|
|
8003
|
+
const lines = [`const ${tmp} = await __wfw.h("${url}");`];
|
|
8004
|
+
for (const n of names) lines.push(`export const ${n} = ${tmp}.${n};`);
|
|
8005
|
+
return lines.join("\n");
|
|
8006
|
+
}
|
|
8007
|
+
const decl = names.map((n) => `${n}`).join(", ");
|
|
8008
|
+
return `const { ${decl} } = await __wfw.h("${url}");`;
|
|
8009
|
+
}
|
|
8010
|
+
if (mixedMatch) {
|
|
8011
|
+
const defaultName = mixedMatch[1];
|
|
8012
|
+
const namedNames = mixedMatch[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8013
|
+
const varName = `__wfw$${varCounter++}`;
|
|
8014
|
+
const lines = [
|
|
8015
|
+
`const ${varName} = await __wfw.h("${url}");`,
|
|
8016
|
+
`const ${defaultName} = ${varName}.default;`
|
|
8017
|
+
];
|
|
8018
|
+
for (const n of namedNames) lines.push(`const { ${n} } = ${varName};`);
|
|
8019
|
+
return lines.join("\n");
|
|
8020
|
+
}
|
|
8021
|
+
return _match;
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return _match;
|
|
8025
|
+
}
|
|
8026
|
+
);
|
|
8027
|
+
}
|
|
8028
|
+
async function transformModule(absPath, root, mountPath) {
|
|
8029
|
+
const mp = mountPath || "";
|
|
8030
|
+
const cacheKey = mp + absPath;
|
|
8031
|
+
const cached = moduleCache.get(cacheKey);
|
|
8032
|
+
if (cached) return { url: `${mp}/__wfw/m/${relative2(root, absPath)}`, code: cached };
|
|
8033
|
+
const source = readFileSync5(absPath, "utf-8");
|
|
8034
|
+
const isTsx = absPath.endsWith(".tsx");
|
|
8035
|
+
const result = await esbuild3.transform(source, {
|
|
8036
|
+
loader: isTsx ? "tsx" : "ts",
|
|
8037
|
+
jsx: isTsx ? "automatic" : void 0,
|
|
8038
|
+
jsxImportSource: isTsx ? "react" : void 0,
|
|
8039
|
+
sourcemap: false
|
|
8040
|
+
});
|
|
8041
|
+
let code = result.code;
|
|
8042
|
+
code = rewriteImports(code, absPath, mp);
|
|
8043
|
+
moduleCache.set(cacheKey, code);
|
|
8044
|
+
const url = `${mp}/__wfw/m/${relative2(root, absPath)}`;
|
|
8045
|
+
return { url, code };
|
|
8046
|
+
}
|
|
8047
|
+
function moduleServer(opts) {
|
|
8048
|
+
const roots = Array.isArray(opts.root) ? opts.root : [opts.root];
|
|
8049
|
+
_setImportRoots(roots);
|
|
8050
|
+
const router = new Router();
|
|
8051
|
+
router.get("/__wfw/m/*", (async (req, ctx) => {
|
|
8052
|
+
const reqUrl = new URL(req.url);
|
|
8053
|
+
const filePath = (ctx.params["*"] || "").split("?")[0];
|
|
8054
|
+
const ext = filePath.split(".").pop();
|
|
8055
|
+
if (ext !== "tsx" && ext !== "ts") {
|
|
8056
|
+
return new Response("Not Found", { status: 404 });
|
|
8057
|
+
}
|
|
8058
|
+
const mountPath = ctx.mountPath || "";
|
|
8059
|
+
for (const root of roots) {
|
|
8060
|
+
const absPath = resolve6(root, filePath);
|
|
8061
|
+
if (existsSync4(absPath)) {
|
|
8062
|
+
try {
|
|
8063
|
+
const { code } = await transformModule(absPath, root, mountPath);
|
|
8064
|
+
return new Response(code, {
|
|
8065
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
8066
|
+
});
|
|
8067
|
+
} catch (err) {
|
|
8068
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8069
|
+
return new Response(`/* Error: ${msg} */`, { status: 500 });
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
return new Response("Not Found", { status: 404 });
|
|
8074
|
+
}));
|
|
8075
|
+
return router;
|
|
8076
|
+
}
|
|
8077
|
+
|
|
8078
|
+
// live.ts
|
|
8079
|
+
var clients = /* @__PURE__ */ new Set();
|
|
7511
8080
|
function broadcastReload() {
|
|
7512
8081
|
for (const ws of clients) {
|
|
7513
8082
|
try {
|
|
@@ -7529,7 +8098,7 @@ function broadcastCss(css) {
|
|
|
7529
8098
|
}
|
|
7530
8099
|
function liveWs() {
|
|
7531
8100
|
return {
|
|
7532
|
-
open(ws) {
|
|
8101
|
+
open(ws, _ctx2) {
|
|
7533
8102
|
clients.add(ws);
|
|
7534
8103
|
ws.on("close", () => clients.delete(ws));
|
|
7535
8104
|
ws.on("error", () => clients.delete(ws));
|
|
@@ -7540,78 +8109,52 @@ function liveRouter(_dir) {
|
|
|
7540
8109
|
const r = new Router();
|
|
7541
8110
|
compileVendorBundle().catch(() => {
|
|
7542
8111
|
});
|
|
7543
|
-
r.get("/__wfw/h/:hash", async (req, ctx) => {
|
|
7544
|
-
const hash = ctx.params.hash.replace(/\.js$/i, "");
|
|
7545
|
-
const code = hotBundleCache.get(hash);
|
|
7546
|
-
if (!code) return new Response("", { status: 404 });
|
|
7547
|
-
return new Response(code, {
|
|
7548
|
-
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7549
|
-
});
|
|
7550
|
-
});
|
|
7551
8112
|
return r;
|
|
7552
8113
|
}
|
|
7553
8114
|
function liveWatcher(dir) {
|
|
7554
|
-
const resolved =
|
|
7555
|
-
const entryPath = join4(resolved, "page.tsx");
|
|
8115
|
+
const resolved = resolve7(dir);
|
|
7556
8116
|
const watcher = chokidar.watch(dir, {
|
|
7557
8117
|
ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
|
|
7558
8118
|
ignoreInitial: true
|
|
7559
8119
|
});
|
|
7560
|
-
function findEntries(changedPath) {
|
|
7561
|
-
const matched = [];
|
|
7562
|
-
for (const [, entry] of ssrEntries) {
|
|
7563
|
-
if (!entry.path.startsWith(resolved)) continue;
|
|
7564
|
-
if (entry.path === changedPath) {
|
|
7565
|
-
matched.push(entry.path);
|
|
7566
|
-
} else {
|
|
7567
|
-
const ed = dirname2(entry.path);
|
|
7568
|
-
if (changedPath.startsWith(ed)) matched.push(entry.path);
|
|
7569
|
-
}
|
|
7570
|
-
}
|
|
7571
|
-
if (matched.length === 0) {
|
|
7572
|
-
for (const [, entry] of ssrEntries) {
|
|
7573
|
-
if (entry.path.startsWith(resolved)) matched.push(entry.path);
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
return matched;
|
|
7577
|
-
}
|
|
7578
8120
|
watcher.on("change", async (filePath) => {
|
|
7579
8121
|
if (/\.tsx?$/i.test(filePath)) {
|
|
7580
8122
|
if (filePath.endsWith("layout.tsx")) {
|
|
7581
8123
|
return broadcastReload();
|
|
7582
8124
|
}
|
|
7583
8125
|
clearCompileCache();
|
|
7584
|
-
|
|
7585
|
-
if (targets.length === 0) return broadcastReload();
|
|
8126
|
+
clearModuleCache();
|
|
7586
8127
|
try {
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
8128
|
+
await compileTsxDev(filePath);
|
|
8129
|
+
} catch (e) {
|
|
8130
|
+
console.error("server-side recompile failed:", e);
|
|
8131
|
+
return broadcastReload();
|
|
8132
|
+
}
|
|
8133
|
+
let css;
|
|
8134
|
+
const cssPath = join4(resolved, "app", "globals.css");
|
|
8135
|
+
if (existsSync5(cssPath)) {
|
|
8136
|
+
css = await compileTailwindCss(cssPath, resolved);
|
|
8137
|
+
}
|
|
8138
|
+
try {
|
|
8139
|
+
const absPath = resolve7(filePath);
|
|
8140
|
+
const { url, code } = await transformModule(absPath, resolved);
|
|
8141
|
+
const msg = { type: "update", url, code };
|
|
8142
|
+
if (css) msg.css = css;
|
|
8143
|
+
const str = JSON.stringify(msg);
|
|
8144
|
+
for (const ws of clients) {
|
|
8145
|
+
try {
|
|
8146
|
+
ws.send(str);
|
|
8147
|
+
} catch {
|
|
8148
|
+
clients.delete(ws);
|
|
7606
8149
|
}
|
|
7607
8150
|
}
|
|
7608
8151
|
} catch (e) {
|
|
7609
|
-
console.error("
|
|
8152
|
+
console.error("module transform failed for HMR:", e);
|
|
7610
8153
|
broadcastReload();
|
|
7611
8154
|
}
|
|
7612
8155
|
} else if (/\.css$/i.test(filePath)) {
|
|
7613
8156
|
const cssPath = join4(resolved, "app", "globals.css");
|
|
7614
|
-
if (
|
|
8157
|
+
if (existsSync5(cssPath)) {
|
|
7615
8158
|
const css = await compileTailwindCss(cssPath, resolved);
|
|
7616
8159
|
if (css) broadcastCss(css);
|
|
7617
8160
|
}
|
|
@@ -7699,7 +8242,7 @@ var isDev2 = isDev();
|
|
|
7699
8242
|
var als2 = new AsyncLocalStorage2();
|
|
7700
8243
|
__registerAls(() => als2.getStore());
|
|
7701
8244
|
function hashId(s) {
|
|
7702
|
-
return
|
|
8245
|
+
return createHash5("md5").update(s).digest("hex").slice(0, 8);
|
|
7703
8246
|
}
|
|
7704
8247
|
function serializeLoaderData(ctx) {
|
|
7705
8248
|
const ld = ctx.loaderData;
|
|
@@ -7774,7 +8317,7 @@ async function resolveRoute(ssrDir, segments, routeCache) {
|
|
|
7774
8317
|
return null;
|
|
7775
8318
|
}
|
|
7776
8319
|
const pageFile = join5(dir, "page.tsx");
|
|
7777
|
-
if (!
|
|
8320
|
+
if (!existsSync6(pageFile)) {
|
|
7778
8321
|
routeCache.set(cacheKey, null);
|
|
7779
8322
|
return null;
|
|
7780
8323
|
}
|
|
@@ -7785,28 +8328,28 @@ async function resolveRoute(ssrDir, segments, routeCache) {
|
|
|
7785
8328
|
let d = dir;
|
|
7786
8329
|
while (d.startsWith(appDir)) {
|
|
7787
8330
|
const lf = join5(d, "layout.tsx");
|
|
7788
|
-
if (
|
|
8331
|
+
if (existsSync6(lf)) layoutFiles.unshift(lf);
|
|
7789
8332
|
if (d === appDir) break;
|
|
7790
|
-
d =
|
|
8333
|
+
d = dirname4(d);
|
|
7791
8334
|
}
|
|
7792
8335
|
const errorFiles = [];
|
|
7793
8336
|
d = dir;
|
|
7794
8337
|
while (d.startsWith(appDir)) {
|
|
7795
8338
|
const ef = join5(d, "error.tsx");
|
|
7796
|
-
if (
|
|
8339
|
+
if (existsSync6(ef)) errorFiles.unshift(ef);
|
|
7797
8340
|
if (d === appDir) break;
|
|
7798
|
-
d =
|
|
8341
|
+
d = dirname4(d);
|
|
7799
8342
|
}
|
|
7800
8343
|
let notFoundFile = null;
|
|
7801
8344
|
d = dir;
|
|
7802
8345
|
while (d.startsWith(appDir)) {
|
|
7803
8346
|
const nf = join5(d, "not-found.tsx");
|
|
7804
|
-
if (
|
|
8347
|
+
if (existsSync6(nf)) {
|
|
7805
8348
|
notFoundFile = nf;
|
|
7806
8349
|
break;
|
|
7807
8350
|
}
|
|
7808
8351
|
if (d === appDir) break;
|
|
7809
|
-
d =
|
|
8352
|
+
d = dirname4(d);
|
|
7810
8353
|
}
|
|
7811
8354
|
const result = {
|
|
7812
8355
|
routePath: "/" + routeParams.join("/"),
|
|
@@ -7818,8 +8361,7 @@ async function resolveRoute(ssrDir, segments, routeCache) {
|
|
|
7818
8361
|
routeCache.set(cacheKey, result);
|
|
7819
8362
|
return result;
|
|
7820
8363
|
}
|
|
7821
|
-
function buildHydrationScript(
|
|
7822
|
-
const ssrPrefix = `${base}/__ssr`;
|
|
8364
|
+
function buildHydrationScript(pageUrl, ctxJson) {
|
|
7823
8365
|
return `
|
|
7824
8366
|
<script type="module">
|
|
7825
8367
|
import { setCtx, TsxContext } from 'weifuwu/react';
|
|
@@ -7832,10 +8374,11 @@ setCtx(_ctx);
|
|
|
7832
8374
|
const _root = document.getElementById('__weifuwu_root');
|
|
7833
8375
|
|
|
7834
8376
|
async function init() {
|
|
7835
|
-
const { default: Page } = await import('${
|
|
7836
|
-
const app = createElement(TsxContext.Provider, { value: _ctx },
|
|
7837
|
-
createElement(Page));
|
|
8377
|
+
const { default: Page } = await import('${pageUrl}');
|
|
7838
8378
|
${isDev2 ? `
|
|
8379
|
+
// Store page URL for __wfw runtime
|
|
8380
|
+
window.__WFW_PAGE_URL = '${pageUrl}';
|
|
8381
|
+
|
|
7839
8382
|
// Stable proxy \u2014 same function ref = React preserves fiber + useState state across HMR
|
|
7840
8383
|
const _pageImpl = { current: Page };
|
|
7841
8384
|
const _pageProxy = new Proxy(function __wfw_page(){}, {
|
|
@@ -7852,14 +8395,22 @@ async function init() {
|
|
|
7852
8395
|
}
|
|
7853
8396
|
renderPage();
|
|
7854
8397
|
|
|
8398
|
+
// HMR: re-render page (sub-modules resolved via __wfw.h() pick up changes)
|
|
8399
|
+
window.__WFW_RERENDER = () => {
|
|
8400
|
+
_tick++;
|
|
8401
|
+
reactRoot.render(createElement(TsxContext.Provider, { value: _ctx },
|
|
8402
|
+
createElement(_pageProxy, { __t: _tick })));
|
|
8403
|
+
};
|
|
8404
|
+
|
|
8405
|
+
// Fallback: swap entire page component
|
|
7855
8406
|
window.__WFW_REFRESH = async (NewComponent) => {
|
|
7856
8407
|
const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
|
|
7857
8408
|
_pageImpl.current = NewComponent;
|
|
7858
|
-
|
|
7859
|
-
reactRoot.render(createElement(TsxContext.Provider, { value: store },
|
|
7860
|
-
createElement(_pageProxy, { __t: _tick })));
|
|
8409
|
+
__WFW_RERENDER();
|
|
7861
8410
|
};
|
|
7862
8411
|
` : `
|
|
8412
|
+
const app = createElement(TsxContext.Provider, { value: _ctx },
|
|
8413
|
+
createElement(Page));
|
|
7863
8414
|
hydrateRoot(_root, app);
|
|
7864
8415
|
`}
|
|
7865
8416
|
}
|
|
@@ -7867,8 +8418,8 @@ async function init() {
|
|
|
7867
8418
|
init();
|
|
7868
8419
|
</script>`;
|
|
7869
8420
|
}
|
|
7870
|
-
function renderPage(pageFile,
|
|
7871
|
-
const absPath =
|
|
8421
|
+
function renderPage(pageFile, projectDir) {
|
|
8422
|
+
const absPath = resolve8(pageFile);
|
|
7872
8423
|
const entryId = hashId(absPath);
|
|
7873
8424
|
ssrEntries.set(entryId, { path: absPath });
|
|
7874
8425
|
return async (req, ctx) => {
|
|
@@ -7897,9 +8448,10 @@ function renderPage(pageFile, outDir) {
|
|
|
7897
8448
|
loaderData,
|
|
7898
8449
|
env: ctx.env ?? {}
|
|
7899
8450
|
};
|
|
8451
|
+
const pageRelative = relative3(projectDir, absPath);
|
|
8452
|
+
const pageUrl = `${base}/__wfw/m/${pageRelative}`;
|
|
7900
8453
|
return als2.run(ctxValue, async () => {
|
|
7901
8454
|
setCtx(ctxValue);
|
|
7902
|
-
await compileBrowser(absPath, outDir);
|
|
7903
8455
|
let element = createElement3(
|
|
7904
8456
|
"div",
|
|
7905
8457
|
{ id: "__weifuwu_root" },
|
|
@@ -7917,7 +8469,7 @@ function renderPage(pageFile, outDir) {
|
|
|
7917
8469
|
loaderData,
|
|
7918
8470
|
tailwind: ctx.tailwind
|
|
7919
8471
|
},
|
|
7920
|
-
buildHydrationScript(
|
|
8472
|
+
buildHydrationScript(pageUrl, JSON.stringify(ctxValue))
|
|
7921
8473
|
);
|
|
7922
8474
|
});
|
|
7923
8475
|
};
|
|
@@ -7932,7 +8484,7 @@ function runChain(mws, handler, req, ctx) {
|
|
|
7932
8484
|
}
|
|
7933
8485
|
function discoverRoutes(dir) {
|
|
7934
8486
|
const appDir = join5(dir, "app");
|
|
7935
|
-
if (!
|
|
8487
|
+
if (!existsSync6(appDir)) return [];
|
|
7936
8488
|
const result = [];
|
|
7937
8489
|
function walk(currentDir, routePath) {
|
|
7938
8490
|
let entries;
|
|
@@ -7953,7 +8505,7 @@ function discoverRoutes(dir) {
|
|
|
7953
8505
|
} else if (entry.name === "page.tsx") {
|
|
7954
8506
|
result.push({
|
|
7955
8507
|
path: routePath || "/",
|
|
7956
|
-
file:
|
|
8508
|
+
file: relative3(appDir, join5(currentDir, entry.name))
|
|
7957
8509
|
});
|
|
7958
8510
|
}
|
|
7959
8511
|
}
|
|
@@ -7963,28 +8515,19 @@ function discoverRoutes(dir) {
|
|
|
7963
8515
|
}
|
|
7964
8516
|
function ssr(opts) {
|
|
7965
8517
|
const r = new Router();
|
|
7966
|
-
const dir =
|
|
7967
|
-
const outDir = resolve6(OUT_DIR);
|
|
8518
|
+
const dir = resolve8(opts.dir);
|
|
7968
8519
|
const routeCache = /* @__PURE__ */ new Map();
|
|
8520
|
+
const wfwRoot = resolve8(import.meta.dirname ?? __dirname);
|
|
8521
|
+
r.use("/", moduleServer({ root: [dir, wfwRoot] }));
|
|
7969
8522
|
compileVendorBundle().catch(() => {
|
|
7970
8523
|
});
|
|
7971
|
-
r.get("/__ssr/:file", (req, ctx) => {
|
|
7972
|
-
const filePath = join5(outDir, ctx.params.file);
|
|
7973
|
-
if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
|
|
7974
|
-
return new Response("Not Found", { status: 404 });
|
|
7975
|
-
}
|
|
7976
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
7977
|
-
return new Response(content, {
|
|
7978
|
-
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7979
|
-
});
|
|
7980
|
-
});
|
|
7981
8524
|
r.get("/__wfw/v/bundle", async () => {
|
|
7982
8525
|
const code = await compileVendorBundle();
|
|
7983
8526
|
return new Response(code, {
|
|
7984
8527
|
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
7985
8528
|
});
|
|
7986
8529
|
});
|
|
7987
|
-
if (
|
|
8530
|
+
if (existsSync6(join5(dir, "app", "globals.css"))) {
|
|
7988
8531
|
r.use("/", tailwindRouter(dir));
|
|
7989
8532
|
}
|
|
7990
8533
|
let devWatcher;
|
|
@@ -8020,7 +8563,7 @@ function ssr(opts) {
|
|
|
8020
8563
|
...resolved.layoutFiles.map((f) => layout(f)),
|
|
8021
8564
|
tailwindContext(dir)
|
|
8022
8565
|
];
|
|
8023
|
-
const handler = (req2, ctx2) => renderPage(resolved.pageFile,
|
|
8566
|
+
const handler = (req2, ctx2) => renderPage(resolved.pageFile, dir)(req2, ctx2);
|
|
8024
8567
|
return runChain(mws, handler, req, ctx);
|
|
8025
8568
|
});
|
|
8026
8569
|
const mod = r;
|
|
@@ -8272,13 +8815,13 @@ function createBashTool(ctx) {
|
|
|
8272
8815
|
return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
|
|
8273
8816
|
}
|
|
8274
8817
|
const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
|
|
8275
|
-
return new Promise((
|
|
8818
|
+
return new Promise((resolve16) => {
|
|
8276
8819
|
const child = exec(
|
|
8277
8820
|
command,
|
|
8278
8821
|
{ cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 },
|
|
8279
8822
|
(error, stdout, stderr) => {
|
|
8280
8823
|
const truncated = stdout.length > 1e6 || stderr.length > 1e6;
|
|
8281
|
-
|
|
8824
|
+
resolve16({
|
|
8282
8825
|
stdout: stdout.slice(0, 1e6),
|
|
8283
8826
|
stderr: stderr.slice(0, 1e6),
|
|
8284
8827
|
exitCode: error?.code ?? 0,
|
|
@@ -8295,8 +8838,8 @@ function createBashTool(ctx) {
|
|
|
8295
8838
|
// opencode/tools/read.ts
|
|
8296
8839
|
import { tool as tool4 } from "ai";
|
|
8297
8840
|
import { z as z6 } from "zod";
|
|
8298
|
-
import { readFileSync as
|
|
8299
|
-
import { resolve as
|
|
8841
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
8842
|
+
import { resolve as resolve9 } from "node:path";
|
|
8300
8843
|
function createReadTool(ctx) {
|
|
8301
8844
|
return tool4({
|
|
8302
8845
|
description: "Read file contents. Supports offset and limit for reading specific line ranges.",
|
|
@@ -8306,11 +8849,11 @@ function createReadTool(ctx) {
|
|
|
8306
8849
|
limit: z6.number().optional().describe("Number of lines to read")
|
|
8307
8850
|
}),
|
|
8308
8851
|
execute: async ({ path: path2, offset, limit }) => {
|
|
8309
|
-
const resolved =
|
|
8852
|
+
const resolved = resolve9(ctx.workspace, path2);
|
|
8310
8853
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
8311
8854
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
8312
8855
|
}
|
|
8313
|
-
const content =
|
|
8856
|
+
const content = readFileSync7(resolved, "utf-8");
|
|
8314
8857
|
const lines = content.split("\n");
|
|
8315
8858
|
const totalLines = lines.length;
|
|
8316
8859
|
if (offset !== void 0) {
|
|
@@ -8338,7 +8881,7 @@ function createReadTool(ctx) {
|
|
|
8338
8881
|
import { tool as tool5 } from "ai";
|
|
8339
8882
|
import { z as z7 } from "zod";
|
|
8340
8883
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
|
|
8341
|
-
import { resolve as
|
|
8884
|
+
import { resolve as resolve10, dirname as dirname5 } from "node:path";
|
|
8342
8885
|
function createWriteTool(ctx) {
|
|
8343
8886
|
return tool5({
|
|
8344
8887
|
description: "Create or overwrite a file. Parent directories are created automatically.",
|
|
@@ -8347,11 +8890,11 @@ function createWriteTool(ctx) {
|
|
|
8347
8890
|
content: z7.string().describe("File content")
|
|
8348
8891
|
}),
|
|
8349
8892
|
execute: async ({ path: path2, content }) => {
|
|
8350
|
-
const resolved =
|
|
8893
|
+
const resolved = resolve10(ctx.workspace, path2);
|
|
8351
8894
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
8352
8895
|
return { error: "Path not allowed" };
|
|
8353
8896
|
}
|
|
8354
|
-
mkdirSync3(
|
|
8897
|
+
mkdirSync3(dirname5(resolved), { recursive: true });
|
|
8355
8898
|
writeFileSync2(resolved, content, "utf-8");
|
|
8356
8899
|
return { path: path2, size: content.length };
|
|
8357
8900
|
}
|
|
@@ -8361,8 +8904,8 @@ function createWriteTool(ctx) {
|
|
|
8361
8904
|
// opencode/tools/edit.ts
|
|
8362
8905
|
import { tool as tool6 } from "ai";
|
|
8363
8906
|
import { z as z8 } from "zod";
|
|
8364
|
-
import { readFileSync as
|
|
8365
|
-
import { resolve as
|
|
8907
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8908
|
+
import { resolve as resolve11 } from "node:path";
|
|
8366
8909
|
function createEditTool(ctx) {
|
|
8367
8910
|
return tool6({
|
|
8368
8911
|
description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
|
|
@@ -8373,11 +8916,11 @@ function createEditTool(ctx) {
|
|
|
8373
8916
|
replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
|
|
8374
8917
|
}),
|
|
8375
8918
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
8376
|
-
const resolved =
|
|
8919
|
+
const resolved = resolve11(ctx.workspace, path2);
|
|
8377
8920
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
|
|
8378
8921
|
return { error: "Path not allowed" };
|
|
8379
8922
|
}
|
|
8380
|
-
const content =
|
|
8923
|
+
const content = readFileSync8(resolved, "utf-8");
|
|
8381
8924
|
if (replaceAll) {
|
|
8382
8925
|
if (!content.includes(oldString)) {
|
|
8383
8926
|
return { error: "oldString not found in file", replaced: 0 };
|
|
@@ -8409,8 +8952,8 @@ function createEditTool(ctx) {
|
|
|
8409
8952
|
import { tool as tool7 } from "ai";
|
|
8410
8953
|
import { z as z9 } from "zod";
|
|
8411
8954
|
import { execFileSync } from "node:child_process";
|
|
8412
|
-
import { resolve as
|
|
8413
|
-
import { existsSync as
|
|
8955
|
+
import { resolve as resolve12 } from "node:path";
|
|
8956
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
8414
8957
|
function createGrepTool(ctx) {
|
|
8415
8958
|
return tool7({
|
|
8416
8959
|
description: "Search file contents using regex. Supports file type filtering and context lines.",
|
|
@@ -8421,10 +8964,10 @@ function createGrepTool(ctx) {
|
|
|
8421
8964
|
context: z9.number().default(0).describe("Number of context lines before and after each match")
|
|
8422
8965
|
}),
|
|
8423
8966
|
execute: async ({ pattern, include, path: path2, context }) => {
|
|
8424
|
-
const searchDir = path2 ?
|
|
8967
|
+
const searchDir = path2 ? resolve12(ctx.workspace, path2) : ctx.workspace;
|
|
8425
8968
|
try {
|
|
8426
8969
|
let stdout;
|
|
8427
|
-
if (
|
|
8970
|
+
if (existsSync7("/usr/bin/rg") || existsSync7("/usr/local/bin/rg")) {
|
|
8428
8971
|
const args = ["-n"];
|
|
8429
8972
|
if (context > 0) args.push("-C", String(context));
|
|
8430
8973
|
if (include) args.push("-g", include);
|
|
@@ -8457,7 +9000,7 @@ function createGrepTool(ctx) {
|
|
|
8457
9000
|
import { tool as tool8 } from "ai";
|
|
8458
9001
|
import { z as z10 } from "zod";
|
|
8459
9002
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
8460
|
-
import { resolve as
|
|
9003
|
+
import { resolve as resolve13 } from "node:path";
|
|
8461
9004
|
function createGlobTool(ctx) {
|
|
8462
9005
|
return tool8({
|
|
8463
9006
|
description: "Find files matching a glob pattern.",
|
|
@@ -8466,7 +9009,7 @@ function createGlobTool(ctx) {
|
|
|
8466
9009
|
path: z10.string().optional().describe("Subdirectory relative to workspace")
|
|
8467
9010
|
}),
|
|
8468
9011
|
execute: async ({ pattern, path: path2 }) => {
|
|
8469
|
-
const searchDir = path2 ?
|
|
9012
|
+
const searchDir = path2 ? resolve13(ctx.workspace, path2) : ctx.workspace;
|
|
8470
9013
|
try {
|
|
8471
9014
|
const stdout = execFileSync2(
|
|
8472
9015
|
"find",
|
|
@@ -8492,7 +9035,7 @@ function createGlobTool(ctx) {
|
|
|
8492
9035
|
// opencode/tools/web.ts
|
|
8493
9036
|
import { tool as tool9 } from "ai";
|
|
8494
9037
|
import { z as z11 } from "zod";
|
|
8495
|
-
function createWebTool(
|
|
9038
|
+
function createWebTool(_ctx2) {
|
|
8496
9039
|
return tool9({
|
|
8497
9040
|
description: "Fetch a URL and return the content as text.",
|
|
8498
9041
|
inputSchema: z11.object({
|
|
@@ -8528,7 +9071,7 @@ function createQuestionTool(ctx) {
|
|
|
8528
9071
|
options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
|
|
8529
9072
|
}),
|
|
8530
9073
|
execute: async ({ question, options }, { toolCallId }) => {
|
|
8531
|
-
return new Promise((
|
|
9074
|
+
return new Promise((resolve16, reject) => {
|
|
8532
9075
|
const timeout = setTimeout(() => {
|
|
8533
9076
|
ctx.pendingQuestions.delete(toolCallId);
|
|
8534
9077
|
reject(new Error("Question timed out"));
|
|
@@ -8536,7 +9079,7 @@ function createQuestionTool(ctx) {
|
|
|
8536
9079
|
ctx.pendingQuestions.set(toolCallId, {
|
|
8537
9080
|
resolve: (answer) => {
|
|
8538
9081
|
clearTimeout(timeout);
|
|
8539
|
-
|
|
9082
|
+
resolve16(answer);
|
|
8540
9083
|
},
|
|
8541
9084
|
reject: (err) => {
|
|
8542
9085
|
clearTimeout(timeout);
|
|
@@ -8857,7 +9400,7 @@ function createWSHandler2(deps) {
|
|
|
8857
9400
|
clients2.delete(ws);
|
|
8858
9401
|
}
|
|
8859
9402
|
},
|
|
8860
|
-
error(ws,
|
|
9403
|
+
error(ws, _ctx2, _err) {
|
|
8861
9404
|
const client = clients2.get(ws);
|
|
8862
9405
|
if (client) {
|
|
8863
9406
|
client.abortController?.abort();
|
|
@@ -8870,7 +9413,7 @@ function createWSHandler2(deps) {
|
|
|
8870
9413
|
// opencode/skills.ts
|
|
8871
9414
|
import { readFile, glob } from "node:fs/promises";
|
|
8872
9415
|
import { homedir } from "node:os";
|
|
8873
|
-
import { resolve as
|
|
9416
|
+
import { resolve as resolve14 } from "node:path";
|
|
8874
9417
|
import { parse as parseYaml } from "yaml";
|
|
8875
9418
|
var SEARCH_DIRS = [
|
|
8876
9419
|
(ws) => `${ws}/.opencode/skills`,
|
|
@@ -8908,7 +9451,7 @@ async function scanDir(dir) {
|
|
|
8908
9451
|
try {
|
|
8909
9452
|
const files = [];
|
|
8910
9453
|
for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
|
|
8911
|
-
const skill = await parseSkillFile(
|
|
9454
|
+
const skill = await parseSkillFile(resolve14(dir, entry));
|
|
8912
9455
|
if (skill) files.push(skill);
|
|
8913
9456
|
}
|
|
8914
9457
|
return files;
|
|
@@ -9335,7 +9878,7 @@ function theme(options) {
|
|
|
9335
9878
|
|
|
9336
9879
|
// i18n.ts
|
|
9337
9880
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
9338
|
-
import { join as join7, resolve as
|
|
9881
|
+
import { join as join7, resolve as resolve15 } from "node:path";
|
|
9339
9882
|
var DEFAULTS2 = {
|
|
9340
9883
|
default: "en",
|
|
9341
9884
|
cookie: "locale",
|
|
@@ -9353,7 +9896,7 @@ function translate(msgs, key, params, fallback) {
|
|
|
9353
9896
|
}
|
|
9354
9897
|
function i18n(options) {
|
|
9355
9898
|
const opts = { ...DEFAULTS2, ...options };
|
|
9356
|
-
const dir = opts.dir ?
|
|
9899
|
+
const dir = opts.dir ? resolve15(opts.dir) : void 0;
|
|
9357
9900
|
const cache3 = /* @__PURE__ */ new Map();
|
|
9358
9901
|
function validLocale(locale) {
|
|
9359
9902
|
return /^[\w-]+$/.test(locale) && !locale.includes("..");
|
|
@@ -9863,381 +10406,6 @@ function logdb(options) {
|
|
|
9863
10406
|
// iii/client.ts
|
|
9864
10407
|
import crypto8 from "node:crypto";
|
|
9865
10408
|
|
|
9866
|
-
// iii/stream.ts
|
|
9867
|
-
function notify(channels, stream, group, item, event, data) {
|
|
9868
|
-
const keys = [`${stream}`, `${stream}:${group}`, `${stream}:${group}:${item}`];
|
|
9869
|
-
const msg = JSON.stringify({
|
|
9870
|
-
type: "stream",
|
|
9871
|
-
stream_name: stream,
|
|
9872
|
-
group_id: group,
|
|
9873
|
-
item_id: item,
|
|
9874
|
-
event,
|
|
9875
|
-
data
|
|
9876
|
-
});
|
|
9877
|
-
for (const key of keys) {
|
|
9878
|
-
const subs = channels.get(key);
|
|
9879
|
-
if (!subs) continue;
|
|
9880
|
-
for (const ws of subs) {
|
|
9881
|
-
try {
|
|
9882
|
-
ws.send(msg);
|
|
9883
|
-
} catch {
|
|
9884
|
-
}
|
|
9885
|
-
}
|
|
9886
|
-
}
|
|
9887
|
-
}
|
|
9888
|
-
function deepClone(v) {
|
|
9889
|
-
return JSON.parse(JSON.stringify(v));
|
|
9890
|
-
}
|
|
9891
|
-
function applyOps(value, ops) {
|
|
9892
|
-
let current = deepClone(value ?? {});
|
|
9893
|
-
for (const op2 of ops) {
|
|
9894
|
-
switch (op2.op) {
|
|
9895
|
-
case "set":
|
|
9896
|
-
current = deepClone(op2.value);
|
|
9897
|
-
break;
|
|
9898
|
-
case "merge":
|
|
9899
|
-
if (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
9900
|
-
current = { ...current, ...deepClone(op2.value) };
|
|
9901
|
-
} else {
|
|
9902
|
-
current = deepClone(op2.value);
|
|
9903
|
-
}
|
|
9904
|
-
break;
|
|
9905
|
-
case "increment":
|
|
9906
|
-
current = (typeof current === "number" ? current : 0) + op2.value;
|
|
9907
|
-
break;
|
|
9908
|
-
case "decrement":
|
|
9909
|
-
current = (typeof current === "number" ? current : 0) - op2.value;
|
|
9910
|
-
break;
|
|
9911
|
-
case "append":
|
|
9912
|
-
if (!Array.isArray(current)) current = [];
|
|
9913
|
-
current.push(deepClone(op2.value));
|
|
9914
|
-
break;
|
|
9915
|
-
case "remove":
|
|
9916
|
-
current = null;
|
|
9917
|
-
break;
|
|
9918
|
-
}
|
|
9919
|
-
}
|
|
9920
|
-
return current;
|
|
9921
|
-
}
|
|
9922
|
-
function createMemoryStore(channels) {
|
|
9923
|
-
const store2 = /* @__PURE__ */ new Map();
|
|
9924
|
-
function key(stream, group, item) {
|
|
9925
|
-
return `${stream}:${group}:${item}`;
|
|
9926
|
-
}
|
|
9927
|
-
return {
|
|
9928
|
-
async set(stream, group, item, data) {
|
|
9929
|
-
const k = key(stream, group, item);
|
|
9930
|
-
const old = store2.get(k) ?? null;
|
|
9931
|
-
store2.set(k, deepClone(data));
|
|
9932
|
-
notify(channels, stream, group, item, "set", data);
|
|
9933
|
-
return { old_value: old, new_value: deepClone(data) };
|
|
9934
|
-
},
|
|
9935
|
-
async get(stream, group, item) {
|
|
9936
|
-
const v = store2.get(key(stream, group, item)) ?? null;
|
|
9937
|
-
return { value: deepClone(v) };
|
|
9938
|
-
},
|
|
9939
|
-
async delete(stream, group, item) {
|
|
9940
|
-
const k = key(stream, group, item);
|
|
9941
|
-
const old = store2.get(k) ?? null;
|
|
9942
|
-
store2.delete(k);
|
|
9943
|
-
notify(channels, stream, group, item, "delete", null);
|
|
9944
|
-
return { old_value: old };
|
|
9945
|
-
},
|
|
9946
|
-
async list(stream, group) {
|
|
9947
|
-
const items = [];
|
|
9948
|
-
const prefix = `${stream}:${group}:`;
|
|
9949
|
-
for (const [k, v] of store2) {
|
|
9950
|
-
if (k.startsWith(prefix) && !k.slice(prefix.length).includes(":")) {
|
|
9951
|
-
items.push({ item_id: k.slice(prefix.length), data: deepClone(v) });
|
|
9952
|
-
}
|
|
9953
|
-
}
|
|
9954
|
-
return { items };
|
|
9955
|
-
},
|
|
9956
|
-
async list_groups(stream) {
|
|
9957
|
-
const groups = /* @__PURE__ */ new Set();
|
|
9958
|
-
const prefix = `${stream}:`;
|
|
9959
|
-
for (const k of store2.keys()) {
|
|
9960
|
-
if (k.startsWith(prefix)) {
|
|
9961
|
-
const rest = k.slice(prefix.length);
|
|
9962
|
-
const g = rest.split(":")[0];
|
|
9963
|
-
if (g) groups.add(g);
|
|
9964
|
-
}
|
|
9965
|
-
}
|
|
9966
|
-
return { groups: Array.from(groups) };
|
|
9967
|
-
},
|
|
9968
|
-
async list_all() {
|
|
9969
|
-
const streamMap = /* @__PURE__ */ new Map();
|
|
9970
|
-
for (const k of store2.keys()) {
|
|
9971
|
-
const parts = k.split(":");
|
|
9972
|
-
const s = parts[0];
|
|
9973
|
-
const g = parts[1];
|
|
9974
|
-
if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: /* @__PURE__ */ new Set() });
|
|
9975
|
-
const entry = streamMap.get(s);
|
|
9976
|
-
if (g) entry.groups.add(g);
|
|
9977
|
-
entry.items.add(k);
|
|
9978
|
-
}
|
|
9979
|
-
const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
|
|
9980
|
-
stream_name: name,
|
|
9981
|
-
group_count: info.groups.size,
|
|
9982
|
-
item_count: info.items.size
|
|
9983
|
-
}));
|
|
9984
|
-
return { streams, count: streams.length };
|
|
9985
|
-
},
|
|
9986
|
-
async send(stream, group, type, data, id2) {
|
|
9987
|
-
notify(channels, stream, group, id2 ?? "", "send", { type, data });
|
|
9988
|
-
},
|
|
9989
|
-
async update(stream, group, item, ops) {
|
|
9990
|
-
const k = key(stream, group, item);
|
|
9991
|
-
const old = deepClone(store2.get(k) ?? null);
|
|
9992
|
-
const newVal = applyOps(old, ops);
|
|
9993
|
-
store2.set(k, deepClone(newVal));
|
|
9994
|
-
notify(channels, stream, group, item, "update", newVal);
|
|
9995
|
-
return { old_value: old, new_value: deepClone(newVal) };
|
|
9996
|
-
}
|
|
9997
|
-
};
|
|
9998
|
-
}
|
|
9999
|
-
function createPgStore(channels, pg) {
|
|
10000
|
-
const sql2 = pg.sql;
|
|
10001
|
-
return {
|
|
10002
|
-
async set(stream, group, item, data) {
|
|
10003
|
-
const rows = await sql2`
|
|
10004
|
-
INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
|
|
10005
|
-
VALUES (${stream}, ${group}, ${item}, ${data})
|
|
10006
|
-
ON CONFLICT (stream_name, group_id, item_id)
|
|
10007
|
-
DO UPDATE SET data = ${data}, updated_at = NOW()
|
|
10008
|
-
RETURNING data
|
|
10009
|
-
`;
|
|
10010
|
-
notify(channels, stream, group, item, "set", data);
|
|
10011
|
-
return { old_value: null, new_value: data };
|
|
10012
|
-
},
|
|
10013
|
-
async get(stream, group, item) {
|
|
10014
|
-
const rows = await sql2`
|
|
10015
|
-
SELECT data FROM "_iii_stream"
|
|
10016
|
-
WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
|
|
10017
|
-
`;
|
|
10018
|
-
const row = rows[0];
|
|
10019
|
-
let value = row?.data ?? null;
|
|
10020
|
-
if (typeof value === "string") value = JSON.parse(value);
|
|
10021
|
-
return { value };
|
|
10022
|
-
},
|
|
10023
|
-
async delete(stream, group, item) {
|
|
10024
|
-
const rows = await sql2`
|
|
10025
|
-
DELETE FROM "_iii_stream"
|
|
10026
|
-
WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
|
|
10027
|
-
RETURNING data
|
|
10028
|
-
`;
|
|
10029
|
-
const old = rows[0]?.data ?? null;
|
|
10030
|
-
notify(channels, stream, group, item, "delete", null);
|
|
10031
|
-
return { old_value: old };
|
|
10032
|
-
},
|
|
10033
|
-
async list(stream, group) {
|
|
10034
|
-
const rows = await sql2`
|
|
10035
|
-
SELECT item_id, data FROM "_iii_stream"
|
|
10036
|
-
WHERE stream_name = ${stream} AND group_id = ${group}
|
|
10037
|
-
ORDER BY item_id
|
|
10038
|
-
`;
|
|
10039
|
-
const items = rows.map((r) => ({
|
|
10040
|
-
item_id: r.item_id,
|
|
10041
|
-
data: typeof r.data === "string" ? JSON.parse(r.data) : r.data
|
|
10042
|
-
}));
|
|
10043
|
-
return { items };
|
|
10044
|
-
},
|
|
10045
|
-
async list_groups(stream) {
|
|
10046
|
-
const rows = await sql2`
|
|
10047
|
-
SELECT DISTINCT group_id FROM "_iii_stream"
|
|
10048
|
-
WHERE stream_name = ${stream}
|
|
10049
|
-
ORDER BY group_id
|
|
10050
|
-
`;
|
|
10051
|
-
return { groups: rows.map((r) => r.group_id) };
|
|
10052
|
-
},
|
|
10053
|
-
async list_all() {
|
|
10054
|
-
const rows = await sql2`
|
|
10055
|
-
SELECT stream_name, COUNT(DISTINCT group_id) as group_count, COUNT(*) as item_count
|
|
10056
|
-
FROM "_iii_stream"
|
|
10057
|
-
GROUP BY stream_name
|
|
10058
|
-
ORDER BY stream_name
|
|
10059
|
-
`;
|
|
10060
|
-
const streams = rows.map((r) => ({
|
|
10061
|
-
stream_name: r.stream_name,
|
|
10062
|
-
group_count: Number(r.group_count),
|
|
10063
|
-
item_count: Number(r.item_count)
|
|
10064
|
-
}));
|
|
10065
|
-
return { streams, count: streams.length };
|
|
10066
|
-
},
|
|
10067
|
-
async send(stream, group, type, data, id2) {
|
|
10068
|
-
notify(channels, stream, group, id2 ?? "", "send", { type, data });
|
|
10069
|
-
},
|
|
10070
|
-
async update(stream, group, item, ops) {
|
|
10071
|
-
const { value: oldVal } = await this.get(stream, group, item);
|
|
10072
|
-
const newVal = applyOps(oldVal, ops);
|
|
10073
|
-
await sql2`
|
|
10074
|
-
INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
|
|
10075
|
-
VALUES (${stream}, ${group}, ${item}, ${newVal})
|
|
10076
|
-
ON CONFLICT (stream_name, group_id, item_id)
|
|
10077
|
-
DO UPDATE SET data = ${newVal}, updated_at = NOW()
|
|
10078
|
-
`;
|
|
10079
|
-
notify(channels, stream, group, item, "update", newVal);
|
|
10080
|
-
return { old_value: oldVal, new_value: deepClone(newVal) };
|
|
10081
|
-
}
|
|
10082
|
-
};
|
|
10083
|
-
}
|
|
10084
|
-
function createRedisStore(channels, redis2, ttl) {
|
|
10085
|
-
function hashKey(stream, group) {
|
|
10086
|
-
return `iii:stream:${stream}:${group}`;
|
|
10087
|
-
}
|
|
10088
|
-
function setTTL(hk) {
|
|
10089
|
-
if (ttl) redis2.expire(hk, ttl);
|
|
10090
|
-
}
|
|
10091
|
-
return {
|
|
10092
|
-
async set(stream, group, item, data) {
|
|
10093
|
-
const hk = hashKey(stream, group);
|
|
10094
|
-
const oldRaw = await redis2.hget(hk, item);
|
|
10095
|
-
const old = oldRaw ? JSON.parse(oldRaw) : null;
|
|
10096
|
-
await redis2.hset(hk, item, JSON.stringify(data));
|
|
10097
|
-
setTTL(hk);
|
|
10098
|
-
await redis2.publish(
|
|
10099
|
-
`iii:stream:${stream}`,
|
|
10100
|
-
JSON.stringify({ event: "set", group, item, data })
|
|
10101
|
-
);
|
|
10102
|
-
notify(channels, stream, group, item, "set", data);
|
|
10103
|
-
return { old_value: old, new_value: deepClone(data) };
|
|
10104
|
-
},
|
|
10105
|
-
async get(stream, group, item) {
|
|
10106
|
-
const raw = await redis2.hget(hashKey(stream, group), item);
|
|
10107
|
-
return { value: raw ? JSON.parse(raw) : null };
|
|
10108
|
-
},
|
|
10109
|
-
async delete(stream, group, item) {
|
|
10110
|
-
const hk = hashKey(stream, group);
|
|
10111
|
-
const oldRaw = await redis2.hget(hk, item);
|
|
10112
|
-
const old = oldRaw ? JSON.parse(oldRaw) : null;
|
|
10113
|
-
await redis2.hdel(hk, item);
|
|
10114
|
-
const remaining = await redis2.hlen(hk);
|
|
10115
|
-
if (remaining === 0) await redis2.del(hk);
|
|
10116
|
-
await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "delete", group, item }));
|
|
10117
|
-
notify(channels, stream, group, item, "delete", null);
|
|
10118
|
-
return { old_value: old };
|
|
10119
|
-
},
|
|
10120
|
-
async list(stream, group) {
|
|
10121
|
-
const raw = await redis2.hgetall(hashKey(stream, group));
|
|
10122
|
-
const items = Object.entries(raw).map(([item_id, data]) => ({
|
|
10123
|
-
item_id,
|
|
10124
|
-
data: JSON.parse(data)
|
|
10125
|
-
}));
|
|
10126
|
-
return { items };
|
|
10127
|
-
},
|
|
10128
|
-
async list_groups(stream) {
|
|
10129
|
-
const pattern = `iii:stream:${stream}:*`;
|
|
10130
|
-
let cursor = "0";
|
|
10131
|
-
const groups = /* @__PURE__ */ new Set();
|
|
10132
|
-
do {
|
|
10133
|
-
const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
|
|
10134
|
-
cursor = next;
|
|
10135
|
-
for (const k of keys) {
|
|
10136
|
-
const parts = k.split(":");
|
|
10137
|
-
const g = parts.slice(3).join(":");
|
|
10138
|
-
if (g) groups.add(g);
|
|
10139
|
-
}
|
|
10140
|
-
} while (cursor !== "0");
|
|
10141
|
-
return { groups: Array.from(groups) };
|
|
10142
|
-
},
|
|
10143
|
-
async list_all() {
|
|
10144
|
-
const pattern = "iii:stream:*";
|
|
10145
|
-
let cursor = "0";
|
|
10146
|
-
const streamMap = /* @__PURE__ */ new Map();
|
|
10147
|
-
do {
|
|
10148
|
-
const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
|
|
10149
|
-
cursor = next;
|
|
10150
|
-
for (const k of keys) {
|
|
10151
|
-
const parts = k.split(":");
|
|
10152
|
-
const s = parts[2];
|
|
10153
|
-
const g = parts.slice(3).join(":");
|
|
10154
|
-
if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: 0 });
|
|
10155
|
-
const entry = streamMap.get(s);
|
|
10156
|
-
if (g) entry.groups.add(g);
|
|
10157
|
-
entry.items++;
|
|
10158
|
-
}
|
|
10159
|
-
} while (cursor !== "0");
|
|
10160
|
-
const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
|
|
10161
|
-
stream_name: name,
|
|
10162
|
-
group_count: info.groups.size,
|
|
10163
|
-
item_count: info.items
|
|
10164
|
-
}));
|
|
10165
|
-
return { streams, count: streams.length };
|
|
10166
|
-
},
|
|
10167
|
-
async send(stream, group, type, data, id2) {
|
|
10168
|
-
notify(channels, stream, group, id2 ?? "", "send", { type, data });
|
|
10169
|
-
},
|
|
10170
|
-
async update(stream, group, item, ops) {
|
|
10171
|
-
const hk = hashKey(stream, group);
|
|
10172
|
-
const oldRaw = await redis2.hget(hk, item);
|
|
10173
|
-
const old = oldRaw ? JSON.parse(oldRaw) : null;
|
|
10174
|
-
const newVal = applyOps(old, ops);
|
|
10175
|
-
await redis2.hset(hk, item, JSON.stringify(newVal));
|
|
10176
|
-
setTTL(hk);
|
|
10177
|
-
await redis2.publish(
|
|
10178
|
-
`iii:stream:${stream}`,
|
|
10179
|
-
JSON.stringify({ event: "update", group, item, data: newVal })
|
|
10180
|
-
);
|
|
10181
|
-
notify(channels, stream, group, item, "update", newVal);
|
|
10182
|
-
return { old_value: old, new_value: deepClone(newVal) };
|
|
10183
|
-
}
|
|
10184
|
-
};
|
|
10185
|
-
}
|
|
10186
|
-
function createStream(opts) {
|
|
10187
|
-
const channels = /* @__PURE__ */ new Map();
|
|
10188
|
-
const store2 = opts?.pg ? createPgStore(channels, opts.pg) : opts?.redis ? createRedisStore(channels, opts.redis, opts.streamTTL ?? 3600) : createMemoryStore(channels);
|
|
10189
|
-
let redisSub = null;
|
|
10190
|
-
if (opts?.redis) {
|
|
10191
|
-
redisSub = opts.redis.duplicate();
|
|
10192
|
-
redisSub.on("message", (rawChannel, rawData) => {
|
|
10193
|
-
if (!rawChannel.startsWith("iii:stream:")) return;
|
|
10194
|
-
const stream = rawChannel.slice("iii:stream:".length);
|
|
10195
|
-
try {
|
|
10196
|
-
const msg = JSON.parse(rawData);
|
|
10197
|
-
if (msg.event === "set" || msg.event === "update") {
|
|
10198
|
-
notify(channels, stream, msg.group, msg.item, msg.event, msg.data);
|
|
10199
|
-
} else if (msg.event === "delete") {
|
|
10200
|
-
notify(channels, stream, msg.group, msg.item, "delete", null);
|
|
10201
|
-
}
|
|
10202
|
-
} catch {
|
|
10203
|
-
}
|
|
10204
|
-
});
|
|
10205
|
-
}
|
|
10206
|
-
return {
|
|
10207
|
-
...store2,
|
|
10208
|
-
subscribe(ws, sub) {
|
|
10209
|
-
const key = sub.item_id ? `${sub.stream_name}:${sub.group_id}:${sub.item_id}` : sub.group_id ? `${sub.stream_name}:${sub.group_id}` : sub.stream_name;
|
|
10210
|
-
if (!channels.has(key)) channels.set(key, /* @__PURE__ */ new Set());
|
|
10211
|
-
channels.get(key).add(ws);
|
|
10212
|
-
if (redisSub && sub.stream_name) {
|
|
10213
|
-
redisSub.subscribe(`iii:stream:${sub.stream_name}`);
|
|
10214
|
-
}
|
|
10215
|
-
},
|
|
10216
|
-
unsubscribe(ws) {
|
|
10217
|
-
for (const [, subs] of channels) subs.delete(ws);
|
|
10218
|
-
},
|
|
10219
|
-
async migrate() {
|
|
10220
|
-
if (opts?.pg) {
|
|
10221
|
-
const sql2 = opts.pg.sql;
|
|
10222
|
-
await sql2`
|
|
10223
|
-
CREATE TABLE IF NOT EXISTS "_iii_stream" (
|
|
10224
|
-
stream_name TEXT NOT NULL,
|
|
10225
|
-
group_id TEXT NOT NULL,
|
|
10226
|
-
item_id TEXT NOT NULL,
|
|
10227
|
-
data JSONB,
|
|
10228
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
10229
|
-
PRIMARY KEY (stream_name, group_id, item_id)
|
|
10230
|
-
)
|
|
10231
|
-
`;
|
|
10232
|
-
await sql2`CREATE INDEX IF NOT EXISTS idx_iii_stream_group ON "_iii_stream" (stream_name, group_id)`;
|
|
10233
|
-
}
|
|
10234
|
-
},
|
|
10235
|
-
async close() {
|
|
10236
|
-
if (redisSub) await redisSub.quit();
|
|
10237
|
-
}
|
|
10238
|
-
};
|
|
10239
|
-
}
|
|
10240
|
-
|
|
10241
10409
|
// iii/ws.ts
|
|
10242
10410
|
function createWsHandler(deps) {
|
|
10243
10411
|
const wsToWorkerId = /* @__PURE__ */ new Map();
|
|
@@ -10245,7 +10413,7 @@ function createWsHandler(deps) {
|
|
|
10245
10413
|
return wsToWorkerId.get(ws) || "";
|
|
10246
10414
|
}
|
|
10247
10415
|
return {
|
|
10248
|
-
open(_ws,
|
|
10416
|
+
open(_ws, _ctx2) {
|
|
10249
10417
|
},
|
|
10250
10418
|
async message(ws, ctx, data) {
|
|
10251
10419
|
let msg;
|
|
@@ -10271,9 +10439,9 @@ function createWsHandler(deps) {
|
|
|
10271
10439
|
const workerId = getWorkerId(ws);
|
|
10272
10440
|
if (workerId) {
|
|
10273
10441
|
deps.registerRemoteTrigger(workerId, {
|
|
10274
|
-
type: msg.
|
|
10275
|
-
function_id: msg.function_id,
|
|
10276
|
-
config: msg.config || {}
|
|
10442
|
+
type: msg.input?.type || "custom",
|
|
10443
|
+
function_id: msg.input?.function_id || msg.id,
|
|
10444
|
+
config: msg.input?.config || {}
|
|
10277
10445
|
});
|
|
10278
10446
|
}
|
|
10279
10447
|
break;
|
|
@@ -10285,11 +10453,7 @@ function createWsHandler(deps) {
|
|
|
10285
10453
|
}
|
|
10286
10454
|
case "unregister_trigger": {
|
|
10287
10455
|
const workerId = getWorkerId(ws);
|
|
10288
|
-
if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id);
|
|
10289
|
-
break;
|
|
10290
|
-
}
|
|
10291
|
-
case "invoke": {
|
|
10292
|
-
deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
|
|
10456
|
+
if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id || msg.id);
|
|
10293
10457
|
break;
|
|
10294
10458
|
}
|
|
10295
10459
|
case "invoke_result": {
|
|
@@ -10300,31 +10464,20 @@ function createWsHandler(deps) {
|
|
|
10300
10464
|
deps.handleInvokeError(msg.invocation_id, msg.error);
|
|
10301
10465
|
break;
|
|
10302
10466
|
}
|
|
10303
|
-
case "
|
|
10304
|
-
deps.
|
|
10305
|
-
stream_name: msg.stream_name || msg.channel || "",
|
|
10306
|
-
group_id: msg.group_id,
|
|
10307
|
-
item_id: msg.item_id
|
|
10308
|
-
});
|
|
10309
|
-
break;
|
|
10310
|
-
}
|
|
10311
|
-
case "unsubscribe": {
|
|
10312
|
-
deps.removeStreamSubscriber(ws);
|
|
10467
|
+
case "invoke": {
|
|
10468
|
+
deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
|
|
10313
10469
|
break;
|
|
10314
10470
|
}
|
|
10471
|
+
default:
|
|
10472
|
+
ws.send(JSON.stringify({ type: "error", message: `Unknown message type: ${msg.type}` }));
|
|
10315
10473
|
}
|
|
10316
10474
|
},
|
|
10317
10475
|
close(ws) {
|
|
10318
10476
|
const workerId = getWorkerId(ws);
|
|
10319
|
-
if (workerId)
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
error(ws) {
|
|
10324
|
-
const workerId = getWorkerId(ws);
|
|
10325
|
-
if (workerId) deps.unregisterRemoteWorker(workerId);
|
|
10326
|
-
wsToWorkerId.delete(ws);
|
|
10327
|
-
deps.removeStreamSubscriber(ws);
|
|
10477
|
+
if (workerId) {
|
|
10478
|
+
deps.unregisterRemoteWorker(workerId);
|
|
10479
|
+
wsToWorkerId.delete(ws);
|
|
10480
|
+
}
|
|
10328
10481
|
}
|
|
10329
10482
|
};
|
|
10330
10483
|
}
|
|
@@ -10369,38 +10522,11 @@ function buildRouter5(engine, wsHandler) {
|
|
|
10369
10522
|
}
|
|
10370
10523
|
|
|
10371
10524
|
// iii/client.ts
|
|
10372
|
-
function iii(
|
|
10373
|
-
const stream = createStream({ pg: opts.pg, redis: opts.redis, streamTTL: opts.streamTTL });
|
|
10525
|
+
function iii(_opts = {}) {
|
|
10374
10526
|
const workers = /* @__PURE__ */ new Map();
|
|
10375
10527
|
const functions = /* @__PURE__ */ new Map();
|
|
10376
10528
|
const triggers = /* @__PURE__ */ new Map();
|
|
10377
10529
|
const pending = /* @__PURE__ */ new Map();
|
|
10378
|
-
function registerBuiltin(id2, handler) {
|
|
10379
|
-
functions.set(id2, {
|
|
10380
|
-
id: id2,
|
|
10381
|
-
handler,
|
|
10382
|
-
workerId: "__iii__",
|
|
10383
|
-
workerName: "__iii__",
|
|
10384
|
-
triggers: []
|
|
10385
|
-
});
|
|
10386
|
-
}
|
|
10387
|
-
registerBuiltin(
|
|
10388
|
-
"stream::set",
|
|
10389
|
-
(p) => stream.set(p.stream_name, p.group_id, p.item_id, p.data)
|
|
10390
|
-
);
|
|
10391
|
-
registerBuiltin("stream::get", (p) => stream.get(p.stream_name, p.group_id, p.item_id));
|
|
10392
|
-
registerBuiltin("stream::delete", (p) => stream.delete(p.stream_name, p.group_id, p.item_id));
|
|
10393
|
-
registerBuiltin("stream::list", (p) => stream.list(p.stream_name, p.group_id));
|
|
10394
|
-
registerBuiltin("stream::list_groups", (p) => stream.list_groups(p.stream_name));
|
|
10395
|
-
registerBuiltin("stream::list_all", () => stream.list_all());
|
|
10396
|
-
registerBuiltin(
|
|
10397
|
-
"stream::send",
|
|
10398
|
-
(p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id)
|
|
10399
|
-
);
|
|
10400
|
-
registerBuiltin(
|
|
10401
|
-
"stream::update",
|
|
10402
|
-
(p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops)
|
|
10403
|
-
);
|
|
10404
10530
|
function addLocalWorker(worker) {
|
|
10405
10531
|
const workerId = crypto8.randomUUID();
|
|
10406
10532
|
const reg = {
|
|
@@ -10447,12 +10573,12 @@ function iii(opts = {}) {
|
|
|
10447
10573
|
const handler = async (payload) => {
|
|
10448
10574
|
if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
|
|
10449
10575
|
const invocationId = crypto8.randomUUID();
|
|
10450
|
-
return new Promise((
|
|
10576
|
+
return new Promise((resolve16, reject) => {
|
|
10451
10577
|
const timer = setTimeout(() => {
|
|
10452
10578
|
pending.delete(invocationId);
|
|
10453
10579
|
reject(new Error(`Invocation timed out for "${id2}"`));
|
|
10454
10580
|
}, 3e4);
|
|
10455
|
-
pending.set(invocationId, { resolve:
|
|
10581
|
+
pending.set(invocationId, { resolve: resolve16, reject, timer });
|
|
10456
10582
|
worker.ws.send(
|
|
10457
10583
|
JSON.stringify({
|
|
10458
10584
|
type: "invoke",
|
|
@@ -10519,12 +10645,6 @@ function iii(opts = {}) {
|
|
|
10519
10645
|
worker.triggers = worker.triggers.filter((t) => t.function_id !== functionId);
|
|
10520
10646
|
}
|
|
10521
10647
|
},
|
|
10522
|
-
addStreamSubscriber(ws, sub) {
|
|
10523
|
-
stream.subscribe(ws, sub);
|
|
10524
|
-
},
|
|
10525
|
-
removeStreamSubscriber(ws) {
|
|
10526
|
-
stream.unsubscribe(ws);
|
|
10527
|
-
},
|
|
10528
10648
|
handleInvokeResult(invocationId, result) {
|
|
10529
10649
|
const p = pending.get(invocationId);
|
|
10530
10650
|
if (p) {
|
|
@@ -10577,7 +10697,7 @@ function iii(opts = {}) {
|
|
|
10577
10697
|
}
|
|
10578
10698
|
function trigger(request) {
|
|
10579
10699
|
const fn = functions.get(request.function_id);
|
|
10580
|
-
if (!fn)
|
|
10700
|
+
if (!fn) return Promise.reject(new Error(`Function "${request.function_id}" not found`));
|
|
10581
10701
|
const ctx = { engine: engineRef, functionId: request.function_id, workerName: fn.workerName };
|
|
10582
10702
|
if (request.action === "void") {
|
|
10583
10703
|
queueMicrotask(() => fn.handler(request.payload, ctx));
|
|
@@ -10624,7 +10744,6 @@ function iii(opts = {}) {
|
|
|
10624
10744
|
mod.listFunctions = listFunctions;
|
|
10625
10745
|
mod.listTriggers = listTriggers;
|
|
10626
10746
|
mod.migrate = async () => {
|
|
10627
|
-
await stream.migrate();
|
|
10628
10747
|
};
|
|
10629
10748
|
mod.close = async () => {
|
|
10630
10749
|
for (const [, p] of pending) {
|
|
@@ -10636,7 +10755,6 @@ function iii(opts = {}) {
|
|
|
10636
10755
|
workers.clear();
|
|
10637
10756
|
functions.clear();
|
|
10638
10757
|
triggers.clear();
|
|
10639
|
-
await stream.close();
|
|
10640
10758
|
};
|
|
10641
10759
|
return mod;
|
|
10642
10760
|
}
|
|
@@ -10703,8 +10821,8 @@ function registerWorker(url) {
|
|
|
10703
10821
|
function connect() {
|
|
10704
10822
|
if (intentionalClose) return;
|
|
10705
10823
|
ws = new WebSocket(url);
|
|
10706
|
-
ready = new Promise((
|
|
10707
|
-
resolveReady =
|
|
10824
|
+
ready = new Promise((resolve16) => {
|
|
10825
|
+
resolveReady = resolve16;
|
|
10708
10826
|
});
|
|
10709
10827
|
ws.onopen = () => {
|
|
10710
10828
|
reconnectAttempt = 0;
|
|
@@ -10773,11 +10891,6 @@ function registerWorker(url) {
|
|
|
10773
10891
|
}
|
|
10774
10892
|
break;
|
|
10775
10893
|
}
|
|
10776
|
-
case "stream": {
|
|
10777
|
-
const handler = handlers.get("__stream__");
|
|
10778
|
-
if (handler) handler(msg, {});
|
|
10779
|
-
break;
|
|
10780
|
-
}
|
|
10781
10894
|
}
|
|
10782
10895
|
};
|
|
10783
10896
|
ws.onclose = () => {
|
|
@@ -10835,13 +10948,13 @@ function registerWorker(url) {
|
|
|
10835
10948
|
}
|
|
10836
10949
|
return Promise.resolve(fn(request.payload, ctx));
|
|
10837
10950
|
}
|
|
10838
|
-
return new Promise((
|
|
10951
|
+
return new Promise((resolve16, reject) => {
|
|
10839
10952
|
const invocationId = genId();
|
|
10840
10953
|
const timer = setTimeout(() => {
|
|
10841
10954
|
pendingInvocations.delete(invocationId);
|
|
10842
10955
|
reject(new Error(`Invocation timed out for "${request.function_id}"`));
|
|
10843
10956
|
}, request.timeout_ms || 3e4);
|
|
10844
|
-
pendingInvocations.set(invocationId, { resolve:
|
|
10957
|
+
pendingInvocations.set(invocationId, { resolve: resolve16, reject, timer });
|
|
10845
10958
|
send({
|
|
10846
10959
|
type: "invoke",
|
|
10847
10960
|
invocation_id: invocationId,
|
|
@@ -10850,9 +10963,6 @@ function registerWorker(url) {
|
|
|
10850
10963
|
});
|
|
10851
10964
|
});
|
|
10852
10965
|
},
|
|
10853
|
-
onStream(handler) {
|
|
10854
|
-
handlers.set("__stream__", handler);
|
|
10855
|
-
},
|
|
10856
10966
|
close() {
|
|
10857
10967
|
intentionalClose = true;
|
|
10858
10968
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
@@ -11104,6 +11214,7 @@ function session(options) {
|
|
|
11104
11214
|
}
|
|
11105
11215
|
return res;
|
|
11106
11216
|
});
|
|
11217
|
+
mw.__meta = { injects: ["session"], depends: [] };
|
|
11107
11218
|
mw.close = async () => {
|
|
11108
11219
|
await closeStore?.();
|
|
11109
11220
|
};
|
|
@@ -12033,6 +12144,475 @@ function permissions(options) {
|
|
|
12033
12144
|
mw.migrate = migrate;
|
|
12034
12145
|
return mw;
|
|
12035
12146
|
}
|
|
12147
|
+
|
|
12148
|
+
// mcp.ts
|
|
12149
|
+
import { spawn } from "node:child_process";
|
|
12150
|
+
import { createInterface } from "node:readline";
|
|
12151
|
+
import { z as z14 } from "zod";
|
|
12152
|
+
var _requestId = 0;
|
|
12153
|
+
function nextId() {
|
|
12154
|
+
return ++_requestId;
|
|
12155
|
+
}
|
|
12156
|
+
function createRequest2(id2, method, params) {
|
|
12157
|
+
return JSON.stringify({
|
|
12158
|
+
jsonrpc: "2.0",
|
|
12159
|
+
id: id2,
|
|
12160
|
+
method,
|
|
12161
|
+
params
|
|
12162
|
+
});
|
|
12163
|
+
}
|
|
12164
|
+
function mcpClient(options) {
|
|
12165
|
+
const { command, args = [], env: env2 } = options;
|
|
12166
|
+
const timeout = options.timeout ?? 15e3;
|
|
12167
|
+
const maxResponseSize = options.maxResponseSize ?? 10 * 1024 * 1024;
|
|
12168
|
+
let proc = null;
|
|
12169
|
+
let rl = null;
|
|
12170
|
+
const pending = /* @__PURE__ */ new Map();
|
|
12171
|
+
let _tools = null;
|
|
12172
|
+
function ensureProcess() {
|
|
12173
|
+
if (proc && !proc.killed) return;
|
|
12174
|
+
proc = spawn(command, args, {
|
|
12175
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12176
|
+
env: { ...process.env, ...env2 }
|
|
12177
|
+
});
|
|
12178
|
+
rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
12179
|
+
let buffer = "";
|
|
12180
|
+
rl.on("line", (line) => {
|
|
12181
|
+
buffer += line;
|
|
12182
|
+
try {
|
|
12183
|
+
const msg = JSON.parse(buffer);
|
|
12184
|
+
buffer = "";
|
|
12185
|
+
handleMessage(msg);
|
|
12186
|
+
} catch {
|
|
12187
|
+
}
|
|
12188
|
+
});
|
|
12189
|
+
proc.stderr?.on("data", (chunk) => {
|
|
12190
|
+
const text2 = chunk.toString().trim();
|
|
12191
|
+
if (text2) {
|
|
12192
|
+
console.debug(`[mcp:${command}] stderr:`, text2);
|
|
12193
|
+
}
|
|
12194
|
+
});
|
|
12195
|
+
proc.on("exit", (code, signal) => {
|
|
12196
|
+
console.debug(`[mcp:${command}] exited (code=${code} signal=${signal})`);
|
|
12197
|
+
for (const [id2, { reject, timer }] of pending) {
|
|
12198
|
+
clearTimeout(timer);
|
|
12199
|
+
reject(new Error(`MCP server exited (code=${code} signal=${signal})`));
|
|
12200
|
+
pending.delete(id2);
|
|
12201
|
+
}
|
|
12202
|
+
proc = null;
|
|
12203
|
+
rl = null;
|
|
12204
|
+
});
|
|
12205
|
+
proc.on("error", (err) => {
|
|
12206
|
+
console.error(`[mcp:${command}] error:`, err.message);
|
|
12207
|
+
for (const [, { reject, timer }] of pending) {
|
|
12208
|
+
clearTimeout(timer);
|
|
12209
|
+
reject(err);
|
|
12210
|
+
}
|
|
12211
|
+
pending.clear();
|
|
12212
|
+
});
|
|
12213
|
+
}
|
|
12214
|
+
function handleMessage(msg) {
|
|
12215
|
+
if (msg.id !== void 0 && pending.has(msg.id)) {
|
|
12216
|
+
const { resolve: resolve16, reject, timer } = pending.get(msg.id);
|
|
12217
|
+
clearTimeout(timer);
|
|
12218
|
+
pending.delete(msg.id);
|
|
12219
|
+
if (msg.error) {
|
|
12220
|
+
reject(new Error(`MCP error: ${JSON.stringify(msg.error)}`));
|
|
12221
|
+
} else {
|
|
12222
|
+
resolve16(msg.result);
|
|
12223
|
+
}
|
|
12224
|
+
}
|
|
12225
|
+
}
|
|
12226
|
+
function sendRequest(method, params) {
|
|
12227
|
+
ensureProcess();
|
|
12228
|
+
const id2 = nextId();
|
|
12229
|
+
const body = createRequest2(id2, method, params);
|
|
12230
|
+
return new Promise((resolve16, reject) => {
|
|
12231
|
+
const timer = setTimeout(() => {
|
|
12232
|
+
pending.delete(id2);
|
|
12233
|
+
reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
|
|
12234
|
+
}, timeout);
|
|
12235
|
+
pending.set(id2, { resolve: resolve16, reject, timer });
|
|
12236
|
+
if (proc?.stdin?.writable) {
|
|
12237
|
+
proc.stdin.write(body + "\n");
|
|
12238
|
+
} else {
|
|
12239
|
+
clearTimeout(timer);
|
|
12240
|
+
pending.delete(id2);
|
|
12241
|
+
reject(new Error("MCP server stdin not available"));
|
|
12242
|
+
}
|
|
12243
|
+
});
|
|
12244
|
+
}
|
|
12245
|
+
async function initialize() {
|
|
12246
|
+
ensureProcess();
|
|
12247
|
+
await sendRequest("initialize", {
|
|
12248
|
+
protocolVersion: "0.1.0",
|
|
12249
|
+
capabilities: {},
|
|
12250
|
+
clientInfo: { name: "weifuwu", version: "0.25.0" }
|
|
12251
|
+
});
|
|
12252
|
+
try {
|
|
12253
|
+
if (proc?.stdin?.writable) {
|
|
12254
|
+
proc.stdin.write(
|
|
12255
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"
|
|
12256
|
+
);
|
|
12257
|
+
}
|
|
12258
|
+
} catch {
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
function mcpSchemaToZod(inputSchema) {
|
|
12262
|
+
if (!inputSchema || !inputSchema.properties) {
|
|
12263
|
+
return z14.object({});
|
|
12264
|
+
}
|
|
12265
|
+
const shape = {};
|
|
12266
|
+
const required = new Set(inputSchema.required ?? []);
|
|
12267
|
+
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
|
12268
|
+
const p = prop;
|
|
12269
|
+
let field;
|
|
12270
|
+
switch (p.type) {
|
|
12271
|
+
case "string":
|
|
12272
|
+
field = z14.string();
|
|
12273
|
+
break;
|
|
12274
|
+
case "number":
|
|
12275
|
+
field = z14.number();
|
|
12276
|
+
break;
|
|
12277
|
+
case "integer":
|
|
12278
|
+
field = z14.number().int();
|
|
12279
|
+
break;
|
|
12280
|
+
case "boolean":
|
|
12281
|
+
field = z14.boolean();
|
|
12282
|
+
break;
|
|
12283
|
+
case "array":
|
|
12284
|
+
field = z14.array(z14.any());
|
|
12285
|
+
break;
|
|
12286
|
+
case "object":
|
|
12287
|
+
field = z14.record(z14.string(), z14.any());
|
|
12288
|
+
break;
|
|
12289
|
+
default:
|
|
12290
|
+
field = z14.any();
|
|
12291
|
+
}
|
|
12292
|
+
if (p.description) {
|
|
12293
|
+
field = field.describe(p.description);
|
|
12294
|
+
}
|
|
12295
|
+
if (!required.has(key)) {
|
|
12296
|
+
field = field.optional();
|
|
12297
|
+
}
|
|
12298
|
+
shape[key] = field;
|
|
12299
|
+
}
|
|
12300
|
+
return z14.object(shape);
|
|
12301
|
+
}
|
|
12302
|
+
async function refresh() {
|
|
12303
|
+
_tools = null;
|
|
12304
|
+
await getTools();
|
|
12305
|
+
}
|
|
12306
|
+
async function getTools() {
|
|
12307
|
+
if (_tools) return _tools;
|
|
12308
|
+
await initialize();
|
|
12309
|
+
const result = await sendRequest("tools/list");
|
|
12310
|
+
const defs = result?.tools ?? [];
|
|
12311
|
+
const tools = {};
|
|
12312
|
+
for (const def of defs) {
|
|
12313
|
+
const paramsSchema = mcpSchemaToZod(def.inputSchema);
|
|
12314
|
+
tools[def.name] = {
|
|
12315
|
+
description: def.description ?? "MCP tool: " + def.name,
|
|
12316
|
+
parameters: paramsSchema,
|
|
12317
|
+
execute: async (args2) => {
|
|
12318
|
+
const raw = await callToolInternal(def.name, args2);
|
|
12319
|
+
return raw;
|
|
12320
|
+
}
|
|
12321
|
+
};
|
|
12322
|
+
}
|
|
12323
|
+
_tools = tools;
|
|
12324
|
+
return tools;
|
|
12325
|
+
}
|
|
12326
|
+
async function callToolInternal(name, args2) {
|
|
12327
|
+
const result = await sendRequest("tools/call", {
|
|
12328
|
+
name,
|
|
12329
|
+
arguments: args2
|
|
12330
|
+
});
|
|
12331
|
+
const content = result?.content;
|
|
12332
|
+
if (Array.isArray(content)) {
|
|
12333
|
+
const textParts = content.filter((c) => c.type === "text").map((c) => c.text ?? "");
|
|
12334
|
+
if (textParts.length > 0) {
|
|
12335
|
+
let combined = textParts.join("\n");
|
|
12336
|
+
if (combined.length > maxResponseSize) {
|
|
12337
|
+
combined = combined.slice(0, maxResponseSize) + "\n... [truncated]";
|
|
12338
|
+
}
|
|
12339
|
+
return combined;
|
|
12340
|
+
}
|
|
12341
|
+
const resourceParts = content.filter((c) => c.type === "resource");
|
|
12342
|
+
if (resourceParts.length > 0) {
|
|
12343
|
+
return resourceParts;
|
|
12344
|
+
}
|
|
12345
|
+
return content;
|
|
12346
|
+
}
|
|
12347
|
+
return result;
|
|
12348
|
+
}
|
|
12349
|
+
async function callTool(name, args2) {
|
|
12350
|
+
return callToolInternal(name, args2);
|
|
12351
|
+
}
|
|
12352
|
+
async function close() {
|
|
12353
|
+
try {
|
|
12354
|
+
await sendRequest("shutdown");
|
|
12355
|
+
} catch {
|
|
12356
|
+
}
|
|
12357
|
+
if (proc && !proc.killed) {
|
|
12358
|
+
proc.kill("SIGTERM");
|
|
12359
|
+
setTimeout(() => {
|
|
12360
|
+
if (proc && !proc.killed) {
|
|
12361
|
+
try {
|
|
12362
|
+
proc.kill("SIGKILL");
|
|
12363
|
+
} catch {
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
}, 3e3);
|
|
12367
|
+
}
|
|
12368
|
+
for (const [, { reject, timer }] of pending) {
|
|
12369
|
+
clearTimeout(timer);
|
|
12370
|
+
reject(new Error("MCP client closed"));
|
|
12371
|
+
}
|
|
12372
|
+
pending.clear();
|
|
12373
|
+
proc = null;
|
|
12374
|
+
rl = null;
|
|
12375
|
+
}
|
|
12376
|
+
return {
|
|
12377
|
+
getTools,
|
|
12378
|
+
refresh,
|
|
12379
|
+
callTool,
|
|
12380
|
+
close
|
|
12381
|
+
};
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
// notifier/client.ts
|
|
12385
|
+
var DEFAULT_CHANNELS = ["inbox"];
|
|
12386
|
+
function notifier(opts) {
|
|
12387
|
+
const { sql: sql2, mailer: mailer2, hub } = opts;
|
|
12388
|
+
const table = opts.table ?? "_notifications";
|
|
12389
|
+
const fromName = opts.fromName ?? "System";
|
|
12390
|
+
const pageSize = opts.pageSize ?? 50;
|
|
12391
|
+
function escapeIdent7(s) {
|
|
12392
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
12393
|
+
}
|
|
12394
|
+
const tbl = escapeIdent7(table);
|
|
12395
|
+
async function migrate() {
|
|
12396
|
+
await sql2.unsafe(`
|
|
12397
|
+
CREATE TABLE IF NOT EXISTS ${tbl} (
|
|
12398
|
+
id SERIAL PRIMARY KEY,
|
|
12399
|
+
user_id INTEGER NOT NULL,
|
|
12400
|
+
title TEXT NOT NULL,
|
|
12401
|
+
body TEXT NOT NULL DEFAULT '',
|
|
12402
|
+
action_url TEXT,
|
|
12403
|
+
action_text TEXT,
|
|
12404
|
+
type TEXT NOT NULL DEFAULT 'default',
|
|
12405
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
12406
|
+
read_at TIMESTAMPTZ,
|
|
12407
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
12408
|
+
)
|
|
12409
|
+
`);
|
|
12410
|
+
await sql2.unsafe(`
|
|
12411
|
+
CREATE INDEX IF NOT EXISTS "${table}_user_unread"
|
|
12412
|
+
ON ${tbl} (user_id, created_at DESC)
|
|
12413
|
+
WHERE read_at IS NULL
|
|
12414
|
+
`);
|
|
12415
|
+
await sql2.unsafe(`
|
|
12416
|
+
CREATE INDEX IF NOT EXISTS "${table}_user_all"
|
|
12417
|
+
ON ${tbl} (user_id, created_at DESC)
|
|
12418
|
+
`);
|
|
12419
|
+
await sql2.unsafe(`
|
|
12420
|
+
CREATE TABLE IF NOT EXISTS "_notify_prefs" (
|
|
12421
|
+
user_id INTEGER PRIMARY KEY,
|
|
12422
|
+
channels JSONB NOT NULL DEFAULT '["inbox"]'::jsonb
|
|
12423
|
+
)
|
|
12424
|
+
`);
|
|
12425
|
+
}
|
|
12426
|
+
async function insertNotification(userId2, message) {
|
|
12427
|
+
await sql2.unsafe(
|
|
12428
|
+
`INSERT INTO ${tbl} (user_id, title, body, action_url, action_text, type, metadata)
|
|
12429
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
12430
|
+
[
|
|
12431
|
+
userId2,
|
|
12432
|
+
message.title,
|
|
12433
|
+
message.body ?? "",
|
|
12434
|
+
message.actionUrl ?? null,
|
|
12435
|
+
message.actionText ?? null,
|
|
12436
|
+
message.type ?? "default",
|
|
12437
|
+
message.metadata ?? {}
|
|
12438
|
+
]
|
|
12439
|
+
);
|
|
12440
|
+
}
|
|
12441
|
+
async function send(to, message) {
|
|
12442
|
+
const prefs = await getPreferences(to.userId);
|
|
12443
|
+
if (prefs.channels.includes("inbox")) {
|
|
12444
|
+
await insertNotification(to.userId, message);
|
|
12445
|
+
}
|
|
12446
|
+
if (prefs.channels.includes("email") && mailer2 && to.email) {
|
|
12447
|
+
const html = renderEmail(message);
|
|
12448
|
+
mailer2.send({
|
|
12449
|
+
to: to.email,
|
|
12450
|
+
subject: message.title,
|
|
12451
|
+
text: message.body ?? "",
|
|
12452
|
+
html
|
|
12453
|
+
}).catch((err) => console.error("[notifier] email send failed:", err.message));
|
|
12454
|
+
}
|
|
12455
|
+
if (prefs.channels.includes("ws") && hub) {
|
|
12456
|
+
hub.broadcast(`notify:${to.userId}`, {
|
|
12457
|
+
type: "notification",
|
|
12458
|
+
data: {
|
|
12459
|
+
title: message.title,
|
|
12460
|
+
body: message.body,
|
|
12461
|
+
actionUrl: message.actionUrl,
|
|
12462
|
+
actionText: message.actionText,
|
|
12463
|
+
type: message.type ?? "default",
|
|
12464
|
+
metadata: message.metadata,
|
|
12465
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12466
|
+
}
|
|
12467
|
+
});
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
12470
|
+
async function broadcast(message) {
|
|
12471
|
+
const rows = await sql2`
|
|
12472
|
+
SELECT user_id FROM "_notify_prefs"
|
|
12473
|
+
WHERE channels @> ${sql2.json(["inbox"])}
|
|
12474
|
+
`;
|
|
12475
|
+
for (const row of rows) {
|
|
12476
|
+
await insertNotification(row.user_id, message);
|
|
12477
|
+
if (hub) {
|
|
12478
|
+
hub.broadcast(`notify:${row.user_id}`, {
|
|
12479
|
+
type: "notification",
|
|
12480
|
+
data: {
|
|
12481
|
+
title: message.title,
|
|
12482
|
+
body: message.body,
|
|
12483
|
+
actionUrl: message.actionUrl,
|
|
12484
|
+
actionText: message.actionText,
|
|
12485
|
+
type: message.type ?? "default",
|
|
12486
|
+
metadata: message.metadata,
|
|
12487
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12488
|
+
}
|
|
12489
|
+
});
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
async function unreadCount(userId2) {
|
|
12494
|
+
const [row] = await sql2.unsafe(
|
|
12495
|
+
`SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1 AND read_at IS NULL`,
|
|
12496
|
+
[userId2]
|
|
12497
|
+
);
|
|
12498
|
+
return row?.count ?? 0;
|
|
12499
|
+
}
|
|
12500
|
+
async function count(userId2, unreadOnly = false) {
|
|
12501
|
+
if (unreadOnly) return unreadCount(userId2);
|
|
12502
|
+
const [row] = await sql2.unsafe(
|
|
12503
|
+
`SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1`,
|
|
12504
|
+
[userId2]
|
|
12505
|
+
);
|
|
12506
|
+
return row?.count ?? 0;
|
|
12507
|
+
}
|
|
12508
|
+
async function markRead(userId2, notificationIds) {
|
|
12509
|
+
if (notificationIds && notificationIds.length > 0) {
|
|
12510
|
+
const ids = notificationIds.map((_, i) => `$${i + 2}`).join(", ");
|
|
12511
|
+
await sql2.unsafe(
|
|
12512
|
+
`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND id IN (${ids}) AND read_at IS NULL`,
|
|
12513
|
+
[userId2, ...notificationIds]
|
|
12514
|
+
);
|
|
12515
|
+
} else {
|
|
12516
|
+
await sql2.unsafe(`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL`, [
|
|
12517
|
+
userId2
|
|
12518
|
+
]);
|
|
12519
|
+
}
|
|
12520
|
+
}
|
|
12521
|
+
async function list(userId2, opts2) {
|
|
12522
|
+
const limit = opts2?.limit ?? pageSize;
|
|
12523
|
+
const offset = opts2?.offset ?? 0;
|
|
12524
|
+
let where = `user_id = $1`;
|
|
12525
|
+
const params = [userId2];
|
|
12526
|
+
const paramIdx = 2;
|
|
12527
|
+
if (opts2?.unreadOnly) {
|
|
12528
|
+
where += ` AND read_at IS NULL`;
|
|
12529
|
+
}
|
|
12530
|
+
const rows = await sql2.unsafe(
|
|
12531
|
+
`SELECT id, user_id, title, body, action_url, action_text, type, metadata, read_at, created_at
|
|
12532
|
+
FROM ${tbl}
|
|
12533
|
+
WHERE ${where}
|
|
12534
|
+
ORDER BY created_at DESC
|
|
12535
|
+
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
|
|
12536
|
+
[...params, limit, offset]
|
|
12537
|
+
);
|
|
12538
|
+
return rows.map(normalizeNotification);
|
|
12539
|
+
}
|
|
12540
|
+
function normalizeNotification(row) {
|
|
12541
|
+
return {
|
|
12542
|
+
...row,
|
|
12543
|
+
metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata ?? {}
|
|
12544
|
+
};
|
|
12545
|
+
}
|
|
12546
|
+
async function getPreferences(userId2) {
|
|
12547
|
+
const [row] = await sql2.unsafe(`SELECT channels FROM "_notify_prefs" WHERE user_id = $1`, [
|
|
12548
|
+
userId2
|
|
12549
|
+
]);
|
|
12550
|
+
if (!row) {
|
|
12551
|
+
return { channels: [...DEFAULT_CHANNELS] };
|
|
12552
|
+
}
|
|
12553
|
+
let channels;
|
|
12554
|
+
if (typeof row.channels === "string") {
|
|
12555
|
+
channels = JSON.parse(row.channels);
|
|
12556
|
+
} else if (Array.isArray(row.channels)) {
|
|
12557
|
+
channels = row.channels;
|
|
12558
|
+
} else {
|
|
12559
|
+
channels = [...DEFAULT_CHANNELS];
|
|
12560
|
+
}
|
|
12561
|
+
return { channels };
|
|
12562
|
+
}
|
|
12563
|
+
async function setPreferences(userId2, prefs) {
|
|
12564
|
+
await sql2`
|
|
12565
|
+
INSERT INTO "_notify_prefs" (user_id, channels)
|
|
12566
|
+
VALUES (${userId2}, ${sql2.json(prefs.channels)})
|
|
12567
|
+
ON CONFLICT (user_id)
|
|
12568
|
+
DO UPDATE SET channels = ${sql2.json(prefs.channels)}
|
|
12569
|
+
`;
|
|
12570
|
+
}
|
|
12571
|
+
async function clean(days) {
|
|
12572
|
+
const result = await sql2.unsafe(`DELETE FROM ${tbl} WHERE created_at < NOW() - $1::interval`, [
|
|
12573
|
+
`${days} days`
|
|
12574
|
+
]);
|
|
12575
|
+
return Array.isArray(result) ? result.length : 0;
|
|
12576
|
+
}
|
|
12577
|
+
function renderEmail(message) {
|
|
12578
|
+
const actionHtml = message.actionUrl ? `
|
|
12579
|
+
<p><a href="${escapeHtml3(message.actionUrl)}" style="display:inline-block;padding:10px 20px;background:#0066cc;color:#fff;text-decoration:none;border-radius:4px">${escapeHtml3(message.actionText ?? "View")}</a></p>` : "";
|
|
12580
|
+
return `<!DOCTYPE html>
|
|
12581
|
+
<html>
|
|
12582
|
+
<head><meta charset="utf-8"></head>
|
|
12583
|
+
<body style="font-family:sans-serif;padding:20px;max-width:600px">
|
|
12584
|
+
<h2>${escapeHtml3(message.title)}</h2>
|
|
12585
|
+
${message.body ? `<p>${escapeHtml3(message.body)}</p>` : ""}
|
|
12586
|
+
${actionHtml}
|
|
12587
|
+
<hr style="margin-top:30px;border:none;border-top:1px solid #eee">
|
|
12588
|
+
<p style="color:#999;font-size:12px">${escapeHtml3(fromName)}</p>
|
|
12589
|
+
</body>
|
|
12590
|
+
</html>`;
|
|
12591
|
+
}
|
|
12592
|
+
function escapeHtml3(s) {
|
|
12593
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
12594
|
+
}
|
|
12595
|
+
const mw = async (req, ctx, next) => {
|
|
12596
|
+
;
|
|
12597
|
+
ctx.notifier = api;
|
|
12598
|
+
return next(req, ctx);
|
|
12599
|
+
};
|
|
12600
|
+
const api = {
|
|
12601
|
+
send,
|
|
12602
|
+
broadcast,
|
|
12603
|
+
unreadCount,
|
|
12604
|
+
count,
|
|
12605
|
+
markRead,
|
|
12606
|
+
list,
|
|
12607
|
+
getPreferences,
|
|
12608
|
+
setPreferences,
|
|
12609
|
+
clean,
|
|
12610
|
+
migrate,
|
|
12611
|
+
close: async () => {
|
|
12612
|
+
}
|
|
12613
|
+
};
|
|
12614
|
+
return Object.assign(mw, api);
|
|
12615
|
+
}
|
|
12036
12616
|
export {
|
|
12037
12617
|
DEFAULT_MAX_BODY,
|
|
12038
12618
|
MIGRATIONS_TABLE,
|
|
@@ -12087,7 +12667,9 @@ export {
|
|
|
12087
12667
|
logdb,
|
|
12088
12668
|
logger,
|
|
12089
12669
|
mailer,
|
|
12670
|
+
mcpClient,
|
|
12090
12671
|
messager,
|
|
12672
|
+
notifier,
|
|
12091
12673
|
openai,
|
|
12092
12674
|
opencode,
|
|
12093
12675
|
permissions,
|