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 +44 -0
- package/dist/deploy/config.d.ts +2 -0
- package/dist/deploy/gateway.d.ts +2 -0
- package/dist/deploy/index.d.ts +4 -0
- package/dist/deploy/manager.d.ts +16 -0
- package/dist/deploy/process.d.ts +14 -0
- package/dist/deploy/types.d.ts +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +580 -44
- package/package.json +1 -1
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,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(
|
|
10922
|
-
return this.route("GET",
|
|
10921
|
+
get(path2, ...args) {
|
|
10922
|
+
return this.route("GET", path2, ...args);
|
|
10923
10923
|
}
|
|
10924
|
-
post(
|
|
10925
|
-
return this.route("POST",
|
|
10924
|
+
post(path2, ...args) {
|
|
10925
|
+
return this.route("POST", path2, ...args);
|
|
10926
10926
|
}
|
|
10927
|
-
put(
|
|
10928
|
-
return this.route("PUT",
|
|
10927
|
+
put(path2, ...args) {
|
|
10928
|
+
return this.route("PUT", path2, ...args);
|
|
10929
10929
|
}
|
|
10930
|
-
delete(
|
|
10931
|
-
return this.route("DELETE",
|
|
10930
|
+
delete(path2, ...args) {
|
|
10931
|
+
return this.route("DELETE", path2, ...args);
|
|
10932
10932
|
}
|
|
10933
|
-
patch(
|
|
10934
|
-
return this.route("PATCH",
|
|
10933
|
+
patch(path2, ...args) {
|
|
10934
|
+
return this.route("PATCH", path2, ...args);
|
|
10935
10935
|
}
|
|
10936
|
-
head(
|
|
10937
|
-
return this.route("HEAD",
|
|
10936
|
+
head(path2, ...args) {
|
|
10937
|
+
return this.route("HEAD", path2, ...args);
|
|
10938
10938
|
}
|
|
10939
|
-
options(
|
|
10940
|
-
return this.route("OPTIONS",
|
|
10939
|
+
options(path2, ...args) {
|
|
10940
|
+
return this.route("OPTIONS", path2, ...args);
|
|
10941
10941
|
}
|
|
10942
|
-
all(
|
|
10943
|
-
return this.route("*",
|
|
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,
|
|
10949
|
+
route(method, path2, ...args) {
|
|
10950
10950
|
const handler = args.pop();
|
|
10951
10951
|
const middlewares = args;
|
|
10952
|
-
const segments = this.splitPath(
|
|
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(
|
|
10967
|
+
ws(path2, ...args) {
|
|
10968
10968
|
const handler = args.pop();
|
|
10969
10969
|
const middlewares = args;
|
|
10970
|
-
const segments = this.splitPath(
|
|
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(
|
|
11030
|
-
return
|
|
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,
|
|
12195
|
+
function getByPath(obj, path2) {
|
|
12196
12196
|
let current = obj;
|
|
12197
|
-
for (const key of
|
|
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(
|
|
12208
|
-
if (
|
|
12209
|
-
const afterNodes =
|
|
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 (
|
|
12226
|
-
const name15 =
|
|
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 (
|
|
12233
|
-
const key =
|
|
12232
|
+
if (path2.startsWith("$input.")) {
|
|
12233
|
+
const key = path2.slice(7);
|
|
12234
12234
|
return ctx.input[key];
|
|
12235
12235
|
}
|
|
12236
|
-
if (
|
|
12237
|
-
if (
|
|
12238
|
-
if (
|
|
12239
|
-
const num = Number(
|
|
12240
|
-
if (!isNaN(num) &&
|
|
12241
|
-
return
|
|
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}${
|
|
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}${
|
|
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}${
|
|
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}${
|
|
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}${
|
|
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}${
|
|
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}${
|
|
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,
|