weifuwu 0.7.0 → 0.8.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 CHANGED
@@ -28,6 +28,7 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
28
28
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
29
29
  - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` — immutable
30
30
  - **Error handling** — global `onError()`
31
+ - **Deploy** — `deploy()` — self-hosted PaaS: multi-app reverse proxy, subdomain routing, zero-downtime updates, auto SSL, Git-based deployment
31
32
  - **Zero build** — native TypeScript in Node.js v24+
32
33
  - **Zero deps** (core) — only `node:http` and `node:stream`
33
34
 
@@ -998,6 +999,42 @@ const app = new Router()
998
999
  .get('/crash', () => { throw new Error('boom') })
999
1000
  ```
1000
1001
 
1002
+ ## Deploy
1003
+
1004
+ See [deploy.md](./deploy.md) for complete documentation — VPS setup, subdomain routing, blue-green zero-downtime, WebSocket bridge, Git webhook, auto SSL, and management API.
1005
+
1006
+ Quick start on a fresh VPS:
1007
+
1008
+ ```bash
1009
+ # 1. Install Node.js
1010
+ curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
1011
+ apt-get install -y nodejs git
1012
+
1013
+ # 2. Create deploy project
1014
+ mkdir -p /opt/deploy && cd /opt/deploy
1015
+ npm init -y && npm install weifuwu
1016
+
1017
+ # 3. Write deploy.ts
1018
+ cat > deploy.ts << 'EOF'
1019
+ import { deploy, defineConfig } from 'weifuwu'
1020
+ await deploy(defineConfig({
1021
+ domain: 'example.com',
1022
+ deployToken: process.env.DEPLOY_TOKEN,
1023
+ apps: {
1024
+ blog: {
1025
+ repo: 'https://github.com/me/my-blog.git',
1026
+ subdomain: 'blog',
1027
+ entry: 'app.ts',
1028
+ port: 3001,
1029
+ },
1030
+ },
1031
+ }))
1032
+ EOF
1033
+
1034
+ # 4. Run
1035
+ DEPLOY_TOKEN='my-secret' node deploy.ts
1036
+ ```
1037
+
1001
1038
  ## API
1002
1039
 
1003
1040
  ### `serve(handler, options?)`
@@ -1099,6 +1136,13 @@ Returns `Promise<Router>`.
1099
1136
  | `ai(handler)` | AI streaming endpoint (POST) |
1100
1137
  | `workflow(handler)` | Workflow engine (POST + SSE) |
1101
1138
 
1139
+ ### Deploy
1140
+
1141
+ | Import | Description |
1142
+ |--------|-------------|
1143
+ | `deploy(config)` | Start the deployment platform — see [deploy.md](./deploy.md) |
1144
+ | `defineConfig(config)` | Type-safe config helper with validation — see [deploy.md](./deploy.md) |
1145
+
1102
1146
  ### Utilities
1103
1147
 
1104
1148
  | Function | Description |
@@ -0,0 +1,2 @@
1
+ import type { DeployConfig } from './types.ts';
2
+ export declare function defineConfig(config: DeployConfig): DeployConfig;
@@ -0,0 +1,2 @@
1
+ import type { DeployConfig, GatewayResult } from './types.ts';
2
+ export declare function createGateway(config: DeployConfig, getPort: (name: string) => number | undefined): GatewayResult;
@@ -0,0 +1,4 @@
1
+ import type { DeployConfig, DeployServer } from './types.ts';
2
+ export { defineConfig } from './config.ts';
3
+ export type { DeployConfig, AppConfig, DeployServer, AppStatus, GatewayResult } from './types.ts';
4
+ export declare function deploy(config: DeployConfig): Promise<DeployServer>;
@@ -0,0 +1,16 @@
1
+ import { Router } from '../router.ts';
2
+ import type { DeployConfig, AppStatus } from './types.ts';
3
+ export interface AppRuntime {
4
+ config: import('./types.ts').AppConfig;
5
+ status: AppStatus;
6
+ logs: string[];
7
+ process: import('node:child_process').ChildProcess | null;
8
+ currentPort: number;
9
+ startedAt: number | null;
10
+ restartCount: number;
11
+ restartTimer: ReturnType<typeof setTimeout> | undefined;
12
+ }
13
+ export declare function createManager(config: DeployConfig, apps: Map<string, AppRuntime>, manager: {
14
+ deployApp(name: string): Promise<void>;
15
+ reloadConfig(): Promise<void>;
16
+ }): Router;
@@ -0,0 +1,14 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ export interface ManagedProcess {
3
+ child: ChildProcess;
4
+ port: number;
5
+ }
6
+ export declare function forkApp(opts: {
7
+ cwd: string;
8
+ entry: string;
9
+ port: number;
10
+ env?: Record<string, string>;
11
+ onLog?: (line: string) => void;
12
+ }): ManagedProcess;
13
+ export declare function stopProcess(mp: ManagedProcess, timeout?: number): Promise<void>;
14
+ export declare function healthCheck(port: number, path?: string): Promise<boolean>;
@@ -0,0 +1,62 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ import type { Handler } from '../types.ts';
4
+ export interface DeployConfig {
5
+ domain: string;
6
+ port?: number;
7
+ ssl?: {
8
+ email: string;
9
+ staging?: boolean;
10
+ };
11
+ deployToken?: string;
12
+ webhookSecret?: string;
13
+ appsDir?: string;
14
+ defaultApp?: string;
15
+ apps: Record<string, AppConfig>;
16
+ }
17
+ export interface AppConfig {
18
+ repo: string;
19
+ branch?: string;
20
+ subdomain?: string;
21
+ path?: string;
22
+ port: number;
23
+ ports?: [number, number];
24
+ entry: string;
25
+ env?: Record<string, string>;
26
+ healthEndpoint?: string;
27
+ buildCommand?: string;
28
+ }
29
+ export interface AppStatus {
30
+ name: string;
31
+ status: 'starting' | 'running' | 'stopped' | 'error';
32
+ port: number;
33
+ subdomain?: string;
34
+ path?: string;
35
+ pid?: number;
36
+ uptime?: number;
37
+ error?: string;
38
+ }
39
+ export interface DeployServer {
40
+ stop(): Promise<void>;
41
+ ready: Promise<void>;
42
+ url: string;
43
+ apps: {
44
+ list(): AppStatus[];
45
+ status(name: string): AppStatus | undefined;
46
+ deploy(name: string): Promise<void>;
47
+ restart(name: string): Promise<void>;
48
+ stop(name: string): Promise<void>;
49
+ start(name: string): Promise<void>;
50
+ };
51
+ }
52
+ export interface GatewayResult {
53
+ handler: Handler;
54
+ wsHandler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
55
+ }
56
+ declare module '../types.ts' {
57
+ interface Context {
58
+ deploy?: {
59
+ appName?: string;
60
+ };
61
+ }
62
+ }
package/dist/index.d.ts CHANGED
@@ -39,3 +39,5 @@ export { agent } from './agent/index.ts';
39
39
  export type { AgentOptions, AgentModule, AgentConfig, RunParams, RunResult, KnowledgeDoc } from './agent/types.ts';
40
40
  export { messager } from './messager/index.ts';
41
41
  export type { MessagerOptions, MessagerModule, Channel, ChannelMember, Message } from './messager/types.ts';
42
+ export { deploy, defineConfig } from './deploy/index.ts';
43
+ export type { DeployConfig, AppConfig, DeployServer, AppStatus } from './deploy/types.ts';
package/dist/index.js CHANGED
@@ -10918,38 +10918,38 @@ var Router = class _Router {
10918
10918
  }
10919
10919
  return this;
10920
10920
  }
10921
- get(path, ...args) {
10922
- return this.route("GET", path, ...args);
10921
+ get(path2, ...args) {
10922
+ return this.route("GET", path2, ...args);
10923
10923
  }
10924
- post(path, ...args) {
10925
- return this.route("POST", path, ...args);
10924
+ post(path2, ...args) {
10925
+ return this.route("POST", path2, ...args);
10926
10926
  }
10927
- put(path, ...args) {
10928
- return this.route("PUT", path, ...args);
10927
+ put(path2, ...args) {
10928
+ return this.route("PUT", path2, ...args);
10929
10929
  }
10930
- delete(path, ...args) {
10931
- return this.route("DELETE", path, ...args);
10930
+ delete(path2, ...args) {
10931
+ return this.route("DELETE", path2, ...args);
10932
10932
  }
10933
- patch(path, ...args) {
10934
- return this.route("PATCH", path, ...args);
10933
+ patch(path2, ...args) {
10934
+ return this.route("PATCH", path2, ...args);
10935
10935
  }
10936
- head(path, ...args) {
10937
- return this.route("HEAD", path, ...args);
10936
+ head(path2, ...args) {
10937
+ return this.route("HEAD", path2, ...args);
10938
10938
  }
10939
- options(path, ...args) {
10940
- return this.route("OPTIONS", path, ...args);
10939
+ options(path2, ...args) {
10940
+ return this.route("OPTIONS", path2, ...args);
10941
10941
  }
10942
- all(path, ...args) {
10943
- return this.route("*", path, ...args);
10942
+ all(path2, ...args) {
10943
+ return this.route("*", path2, ...args);
10944
10944
  }
10945
10945
  onError(handler) {
10946
10946
  this.errorHandler = handler;
10947
10947
  return this;
10948
10948
  }
10949
- route(method, path, ...args) {
10949
+ route(method, path2, ...args) {
10950
10950
  const handler = args.pop();
10951
10951
  const middlewares = args;
10952
- const segments = this.splitPath(path);
10952
+ const segments = this.splitPath(path2);
10953
10953
  let node = this.root;
10954
10954
  for (const segment of segments) {
10955
10955
  if (segment === "*") {
@@ -10964,10 +10964,10 @@ var Router = class _Router {
10964
10964
  if (middlewares.length > 0) node.middlewares.set(method, middlewares);
10965
10965
  return this;
10966
10966
  }
10967
- ws(path, ...args) {
10967
+ ws(path2, ...args) {
10968
10968
  const handler = args.pop();
10969
10969
  const middlewares = args;
10970
- const segments = this.splitPath(path);
10970
+ const segments = this.splitPath(path2);
10971
10971
  let node = this.wsRoot;
10972
10972
  for (const segment of segments) {
10973
10973
  node = getWsNode(node, segment);
@@ -11026,8 +11026,8 @@ var Router = class _Router {
11026
11026
  });
11027
11027
  };
11028
11028
  }
11029
- splitPath(path) {
11030
- return path.split("/").filter(Boolean);
11029
+ splitPath(path2) {
11030
+ return path2.split("/").filter(Boolean);
11031
11031
  }
11032
11032
  matchTrie(method, segments) {
11033
11033
  let node = this.root;
@@ -12192,9 +12192,9 @@ function tool(def) {
12192
12192
  }
12193
12193
 
12194
12194
  // workflow/reference.ts
12195
- function getByPath(obj, path) {
12195
+ function getByPath(obj, path2) {
12196
12196
  let current = obj;
12197
- for (const key of path) {
12197
+ for (const key of path2) {
12198
12198
  if (current === null || current === void 0) return void 0;
12199
12199
  if (typeof current === "object" && key in current) {
12200
12200
  current = current[key];
@@ -12204,9 +12204,9 @@ function getByPath(obj, path) {
12204
12204
  }
12205
12205
  return current;
12206
12206
  }
12207
- function resolveRef(path, ctx) {
12208
- if (path.startsWith("$nodes.")) {
12209
- const afterNodes = path.slice(7);
12207
+ function resolveRef(path2, ctx) {
12208
+ if (path2.startsWith("$nodes.")) {
12209
+ const afterNodes = path2.slice(7);
12210
12210
  const dotIdx = afterNodes.indexOf(".");
12211
12211
  if (dotIdx === -1) {
12212
12212
  return ctx.nodeOutputs.get(afterNodes);
@@ -12222,23 +12222,23 @@ function resolveRef(path, ctx) {
12222
12222
  }
12223
12223
  return getByPath(output, propPath.split("."));
12224
12224
  }
12225
- if (path.startsWith("$var.")) {
12226
- const name15 = path.slice(5);
12225
+ if (path2.startsWith("$var.")) {
12226
+ const name15 = path2.slice(5);
12227
12227
  if (!ctx.variables.has(name15)) {
12228
12228
  throw new Error(`Variable "${name15}" is not defined`);
12229
12229
  }
12230
12230
  return ctx.variables.get(name15);
12231
12231
  }
12232
- if (path.startsWith("$input.")) {
12233
- const key = path.slice(7);
12232
+ if (path2.startsWith("$input.")) {
12233
+ const key = path2.slice(7);
12234
12234
  return ctx.input[key];
12235
12235
  }
12236
- if (path === "true") return true;
12237
- if (path === "false") return false;
12238
- if (path === "null") return null;
12239
- const num = Number(path);
12240
- if (!isNaN(num) && path.trim() !== "") return num;
12241
- return path;
12236
+ if (path2 === "true") return true;
12237
+ if (path2 === "false") return false;
12238
+ if (path2 === "null") return null;
12239
+ const num = Number(path2);
12240
+ if (!isNaN(num) && path2.trim() !== "") return num;
12241
+ return path2;
12242
12242
  }
12243
12243
  function resolveValue(v, ctx) {
12244
12244
  if (typeof v === "string" && v.startsWith("$")) {
@@ -23884,37 +23884,37 @@ function createOpenAI(options = {}) {
23884
23884
  );
23885
23885
  const createChatModel = (modelId) => new OpenAIChatLanguageModel(modelId, {
23886
23886
  provider: `${providerName}.chat`,
23887
- url: ({ path }) => `${baseURL}${path}`,
23887
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23888
23888
  headers: getHeaders,
23889
23889
  fetch: options.fetch
23890
23890
  });
23891
23891
  const createCompletionModel = (modelId) => new OpenAICompletionLanguageModel(modelId, {
23892
23892
  provider: `${providerName}.completion`,
23893
- url: ({ path }) => `${baseURL}${path}`,
23893
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23894
23894
  headers: getHeaders,
23895
23895
  fetch: options.fetch
23896
23896
  });
23897
23897
  const createEmbeddingModel = (modelId) => new OpenAIEmbeddingModel(modelId, {
23898
23898
  provider: `${providerName}.embedding`,
23899
- url: ({ path }) => `${baseURL}${path}`,
23899
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23900
23900
  headers: getHeaders,
23901
23901
  fetch: options.fetch
23902
23902
  });
23903
23903
  const createImageModel = (modelId) => new OpenAIImageModel(modelId, {
23904
23904
  provider: `${providerName}.image`,
23905
- url: ({ path }) => `${baseURL}${path}`,
23905
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23906
23906
  headers: getHeaders,
23907
23907
  fetch: options.fetch
23908
23908
  });
23909
23909
  const createTranscriptionModel = (modelId) => new OpenAITranscriptionModel(modelId, {
23910
23910
  provider: `${providerName}.transcription`,
23911
- url: ({ path }) => `${baseURL}${path}`,
23911
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23912
23912
  headers: getHeaders,
23913
23913
  fetch: options.fetch
23914
23914
  });
23915
23915
  const createSpeechModel = (modelId) => new OpenAISpeechModel(modelId, {
23916
23916
  provider: `${providerName}.speech`,
23917
- url: ({ path }) => `${baseURL}${path}`,
23917
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23918
23918
  headers: getHeaders,
23919
23919
  fetch: options.fetch
23920
23920
  });
@@ -23929,7 +23929,7 @@ function createOpenAI(options = {}) {
23929
23929
  const createResponsesModel = (modelId) => {
23930
23930
  return new OpenAIResponsesLanguageModel(modelId, {
23931
23931
  provider: `${providerName}.responses`,
23932
- url: ({ path }) => `${baseURL}${path}`,
23932
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23933
23933
  headers: getHeaders,
23934
23934
  fetch: options.fetch,
23935
23935
  fileIdPrefixes: ["file-"]
@@ -24628,6 +24628,540 @@ function messager(options) {
24628
24628
  }
24629
24629
  };
24630
24630
  }
24631
+
24632
+ // deploy/index.ts
24633
+ import { execSync } from "node:child_process";
24634
+ import fs from "node:fs";
24635
+ import path from "node:path";
24636
+
24637
+ // deploy/gateway.ts
24638
+ import WebSocket, { WebSocketServer as WebSocketServer2 } from "ws";
24639
+ function isBareDomain(host, domain) {
24640
+ return host === domain || host === `www.${domain}`;
24641
+ }
24642
+ function matchApp(config, getPort, host, pathname) {
24643
+ for (const [name15, ac] of Object.entries(config.apps)) {
24644
+ if (ac.subdomain && host === `${ac.subdomain}.${config.domain}`) {
24645
+ const port = getPort(name15);
24646
+ if (port) return { name: name15, port };
24647
+ }
24648
+ }
24649
+ const pathApps = Object.entries(config.apps).filter(([, ac]) => ac.path).sort(([, a], [, b]) => (b.path?.length ?? 0) - (a.path?.length ?? 0));
24650
+ for (const [name15, ac] of pathApps) {
24651
+ if (ac.path && pathname.startsWith(ac.path)) {
24652
+ const port = getPort(name15);
24653
+ if (port) return { name: name15, port, stripPath: ac.path };
24654
+ }
24655
+ }
24656
+ if (config.defaultApp && isBareDomain(host, config.domain)) {
24657
+ const port = getPort(config.defaultApp);
24658
+ if (port) return { name: config.defaultApp, port };
24659
+ }
24660
+ return void 0;
24661
+ }
24662
+ function createGateway(config, getPort) {
24663
+ const handler = async (req) => {
24664
+ const url = new URL(req.url);
24665
+ const match = matchApp(config, getPort, url.hostname, url.pathname);
24666
+ if (!match) return new Response("Not Found", { status: 404 });
24667
+ let targetPath = url.pathname;
24668
+ if (match.stripPath && targetPath.startsWith(match.stripPath)) {
24669
+ targetPath = targetPath.slice(match.stripPath.length) || "/";
24670
+ }
24671
+ const target = `http://127.0.0.1:${match.port}${targetPath}${url.search}`;
24672
+ try {
24673
+ const proxyReq = new Request(target, {
24674
+ method: req.method,
24675
+ headers: req.headers,
24676
+ body: req.method !== "GET" && req.method !== "HEAD" ? req.body : null
24677
+ });
24678
+ return await fetch(proxyReq);
24679
+ } catch {
24680
+ return new Response("Bad Gateway", { status: 502 });
24681
+ }
24682
+ };
24683
+ const wss = new WebSocketServer2({ noServer: true });
24684
+ const wsHandler = (req, socket, head) => {
24685
+ const url = new URL(req.url ?? "/", "http://localhost");
24686
+ const host = req.headers.host?.split(":")[0] ?? "";
24687
+ const match = matchApp(config, getPort, host, url.pathname);
24688
+ if (!match) {
24689
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
24690
+ socket.destroy();
24691
+ return;
24692
+ }
24693
+ let targetPath = url.pathname;
24694
+ if (match.stripPath && targetPath.startsWith(match.stripPath)) {
24695
+ targetPath = targetPath.slice(match.stripPath.length) || "/";
24696
+ }
24697
+ const wsUrl = `ws://127.0.0.1:${match.port}${targetPath}${url.search}`;
24698
+ const backendWS = new WebSocket(wsUrl);
24699
+ backendWS.on("open", () => {
24700
+ wss.handleUpgrade(req, socket, head, (clientWS) => {
24701
+ const clientSend = (data) => {
24702
+ clientWS.send(data);
24703
+ };
24704
+ const backendSend = (data) => {
24705
+ backendWS.send(data);
24706
+ };
24707
+ clientWS.on("message", backendSend);
24708
+ backendWS.on("message", clientSend);
24709
+ clientWS.on("close", () => backendWS.close());
24710
+ backendWS.on("close", () => clientWS.close());
24711
+ clientWS.on("error", () => backendWS.close());
24712
+ backendWS.on("error", () => clientWS.close());
24713
+ });
24714
+ });
24715
+ backendWS.on("error", () => {
24716
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
24717
+ socket.destroy();
24718
+ });
24719
+ };
24720
+ return { handler, wsHandler };
24721
+ }
24722
+
24723
+ // deploy/manager.ts
24724
+ import crypto4 from "node:crypto";
24725
+ function createManager(config, apps, manager) {
24726
+ const router = new Router();
24727
+ const auth2 = (req, ctx, next) => {
24728
+ if (!config.deployToken) return next(req, ctx);
24729
+ const header = req.headers.get("authorization") ?? "";
24730
+ const token = header.replace("Bearer ", "");
24731
+ if (token !== config.deployToken) {
24732
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
24733
+ }
24734
+ return next(req, ctx);
24735
+ };
24736
+ router.get("/apps", auth2, () => {
24737
+ const list = Array.from(apps.values()).map((a) => a.status);
24738
+ return Response.json(list);
24739
+ });
24740
+ router.get("/apps/:name", auth2, (req, ctx) => {
24741
+ const app = apps.get(ctx.params.name);
24742
+ if (!app) return new Response("Not Found", { status: 404 });
24743
+ return Response.json(app.status);
24744
+ });
24745
+ router.post("/apps/:name/deploy", auth2, async (req, ctx) => {
24746
+ const app = apps.get(ctx.params.name);
24747
+ if (!app) return new Response("Not Found", { status: 404 });
24748
+ try {
24749
+ await manager.deployApp(ctx.params.name);
24750
+ return Response.json({ success: true });
24751
+ } catch (err) {
24752
+ const msg = err instanceof Error ? err.message : String(err);
24753
+ return Response.json({ error: msg }, { status: 500 });
24754
+ }
24755
+ });
24756
+ router.post("/apps/:name/restart", auth2, async (req, ctx) => {
24757
+ const app = apps.get(ctx.params.name);
24758
+ if (!app) return new Response("Not Found", { status: 404 });
24759
+ try {
24760
+ await manager.deployApp(ctx.params.name);
24761
+ return Response.json({ success: true });
24762
+ } catch (err) {
24763
+ const msg = err instanceof Error ? err.message : String(err);
24764
+ return Response.json({ error: msg }, { status: 500 });
24765
+ }
24766
+ });
24767
+ router.post("/apps/:name/stop", auth2, async (req, ctx) => {
24768
+ const app = apps.get(ctx.params.name);
24769
+ if (!app) return new Response("Not Found", { status: 404 });
24770
+ if (app.process) {
24771
+ app.process.kill("SIGTERM");
24772
+ app.process = null;
24773
+ }
24774
+ app.status = { ...app.status, status: "stopped", pid: void 0 };
24775
+ return Response.json({ success: true });
24776
+ });
24777
+ router.post("/apps/:name/start", auth2, async (req, ctx) => {
24778
+ const app = apps.get(ctx.params.name);
24779
+ if (!app) return new Response("Not Found", { status: 404 });
24780
+ try {
24781
+ await manager.deployApp(ctx.params.name);
24782
+ return Response.json({ success: true });
24783
+ } catch (err) {
24784
+ const msg = err instanceof Error ? err.message : String(err);
24785
+ return Response.json({ error: msg }, { status: 500 });
24786
+ }
24787
+ });
24788
+ router.get("/apps/:name/logs", auth2, (req, ctx) => {
24789
+ const app = apps.get(ctx.params.name);
24790
+ if (!app) return new Response("Not Found", { status: 404 });
24791
+ let index = app.logs.length;
24792
+ let interval;
24793
+ const stream = new ReadableStream({
24794
+ start(controller) {
24795
+ for (const line of app.logs) {
24796
+ controller.enqueue(`data: ${JSON.stringify({ line })}
24797
+
24798
+ `);
24799
+ }
24800
+ interval = setInterval(() => {
24801
+ while (index < app.logs.length) {
24802
+ controller.enqueue(`data: ${JSON.stringify({ line: app.logs[index] })}
24803
+
24804
+ `);
24805
+ index++;
24806
+ }
24807
+ }, 500);
24808
+ },
24809
+ cancel() {
24810
+ if (interval) clearInterval(interval);
24811
+ }
24812
+ });
24813
+ return new Response(stream, {
24814
+ headers: {
24815
+ "Content-Type": "text/event-stream",
24816
+ "Cache-Control": "no-cache"
24817
+ }
24818
+ });
24819
+ });
24820
+ router.post("/reload", auth2, async () => {
24821
+ try {
24822
+ await manager.reloadConfig();
24823
+ return Response.json({ success: true });
24824
+ } catch (err) {
24825
+ const msg = err instanceof Error ? err.message : String(err);
24826
+ return Response.json({ error: msg }, { status: 400 });
24827
+ }
24828
+ });
24829
+ router.post("/webhook", async (req) => {
24830
+ if (config.webhookSecret) {
24831
+ const sig = req.headers.get("x-hub-signature-256") ?? "";
24832
+ const body = await req.clone().text();
24833
+ const hmac = crypto4.createHmac("sha256", config.webhookSecret).update(body).digest("hex");
24834
+ if (sig !== `sha256=${hmac}`) {
24835
+ return Response.json({ error: "invalid signature" }, { status: 401 });
24836
+ }
24837
+ }
24838
+ const payload = await req.json();
24839
+ const repoUrl = payload?.repository?.clone_url ?? payload?.repository?.html_url ?? "";
24840
+ if (!repoUrl) return Response.json({ deployed: [] });
24841
+ const deployed = [];
24842
+ for (const [name15] of apps) {
24843
+ const ac = apps.get(name15)?.config;
24844
+ if (!ac) continue;
24845
+ const repoNorm = ac.repo.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
24846
+ const payloadNorm = repoUrl.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
24847
+ if (payloadNorm.includes(repoNorm)) {
24848
+ await manager.deployApp(name15);
24849
+ deployed.push(name15);
24850
+ }
24851
+ }
24852
+ return Response.json({ deployed });
24853
+ });
24854
+ return router;
24855
+ }
24856
+
24857
+ // deploy/process.ts
24858
+ import { fork } from "node:child_process";
24859
+ function forkApp(opts) {
24860
+ const child = fork(opts.entry, [], {
24861
+ cwd: opts.cwd,
24862
+ env: {
24863
+ ...process.env,
24864
+ ...opts.env,
24865
+ PORT: String(opts.port)
24866
+ },
24867
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
24868
+ });
24869
+ child.stdout?.on("data", (chunk) => {
24870
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
24871
+ opts.onLog?.(line);
24872
+ }
24873
+ });
24874
+ child.stderr?.on("data", (chunk) => {
24875
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
24876
+ opts.onLog?.(`[error] ${line}`);
24877
+ }
24878
+ });
24879
+ return { child, port: opts.port };
24880
+ }
24881
+ function stopProcess(mp, timeout = 1e4) {
24882
+ return new Promise((resolve3) => {
24883
+ const timer = setTimeout(() => {
24884
+ mp.child.kill("SIGKILL");
24885
+ resolve3();
24886
+ }, timeout);
24887
+ mp.child.on("exit", () => {
24888
+ clearTimeout(timer);
24889
+ resolve3();
24890
+ });
24891
+ mp.child.kill("SIGTERM");
24892
+ });
24893
+ }
24894
+ async function healthCheck(port, path2 = "/") {
24895
+ try {
24896
+ const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
24897
+ signal: AbortSignal.timeout(5e3)
24898
+ });
24899
+ return res.ok;
24900
+ } catch {
24901
+ return false;
24902
+ }
24903
+ }
24904
+
24905
+ // deploy/config.ts
24906
+ function defineConfig(config) {
24907
+ if (!config.domain) throw new Error("deploy: domain is required");
24908
+ if (!config.apps || Object.keys(config.apps).length === 0) {
24909
+ throw new Error("deploy: at least one app is required");
24910
+ }
24911
+ for (const [name15, app] of Object.entries(config.apps)) {
24912
+ if (!app.repo) throw new Error(`deploy: app "${name15}" has no repo`);
24913
+ if (!app.entry) throw new Error(`deploy: app "${name15}" has no entry`);
24914
+ if (!app.port) throw new Error(`deploy: app "${name15}" has no port`);
24915
+ }
24916
+ return {
24917
+ port: config.port ?? 80,
24918
+ appsDir: config.appsDir ?? "/opt/weifuwu/apps",
24919
+ ...config
24920
+ };
24921
+ }
24922
+
24923
+ // deploy/index.ts
24924
+ async function deploy(config) {
24925
+ const appsDir = config.appsDir ?? "/opt/weifuwu/apps";
24926
+ const apps = /* @__PURE__ */ new Map();
24927
+ let httpServer;
24928
+ if (!fs.existsSync(appsDir)) {
24929
+ fs.mkdirSync(appsDir, { recursive: true });
24930
+ }
24931
+ async function forkAndCheck(cwd, entry, port, env, onLog, healthEndpoint) {
24932
+ try {
24933
+ const mp = forkApp({ cwd, entry, port, env, onLog });
24934
+ onLog(`[deploy] forked pid ${mp.child.pid} on port ${mp.port}`);
24935
+ const healthy = await healthCheck(port, healthEndpoint ?? "/");
24936
+ if (healthy) onLog("[deploy] health check passed");
24937
+ else onLog("[deploy] health check failed");
24938
+ return mp;
24939
+ } catch (err) {
24940
+ const msg = err instanceof Error ? err.message : String(err);
24941
+ onLog(`[deploy] fork error: ${msg}`);
24942
+ return null;
24943
+ }
24944
+ }
24945
+ function scheduleRestart(name15, runtime) {
24946
+ const delay = Math.min(1e3 * Math.pow(2, runtime.restartCount), 3e4);
24947
+ runtime.restartCount++;
24948
+ runtime.logs.push(`[deploy] auto-restart in ${delay}ms (attempt ${runtime.restartCount})`);
24949
+ runtime.restartTimer = setTimeout(() => initApp(name15), delay);
24950
+ }
24951
+ async function initApp(name15) {
24952
+ const ac = config.apps[name15];
24953
+ if (!ac) return;
24954
+ const old = apps.get(name15);
24955
+ if (old?.restartTimer) {
24956
+ clearTimeout(old.restartTimer);
24957
+ old.restartTimer = void 0;
24958
+ }
24959
+ const appDir = path.join(appsDir, name15);
24960
+ const logs = [];
24961
+ const log = (line) => {
24962
+ logs.push(line);
24963
+ if (logs.length > 1e3) logs.splice(0, logs.length - 1e3);
24964
+ };
24965
+ try {
24966
+ if (fs.existsSync(path.join(appDir, ".git"))) {
24967
+ execSync("git pull", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
24968
+ log("[deploy] git pull done");
24969
+ } else {
24970
+ if (fs.existsSync(appDir)) {
24971
+ fs.rmSync(appDir, { recursive: true });
24972
+ }
24973
+ execSync(`git clone ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
24974
+ log("[deploy] git clone done");
24975
+ if (ac.branch) {
24976
+ execSync(`git checkout ${ac.branch}`, { cwd: appDir, stdio: "pipe", timeout: 3e4 });
24977
+ log(`[deploy] switched to branch ${ac.branch}`);
24978
+ }
24979
+ }
24980
+ } catch (err) {
24981
+ const msg = err instanceof Error ? err.message : String(err);
24982
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
24983
+ log(`[deploy] git error: ${msg}`);
24984
+ if (old?.process) {
24985
+ apps.set(name15, old);
24986
+ }
24987
+ return;
24988
+ }
24989
+ try {
24990
+ execSync("npm install", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
24991
+ log("[deploy] npm install done");
24992
+ } catch (err) {
24993
+ const msg = err instanceof Error ? err.message : String(err);
24994
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
24995
+ log(`[deploy] npm install error: ${msg}`);
24996
+ if (old?.process) apps.set(name15, old);
24997
+ return;
24998
+ }
24999
+ if (ac.buildCommand) {
25000
+ try {
25001
+ execSync(ac.buildCommand, { cwd: appDir, stdio: "pipe", timeout: 12e4 });
25002
+ log("[deploy] build done");
25003
+ } catch (err) {
25004
+ const msg = err instanceof Error ? err.message : String(err);
25005
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
25006
+ log(`[deploy] build error: ${msg}`);
25007
+ if (old?.process) apps.set(name15, old);
25008
+ return;
25009
+ }
25010
+ }
25011
+ let targetPort = ac.port;
25012
+ if (ac.ports && old?.process) {
25013
+ targetPort = old.currentPort === ac.ports[0] ? ac.ports[1] : ac.ports[0];
25014
+ }
25015
+ const mp = await forkAndCheck(appDir, ac.entry, targetPort, ac.env, log, ac.healthEndpoint);
25016
+ if (!mp) {
25017
+ log("[deploy] new process failed to start, keeping old running");
25018
+ if (old?.process) apps.set(name15, old);
25019
+ else {
25020
+ setAppRuntime(name15, ac, logs, { status: "error", port: targetPort, error: "failed to start" });
25021
+ }
25022
+ return;
25023
+ }
25024
+ const runtime = {
25025
+ config: ac,
25026
+ status: { name: name15, status: "running", port: targetPort, subdomain: ac.subdomain, path: ac.path, pid: mp.child.pid ?? void 0 },
25027
+ logs,
25028
+ process: mp.child,
25029
+ currentPort: targetPort,
25030
+ startedAt: Date.now(),
25031
+ restartCount: 0,
25032
+ restartTimer: void 0
25033
+ };
25034
+ apps.set(name15, runtime);
25035
+ mp.child.on("exit", (code, signal) => {
25036
+ runtime.process = null;
25037
+ runtime.status = {
25038
+ ...runtime.status,
25039
+ status: "error",
25040
+ error: `exited (code=${code} signal=${signal})`,
25041
+ pid: void 0
25042
+ };
25043
+ log(`[deploy] process exited code=${code} signal=${signal}`);
25044
+ if (code !== 0 && signal !== "SIGTERM") {
25045
+ scheduleRestart(name15, runtime);
25046
+ }
25047
+ });
25048
+ if (old?.process && old.currentPort !== targetPort) {
25049
+ if (old.restartTimer) clearTimeout(old.restartTimer);
25050
+ log(`[deploy] stopping old process on port ${old.currentPort}`);
25051
+ await stopProcess({ child: old.process, port: old.currentPort });
25052
+ }
25053
+ }
25054
+ function setAppRuntime(name15, ac, logs, overrides) {
25055
+ apps.set(name15, {
25056
+ config: ac,
25057
+ status: { name: name15, ...overrides },
25058
+ logs,
25059
+ process: null,
25060
+ currentPort: overrides.port ?? ac.port,
25061
+ startedAt: null,
25062
+ restartCount: 0,
25063
+ restartTimer: void 0
25064
+ });
25065
+ }
25066
+ for (const name15 of Object.keys(config.apps)) {
25067
+ await initApp(name15);
25068
+ }
25069
+ const getPort = (name15) => apps.get(name15)?.currentPort;
25070
+ const gw = createGateway(config, getPort);
25071
+ const managerRouter = createManager(config, apps, {
25072
+ deployApp: async (name15) => {
25073
+ await initApp(name15);
25074
+ },
25075
+ reloadConfig: async () => {
25076
+ throw new Error("reload not supported, restart the deploy process");
25077
+ }
25078
+ });
25079
+ const fullHandler = async (req, ctx) => {
25080
+ const url = new URL(req.url);
25081
+ if (url.pathname.startsWith("/_deploy")) {
25082
+ const stripped = url.pathname.replace("/_deploy", "") || "/";
25083
+ const rewritten = new URL(stripped + url.search, "http://deploy.local");
25084
+ const rewrittenReq = new Request(rewritten, req);
25085
+ return managerRouter.handler()(rewrittenReq, ctx);
25086
+ }
25087
+ return gw.handler(req, ctx);
25088
+ };
25089
+ if (config.ssl) {
25090
+ ensureCertificates(config);
25091
+ }
25092
+ httpServer = serve(fullHandler, {
25093
+ port: config.port,
25094
+ websocket: gw.wsHandler
25095
+ });
25096
+ const portSuffix = config.port !== 80 ? `:${config.port}` : "";
25097
+ return {
25098
+ stop: async () => {
25099
+ for (const [, app] of apps) {
25100
+ if (app.restartTimer) clearTimeout(app.restartTimer);
25101
+ if (app.process) {
25102
+ await stopProcess({ child: app.process, port: app.currentPort });
25103
+ }
25104
+ }
25105
+ httpServer?.stop();
25106
+ },
25107
+ ready: httpServer.ready,
25108
+ url: `http://${config.domain}${portSuffix}`,
25109
+ apps: {
25110
+ list: () => Array.from(apps.values()).map((a) => a.status),
25111
+ status: (name15) => apps.get(name15)?.status,
25112
+ deploy: async (name15) => {
25113
+ await initApp(name15);
25114
+ },
25115
+ restart: async (name15) => {
25116
+ await initApp(name15);
25117
+ },
25118
+ stop: async (name15) => {
25119
+ const app = apps.get(name15);
25120
+ if (app?.restartTimer) clearTimeout(app.restartTimer);
25121
+ if (app?.process) {
25122
+ await stopProcess({ child: app.process, port: app.currentPort });
25123
+ app.process = null;
25124
+ app.status = { ...app.status, status: "stopped", pid: void 0 };
25125
+ }
25126
+ },
25127
+ start: async (name15) => {
25128
+ await initApp(name15);
25129
+ }
25130
+ }
25131
+ };
25132
+ }
25133
+ function ensureCertificates(config) {
25134
+ const { domain, ssl } = config;
25135
+ if (!ssl) return;
25136
+ const certDir = "/etc/weifuwu/ssl";
25137
+ const certPath = path.join(certDir, `${domain}.pem`);
25138
+ const keyPath = path.join(certDir, `${domain}-key.pem`);
25139
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) return;
25140
+ if (!fs.existsSync(certDir)) {
25141
+ fs.mkdirSync(certDir, { recursive: true });
25142
+ }
25143
+ const acmeHome = path.join(certDir, ".acme.sh");
25144
+ try {
25145
+ execSync("which acme.sh", { stdio: "pipe" });
25146
+ } catch {
25147
+ execSync(
25148
+ `curl -s https://get.acme.sh | sh -s email=${ssl.email}`,
25149
+ { stdio: "pipe", timeout: 6e4 }
25150
+ );
25151
+ }
25152
+ const subdomains = Object.values(config.apps).filter((a) => a.subdomain).map((a) => `${a.subdomain}.${domain}`).join(",");
25153
+ const allDomains = subdomains ? `${domain},${subdomains}` : domain;
25154
+ const acmeSh = path.join(acmeHome, "acme.sh");
25155
+ const staging = ssl.staging ? " --staging" : "";
25156
+ execSync(
25157
+ `${acmeSh} --issue -d ${allDomains} --standalone${staging} --cert-file ${certPath} --key-file ${keyPath}`,
25158
+ { stdio: "pipe", timeout: 12e4 }
25159
+ );
25160
+ execSync(
25161
+ `${acmeSh} --install-cronjob`,
25162
+ { stdio: "pipe", timeout: 3e4 }
25163
+ );
25164
+ }
24631
25165
  export {
24632
25166
  Router,
24633
25167
  TsxContext,
@@ -24638,7 +25172,9 @@ export {
24638
25172
  cors,
24639
25173
  createSSEManager,
24640
25174
  createWorkflowEngine,
25175
+ defineConfig,
24641
25176
  deleteCookie,
25177
+ deploy,
24642
25178
  generateWorkflow,
24643
25179
  getCookies,
24644
25180
  graphql,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",