@aitty/server 0.1.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/dist/cli-token.d.ts +5 -0
- package/dist/cli-token.js +14 -0
- package/dist/frontend/browser-shell.d.ts +34 -0
- package/dist/frontend/browser-shell.html +218 -0
- package/dist/frontend/browser-shell.js +291 -0
- package/dist/frontend/terminal-app.js +4780 -0
- package/dist/frontend/terminal.css +268 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/logging.d.ts +17 -0
- package/dist/logging.js +34 -0
- package/dist/network-policy.d.ts +10 -0
- package/dist/network-policy.js +88 -0
- package/dist/runtime/dependencies.d.ts +16 -0
- package/dist/runtime/dependencies.js +16 -0
- package/dist/runtime/output-buffer.d.ts +11 -0
- package/dist/runtime/output-buffer.js +202 -0
- package/dist/runtime/pty-session.d.ts +59 -0
- package/dist/runtime/pty-session.js +247 -0
- package/dist/runtime/websocket-transport.d.ts +32 -0
- package/dist/runtime/websocket-transport.js +465 -0
- package/dist/server.d.ts +51 -0
- package/dist/server.js +234 -0
- package/dist/theme-source.d.ts +2 -0
- package/dist/theme-source.js +2 -0
- package/package.json +73 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createSessionToken } from "./cli-token.js";
|
|
2
|
+
import { createNetworkPolicy } from "./network-policy.js";
|
|
3
|
+
import { handleBrowserShellRequest } from "./frontend/browser-shell.js";
|
|
4
|
+
import { createStructuredLogger } from "./logging.js";
|
|
5
|
+
import { createPtySession } from "./runtime/pty-session.js";
|
|
6
|
+
import { createWebSocketTransport } from "./runtime/websocket-transport.js";
|
|
7
|
+
import { readThemeSource } from "./theme-source.js";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { constants } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { createAittyPortlessIdentity, normalizeAittyPortlessPathname } from "@aitty/protocol";
|
|
12
|
+
//#region src/server.ts
|
|
13
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
14
|
+
const DEFAULT_PORT = 0;
|
|
15
|
+
const DEFAULT_PORTLESS_PROJECT = "session";
|
|
16
|
+
async function createAittyServer(options, dependencies, io = {}) {
|
|
17
|
+
const stderr = io.stderr ?? process.stderr;
|
|
18
|
+
const token = createSessionToken();
|
|
19
|
+
const logger = createStructuredLogger({
|
|
20
|
+
token,
|
|
21
|
+
verbose: options.verbose,
|
|
22
|
+
writer: stderr
|
|
23
|
+
});
|
|
24
|
+
const sourceTheme = await readThemeSource(options.themeSource).catch((error) => {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
logger.warn(`Failed to read theme source: ${message}`);
|
|
27
|
+
});
|
|
28
|
+
const requestState = {};
|
|
29
|
+
const runtimeKind = options.runtimeKind ?? "pty";
|
|
30
|
+
const shellOptions = {
|
|
31
|
+
...options.shell,
|
|
32
|
+
theme: options.theme ?? sourceTheme ?? options.shell?.theme
|
|
33
|
+
};
|
|
34
|
+
const server = createServer((request, response) => {
|
|
35
|
+
handleBrowserShellRequest(request, response, token, runtimeKind, requestState.networkPolicy, shellOptions).catch((error) => {
|
|
36
|
+
if (!response.writableEnded) {
|
|
37
|
+
response.statusCode = 500;
|
|
38
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
39
|
+
response.end("Internal Server Error");
|
|
40
|
+
}
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
logger.error(`Failed to serve browser request: ${message}`);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
await listen(server, options.host ?? DEFAULT_HOST, options.port ?? DEFAULT_PORT);
|
|
46
|
+
let session;
|
|
47
|
+
try {
|
|
48
|
+
session = createPtySession({
|
|
49
|
+
args: options.args ?? [],
|
|
50
|
+
bufferSize: options.bufferSize,
|
|
51
|
+
cwd: options.cwd,
|
|
52
|
+
env: options.env ?? process.env,
|
|
53
|
+
file: options.command
|
|
54
|
+
}, dependencies.nodePty);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
await closeServer(server);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const address = server.address();
|
|
60
|
+
if (!address || typeof address === "string") {
|
|
61
|
+
session.kill("SIGTERM");
|
|
62
|
+
await closeServer(server);
|
|
63
|
+
throw new Error("Server did not expose a TCP address");
|
|
64
|
+
}
|
|
65
|
+
const host = normalizeHost(address, options.host ?? DEFAULT_HOST);
|
|
66
|
+
const port = address.port;
|
|
67
|
+
const publicHost = options.publicHost ?? host;
|
|
68
|
+
const publicOrigin = options.publicOrigin ?? buildAittyOrigin(publicHost, port);
|
|
69
|
+
const portless = createPortlessIdentity(options, token);
|
|
70
|
+
const url = buildAittyUrl(publicOrigin, token, portless.path);
|
|
71
|
+
requestState.networkPolicy = createNetworkPolicy(host, publicHost, port);
|
|
72
|
+
const websocketTransport = dependencies.WebSocketServer ? createWebSocketTransport({
|
|
73
|
+
WebSocketServer: dependencies.WebSocketServer,
|
|
74
|
+
networkPolicy: requestState.networkPolicy,
|
|
75
|
+
server,
|
|
76
|
+
session,
|
|
77
|
+
stderr,
|
|
78
|
+
themeSource: options.themeSource,
|
|
79
|
+
token,
|
|
80
|
+
verbose: options.verbose
|
|
81
|
+
}) : void 0;
|
|
82
|
+
let finalized = false;
|
|
83
|
+
let childExitHandled = false;
|
|
84
|
+
let requestedShutdownSignal = null;
|
|
85
|
+
let signalHandlersInstalled = false;
|
|
86
|
+
let signalDisposers = [];
|
|
87
|
+
let resolveClosed;
|
|
88
|
+
const closed = new Promise((resolve) => {
|
|
89
|
+
resolveClosed = resolve;
|
|
90
|
+
});
|
|
91
|
+
const finalize = async (exitCode) => {
|
|
92
|
+
if (finalized) return closed;
|
|
93
|
+
finalized = true;
|
|
94
|
+
exitSubscription.dispose();
|
|
95
|
+
if (signalHandlersInstalled) {
|
|
96
|
+
for (const dispose of signalDisposers) dispose();
|
|
97
|
+
signalDisposers = [];
|
|
98
|
+
signalHandlersInstalled = false;
|
|
99
|
+
}
|
|
100
|
+
await websocketTransport?.close();
|
|
101
|
+
await closeServer(server);
|
|
102
|
+
resolveClosed(exitCode);
|
|
103
|
+
return exitCode;
|
|
104
|
+
};
|
|
105
|
+
const handleChildExit = async (event) => {
|
|
106
|
+
if (childExitHandled) return closed;
|
|
107
|
+
childExitHandled = true;
|
|
108
|
+
const presentedExit = applyRequestedShutdownSignal(event, requestedShutdownSignal);
|
|
109
|
+
logger.info(formatChildExit(presentedExit));
|
|
110
|
+
try {
|
|
111
|
+
await websocketTransport?.notifyExit(presentedExit);
|
|
112
|
+
} catch {}
|
|
113
|
+
return finalize(resolveProcessExitCode(presentedExit));
|
|
114
|
+
};
|
|
115
|
+
const exitSubscription = session.onExit((event) => {
|
|
116
|
+
handleChildExit(event);
|
|
117
|
+
});
|
|
118
|
+
const close = async (signal = "SIGTERM") => {
|
|
119
|
+
requestedShutdownSignal ??= signal;
|
|
120
|
+
if (!childExitHandled) await handleChildExit(await session.close(signal));
|
|
121
|
+
return closed;
|
|
122
|
+
};
|
|
123
|
+
if (io.registerSignalHandlers !== false) {
|
|
124
|
+
signalHandlersInstalled = true;
|
|
125
|
+
signalDisposers = ["SIGINT", "SIGTERM"].map((signal) => {
|
|
126
|
+
const handler = () => {
|
|
127
|
+
close(signal);
|
|
128
|
+
};
|
|
129
|
+
process.once(signal, handler);
|
|
130
|
+
return () => {
|
|
131
|
+
process.removeListener(signal, handler);
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
logger.info(`startup bind=${host}:${port} public-origin=${publicOrigin}`);
|
|
136
|
+
logger.debug(`startup command=${options.command} child-pid=${session.pid}`);
|
|
137
|
+
return {
|
|
138
|
+
childPid: session.pid,
|
|
139
|
+
close,
|
|
140
|
+
closed,
|
|
141
|
+
host,
|
|
142
|
+
port,
|
|
143
|
+
portless,
|
|
144
|
+
session,
|
|
145
|
+
token,
|
|
146
|
+
url
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function buildAittyUrl(origin, token, pathname) {
|
|
150
|
+
const url = new URL(normalizeAittyOrigin(origin));
|
|
151
|
+
url.pathname = normalizePortlessPathname(pathname);
|
|
152
|
+
url.search = "";
|
|
153
|
+
url.searchParams.set("t", token);
|
|
154
|
+
return url.toString();
|
|
155
|
+
}
|
|
156
|
+
function buildAittyOrigin(host, port) {
|
|
157
|
+
return `http://${formatUrlHost(host)}:${port}`;
|
|
158
|
+
}
|
|
159
|
+
function normalizeHost(address, configuredHost) {
|
|
160
|
+
if (address.address === "::" || address.address === "0.0.0.0") return configuredHost;
|
|
161
|
+
return address.address;
|
|
162
|
+
}
|
|
163
|
+
function formatUrlHost(host) {
|
|
164
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
165
|
+
}
|
|
166
|
+
function createPortlessIdentity(options, token) {
|
|
167
|
+
return createAittyPortlessIdentity({
|
|
168
|
+
label: options.portless?.label ?? (path.basename(options.command) || "agent"),
|
|
169
|
+
labelFallback: "agent",
|
|
170
|
+
project: options.portless?.project ?? path.basename(options.cwd),
|
|
171
|
+
projectFallback: DEFAULT_PORTLESS_PROJECT,
|
|
172
|
+
token
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function normalizeAittyOrigin(origin) {
|
|
176
|
+
const candidate = origin.trim();
|
|
177
|
+
if (!candidate) throw new Error("Aitty URL origin must not be empty");
|
|
178
|
+
return candidate.endsWith("/") ? candidate : `${candidate}/`;
|
|
179
|
+
}
|
|
180
|
+
const normalizePortlessPathname = normalizeAittyPortlessPathname;
|
|
181
|
+
function applyRequestedShutdownSignal(event, signal) {
|
|
182
|
+
if (!signal) return event;
|
|
183
|
+
return {
|
|
184
|
+
exitCode: event.exitCode,
|
|
185
|
+
signal: resolveSignalNumber(signal) ?? event.signal
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function resolveSignalNumber(signal) {
|
|
189
|
+
const value = constants.signals[signal];
|
|
190
|
+
return typeof value === "number" ? value : void 0;
|
|
191
|
+
}
|
|
192
|
+
function formatChildExit(event) {
|
|
193
|
+
const signalName = resolveSignalName(event.signal);
|
|
194
|
+
if (signalName) return `child exit signal=${signalName}`;
|
|
195
|
+
return `child exit code=${event.exitCode}`;
|
|
196
|
+
}
|
|
197
|
+
function resolveSignalName(signal) {
|
|
198
|
+
if (typeof signal !== "number") return null;
|
|
199
|
+
const signals = constants.signals;
|
|
200
|
+
for (const [name, value] of Object.entries(signals)) if (value === signal) return name;
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function resolveProcessExitCode(event) {
|
|
204
|
+
if (event.signal) return 128 + event.signal;
|
|
205
|
+
return event.exitCode;
|
|
206
|
+
}
|
|
207
|
+
function listen(server, host, port) {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const onError = (error) => {
|
|
210
|
+
server.off("listening", onListening);
|
|
211
|
+
reject(error);
|
|
212
|
+
};
|
|
213
|
+
const onListening = () => {
|
|
214
|
+
server.off("error", onError);
|
|
215
|
+
resolve();
|
|
216
|
+
};
|
|
217
|
+
server.once("error", onError);
|
|
218
|
+
server.once("listening", onListening);
|
|
219
|
+
server.listen(port, host);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function closeServer(server) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
server.close((error) => {
|
|
225
|
+
if (error) {
|
|
226
|
+
reject(error);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolve();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
export { buildAittyOrigin, buildAittyUrl, createAittyServer };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { AittyTheme, AittyThemeControlFrame, AittyThemeSource, AittyThemeSubscription, createThemeControlFrame, normalizeTheme, readThemeSource, subscribeThemeSource } from "@aitty/protocol";
|
|
2
|
+
export { type AittyTheme, type AittyThemeControlFrame, type AittyThemeSource, type AittyThemeSubscription, createThemeControlFrame, normalizeTheme, readThemeSource, subscribeThemeSource };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aitty/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local PTY, HTTP, and WebSocket server runtime for aitty.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"browser",
|
|
9
|
+
"pty",
|
|
10
|
+
"server",
|
|
11
|
+
"terminal",
|
|
12
|
+
"websocket"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/kingsword09/aitty#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/kingsword09/aitty/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"author": "Kingsword kingsword09 <kingsword09@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/kingsword09/aitty.git",
|
|
23
|
+
"directory": "packages/server"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./cli-token": {
|
|
34
|
+
"types": "./dist/cli-token.d.ts",
|
|
35
|
+
"default": "./dist/cli-token.js"
|
|
36
|
+
},
|
|
37
|
+
"./frontend/*": {
|
|
38
|
+
"types": "./dist/frontend/*.d.ts",
|
|
39
|
+
"default": "./dist/frontend/*.js"
|
|
40
|
+
},
|
|
41
|
+
"./logging": {
|
|
42
|
+
"types": "./dist/logging.d.ts",
|
|
43
|
+
"default": "./dist/logging.js"
|
|
44
|
+
},
|
|
45
|
+
"./network-policy": {
|
|
46
|
+
"types": "./dist/network-policy.d.ts",
|
|
47
|
+
"default": "./dist/network-policy.js"
|
|
48
|
+
},
|
|
49
|
+
"./runtime/*": {
|
|
50
|
+
"types": "./dist/runtime/*.d.ts",
|
|
51
|
+
"default": "./dist/runtime/*.js"
|
|
52
|
+
},
|
|
53
|
+
"./server": {
|
|
54
|
+
"types": "./dist/server.d.ts",
|
|
55
|
+
"default": "./dist/server.js"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"dist"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@aitty/protocol": "workspace:*",
|
|
69
|
+
"node-pty": "1.2.0-beta.12",
|
|
70
|
+
"open": "^11.0.0",
|
|
71
|
+
"ws": "^8.20.0"
|
|
72
|
+
}
|
|
73
|
+
}
|