codex-webapp 0.1.6 → 0.1.8
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/ACKNOWLEDGEMENTS.md +11 -37
- package/README.ja.md +56 -101
- package/README.md +64 -103
- package/docs/architecture.md +85 -0
- package/docs/clean-release-verification.md +95 -0
- package/docs/codex-app-install.md +5 -0
- package/docs/distribution-boundary.md +37 -0
- package/docs/i18n/README.ko.md +11 -65
- package/docs/i18n/README.zh-CN.md +11 -65
- package/package.json +9 -2
- package/scripts/check-public-package-boundary.mjs +248 -0
- package/scripts/verify-clean-release.mjs +492 -0
- package/src/appServerBridge.js +150 -0
- package/src/appServerMessageCodec.js +12 -0
- package/src/auditEvidenceHook.js +18 -0
- package/src/bridgeEventEnvelope.js +29 -0
- package/src/browserPreload.js +176 -0
- package/src/browserSmoke.js +2 -2
- package/src/codexAppRenderer.js +12 -0
- package/src/codexWeb.js +7 -14
- package/src/commands.js +40 -33
- package/src/electronBridge.js +324 -0
- package/src/localServer.js +184 -0
- package/src/projectionManifest.js +8 -0
- package/src/rendererAssetSource.js +278 -0
- package/docs/assets/codex-webapp-readme.png +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
|
|
6
|
+
import { createAuditEvidenceHook, emitAuditEvidence } from "./auditEvidenceHook.js";
|
|
7
|
+
import { CodexAppServerBridge } from "./appServerBridge.js";
|
|
8
|
+
import {
|
|
9
|
+
bridgeErrorResponse,
|
|
10
|
+
bridgeEvent,
|
|
11
|
+
bridgeResponse,
|
|
12
|
+
parseBridgeFrame,
|
|
13
|
+
serializeBridgeMessage,
|
|
14
|
+
} from "./bridgeEventEnvelope.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_HOST_CONFIG = { id: "local", display_name: "Local", kind: "local" };
|
|
17
|
+
const DEBUG_BRIDGE = process.env.CODEX_WEBAPP_DEBUG_BRIDGE === "1";
|
|
18
|
+
|
|
19
|
+
export function attachElectronBridge(server, { cwd = process.cwd(), codexPath = "codex", appServer = null, auditEvidence = null } = {}) {
|
|
20
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
21
|
+
const state = createBridgeState({ cwd, codexPath, appServer, auditEvidence });
|
|
22
|
+
|
|
23
|
+
server.on("upgrade", (request, socket, head) => {
|
|
24
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
25
|
+
if (url.pathname !== "/__backend/ipc") {
|
|
26
|
+
socket.destroy();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
30
|
+
wss.emit("connection", ws, request);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
wss.on("connection", (ws) => {
|
|
35
|
+
sendInitialState(ws, state);
|
|
36
|
+
ws.on("message", (data) => {
|
|
37
|
+
handleFrame(ws, state, data).catch((error) => {
|
|
38
|
+
debug("frame error", { error: String(error?.message ?? error) });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const closeHttpServer = server.close.bind(server);
|
|
44
|
+
server.close = (callback) => {
|
|
45
|
+
for (const client of wss.clients) {
|
|
46
|
+
client.close();
|
|
47
|
+
}
|
|
48
|
+
wss.close(() => {
|
|
49
|
+
state.appServer?.close?.();
|
|
50
|
+
closeHttpServer(callback);
|
|
51
|
+
});
|
|
52
|
+
return server;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return { wss, state };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createBridgeState({ cwd, codexPath, appServer, auditEvidence }) {
|
|
59
|
+
const workspaceRoot = path.resolve(cwd || process.cwd());
|
|
60
|
+
return {
|
|
61
|
+
cwd: workspaceRoot,
|
|
62
|
+
auditEvidenceHook: createAuditEvidenceHook(auditEvidence),
|
|
63
|
+
appServer:
|
|
64
|
+
appServer ||
|
|
65
|
+
new CodexAppServerBridge({
|
|
66
|
+
codexPath,
|
|
67
|
+
cwd: workspaceRoot,
|
|
68
|
+
onNotification: (message) => {
|
|
69
|
+
debug("app-server notification", { method: message.method });
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
sharedObjects: new Map([
|
|
73
|
+
["host_config", DEFAULT_HOST_CONFIG],
|
|
74
|
+
["remote_connections", []],
|
|
75
|
+
["remote_control_connections", []],
|
|
76
|
+
["active_workspace_root", workspaceRoot],
|
|
77
|
+
]),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sendInitialState(ws, state) {
|
|
82
|
+
for (const [key, value] of state.sharedObjects.entries()) {
|
|
83
|
+
sendEvent(ws, state, { type: "shared-object-updated", key, value });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleFrame(ws, state, data) {
|
|
88
|
+
const frame = parseBridgeFrame(data);
|
|
89
|
+
if (!frame) return;
|
|
90
|
+
const { id, kind, payload } = frame || {};
|
|
91
|
+
debug("frame", { id, kind, payloadType: payload?.type, url: payload?.url });
|
|
92
|
+
emitAuditEvidence(state.auditEvidenceHook, { type: "bridge-frame", kind, payloadType: payload?.type });
|
|
93
|
+
try {
|
|
94
|
+
const result = await handleRequest(ws, state, kind, payload);
|
|
95
|
+
if (id) sendJson(ws, bridgeResponse(id, result));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (id) sendJson(ws, bridgeErrorResponse(id, error));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleRequest(ws, state, kind, payload) {
|
|
102
|
+
if (kind === "view-message") {
|
|
103
|
+
await handleViewMessage(ws, state, payload);
|
|
104
|
+
return { accepted: true };
|
|
105
|
+
}
|
|
106
|
+
if (kind === "worker-message") {
|
|
107
|
+
return { accepted: true };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleViewMessage(ws, state, message) {
|
|
113
|
+
if (!message || typeof message !== "object") return;
|
|
114
|
+
if (message.type === "mcp-request") {
|
|
115
|
+
await sendMcpResponse(ws, state, message);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (message.type === "fetch") {
|
|
119
|
+
sendFetchResponse(ws, state, message);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (message.type === "shared-object-subscribe" && message.key) {
|
|
123
|
+
sendEvent(ws, state, { type: "shared-object-updated", key: message.key, value: state.sharedObjects.get(message.key) });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (message.type === "shared-object-set" && message.key) {
|
|
127
|
+
state.sharedObjects.set(message.key, message.value);
|
|
128
|
+
sendEvent(ws, state, { type: "shared-object-updated", key: message.key, value: message.value });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (message.type === "persisted-atom-sync-request") {
|
|
132
|
+
sendEvent(ws, state, { type: "persisted-atom-sync", state: persistedAtomState() });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (message.type === "persisted-atom-update") {
|
|
136
|
+
sendEvent(ws, state, { type: "persisted-atom-updated", key: message.key, value: message.value, deleted: message.deleted });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sendMcpResponse(ws, state, message) {
|
|
141
|
+
const request = message.request;
|
|
142
|
+
if (!request || typeof request !== "object") return;
|
|
143
|
+
const response = await responseForMcpRequest(state, request);
|
|
144
|
+
sendEvent(ws, state, {
|
|
145
|
+
type: "mcp-response",
|
|
146
|
+
hostId: message.hostId || "local",
|
|
147
|
+
message: response,
|
|
148
|
+
response,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function responseForMcpRequest(state, request) {
|
|
153
|
+
try {
|
|
154
|
+
const result = await state.appServer.request(request.method, request.params || {});
|
|
155
|
+
return { id: request.id, result };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
id: request.id,
|
|
159
|
+
error: {
|
|
160
|
+
code: -32000,
|
|
161
|
+
message: String(error?.message ?? error),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sendFetchResponse(ws, state, request) {
|
|
168
|
+
const endpoint = parseCodexEndpoint(request.url);
|
|
169
|
+
debug("fetch", { endpoint, requestId: request.requestId });
|
|
170
|
+
const body = responseForEndpoint(endpoint, state, request);
|
|
171
|
+
sendEvent(ws, state, {
|
|
172
|
+
type: "fetch-response",
|
|
173
|
+
hostId: request.hostId || "local",
|
|
174
|
+
requestId: request.requestId,
|
|
175
|
+
responseType: "success",
|
|
176
|
+
status: 200,
|
|
177
|
+
headers: { "content-type": "application/json" },
|
|
178
|
+
bodyJsonString: JSON.stringify(body),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseCodexEndpoint(url) {
|
|
183
|
+
const raw = String(url || "");
|
|
184
|
+
if (raw.startsWith("vscode://codex/")) {
|
|
185
|
+
return raw.slice("vscode://codex/".length).replace(/^\/+/, "");
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
return new URL(raw, "http://localhost").pathname.replace(/^\/+/, "");
|
|
189
|
+
} catch {
|
|
190
|
+
return raw.replace(/^\/+/, "");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function responseForEndpoint(endpoint, state, request) {
|
|
195
|
+
switch (endpoint) {
|
|
196
|
+
case "active-workspace-roots":
|
|
197
|
+
return { roots: [state.cwd] };
|
|
198
|
+
case "workspace-root-options":
|
|
199
|
+
return {
|
|
200
|
+
roots: [state.cwd],
|
|
201
|
+
options: [{ path: state.cwd, label: path.basename(state.cwd) || state.cwd }],
|
|
202
|
+
};
|
|
203
|
+
case "workspace-directory-entries":
|
|
204
|
+
return { workspaceRoot: state.cwd, entries: [] };
|
|
205
|
+
case "codex-home":
|
|
206
|
+
return { path: process.env.CODEX_HOME || path.join(homedir(), ".codex") };
|
|
207
|
+
case "git-origins":
|
|
208
|
+
return { origins: [] };
|
|
209
|
+
case "get-global-state":
|
|
210
|
+
return { value: globalStateValue(parseBody(request.body)?.key) };
|
|
211
|
+
case "list-pinned-threads":
|
|
212
|
+
return { threadIds: [] };
|
|
213
|
+
case "extension-info":
|
|
214
|
+
return { version: "0.0.0", appName: "Codex" };
|
|
215
|
+
case "os-info":
|
|
216
|
+
return { platform: process.platform, arch: process.arch, homedir: homedir() };
|
|
217
|
+
case "is-copilot-api-available":
|
|
218
|
+
return { available: false };
|
|
219
|
+
case "user-saved-config":
|
|
220
|
+
return { config: {}, configWriteTarget: null };
|
|
221
|
+
case "codex-command-keymap-state":
|
|
222
|
+
return { bindings: [] };
|
|
223
|
+
case "get-configuration":
|
|
224
|
+
return { value: configurationValue(parseBody(request.body)?.key) };
|
|
225
|
+
case "list-automations":
|
|
226
|
+
return { items: [] };
|
|
227
|
+
case "paths-exist":
|
|
228
|
+
return { paths: [], results: [] };
|
|
229
|
+
case "browser-use-origin-state-read":
|
|
230
|
+
return { allowed: false };
|
|
231
|
+
case "ambient-suggestions":
|
|
232
|
+
return { suggestions: [] };
|
|
233
|
+
case "ide-context":
|
|
234
|
+
return { items: [] };
|
|
235
|
+
case "hooks/list":
|
|
236
|
+
return { data: [{ cwd: state.cwd, hooks: [] }] };
|
|
237
|
+
case "skills/list":
|
|
238
|
+
return { data: [] };
|
|
239
|
+
case "wham/tasks/list":
|
|
240
|
+
return { tasks: [] };
|
|
241
|
+
case "wham/usage":
|
|
242
|
+
return { usage: null };
|
|
243
|
+
case "chrome-native-host-install":
|
|
244
|
+
case "chrome-native-host-uninstall":
|
|
245
|
+
return { ok: true };
|
|
246
|
+
case "ipc-request":
|
|
247
|
+
return handleNestedIpcRequest(state, request);
|
|
248
|
+
default:
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function configurationValue(key) {
|
|
254
|
+
switch (key) {
|
|
255
|
+
case "runCodexInWindowsSubsystemForLinux":
|
|
256
|
+
return false;
|
|
257
|
+
case "conversationDetailMode":
|
|
258
|
+
return "expanded";
|
|
259
|
+
case "followUpQueueMode":
|
|
260
|
+
return "off";
|
|
261
|
+
default:
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function globalStateValue(key) {
|
|
267
|
+
switch (key) {
|
|
268
|
+
case "projectless-thread-ids":
|
|
269
|
+
case "sidebar-chat-thread-order":
|
|
270
|
+
case "thread-workspace-root-hints":
|
|
271
|
+
return [];
|
|
272
|
+
case "sidebar-project-thread-orders":
|
|
273
|
+
return {};
|
|
274
|
+
case "use-copilot-auth-if-available":
|
|
275
|
+
return false;
|
|
276
|
+
default:
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function persistedAtomState() {
|
|
282
|
+
return {
|
|
283
|
+
"codex-command-keymap-state": { bindings: [] },
|
|
284
|
+
"statsig_default_enable_features": { memories: false, realtime_conversation: false },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function handleNestedIpcRequest(state, request) {
|
|
289
|
+
const body = parseBody(request.body);
|
|
290
|
+
const channel = body?.channel || body?.type || "";
|
|
291
|
+
if (channel === "codex_desktop:get-system-theme-variant") return "light";
|
|
292
|
+
if (channel === "codex_desktop:get-build-flavor") return "prod";
|
|
293
|
+
if (channel === "codex_desktop:get-sentry-init-options") return { dsn: null, environment: "browser" };
|
|
294
|
+
if (channel === "codex_desktop:get-fast-mode-rollout-metrics") return null;
|
|
295
|
+
if (channel === "active-workspace-roots") return { roots: [state.cwd] };
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parseBody(body) {
|
|
300
|
+
if (!body) return null;
|
|
301
|
+
if (typeof body === "object") return body;
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(String(body));
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function sendEvent(ws, state, payload) {
|
|
310
|
+
emitAuditEvidence(state?.auditEvidenceHook, { type: "bridge-event", payloadType: payload?.type });
|
|
311
|
+
sendJson(ws, bridgeEvent(payload));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sendJson(ws, message) {
|
|
315
|
+
if (ws.readyState === ws.OPEN) {
|
|
316
|
+
ws.send(serializeBridgeMessage(message));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function debug(label, value) {
|
|
321
|
+
if (DEBUG_BRIDGE) {
|
|
322
|
+
console.error(`[codex-webapp bridge] ${label}`, JSON.stringify(value));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
BROWSER_PRELOAD_ROUTE,
|
|
8
|
+
createStaticRendererAssetSource,
|
|
9
|
+
isUnsafeRendererPath,
|
|
10
|
+
matchRendererAsset,
|
|
11
|
+
prepareRendererAssetSource,
|
|
12
|
+
streamAsset,
|
|
13
|
+
} from "./rendererAssetSource.js";
|
|
14
|
+
import { attachElectronBridge } from "./electronBridge.js";
|
|
15
|
+
import { createProjectionManifest } from "./projectionManifest.js";
|
|
16
|
+
import { extractVersion, MIN_CODEX_VERSION } from "./version.js";
|
|
17
|
+
|
|
18
|
+
const HEALTH_CACHE_MS = 1_000;
|
|
19
|
+
const SPAWN_TIMEOUT_MS = 3_000;
|
|
20
|
+
const NOT_FOUND_BODY = Buffer.from(JSON.stringify({ error: "not_found" }));
|
|
21
|
+
const NOT_FOUND_HEADERS = makeHeaders("application/json; charset=utf-8", NOT_FOUND_BODY);
|
|
22
|
+
const PRELOAD_PATH = new URL("./browserPreload.js", import.meta.url);
|
|
23
|
+
|
|
24
|
+
export async function startLocalServer({
|
|
25
|
+
host,
|
|
26
|
+
port,
|
|
27
|
+
cwd,
|
|
28
|
+
codexPath,
|
|
29
|
+
renderer,
|
|
30
|
+
rendererRoot,
|
|
31
|
+
codexAppAsarPath,
|
|
32
|
+
runtimeCacheRoot,
|
|
33
|
+
appServer,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const staticRenderer =
|
|
36
|
+
renderer ||
|
|
37
|
+
(rendererRoot
|
|
38
|
+
? await createStaticRendererAssetSource({ webviewRoot: rendererRoot })
|
|
39
|
+
: await prepareRendererAssetSource({ asarPath: codexAppAsarPath, cacheRoot: runtimeCacheRoot }));
|
|
40
|
+
const healthReader = createHealthReader({ codexPath });
|
|
41
|
+
const server = createServer(async (request, response) => {
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `${host}:${port}`}`);
|
|
44
|
+
|
|
45
|
+
if (request.method === "GET" && url.pathname === "/api/health") {
|
|
46
|
+
const body = Buffer.from(JSON.stringify({ ...healthReader(), renderer: createProjectionManifest(staticRenderer) }, null, 2));
|
|
47
|
+
send(response, 200, makeHeaders("application/json; charset=utf-8", body), body);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((request.method === "GET" || request.method === "HEAD") && url.pathname === BROWSER_PRELOAD_ROUTE) {
|
|
52
|
+
const preload = await readBrowserPreload();
|
|
53
|
+
send(response, 200, preload.headers, request.method === "HEAD" ? Buffer.alloc(0) : preload.body);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (url.pathname === "/__backend/ipc") {
|
|
58
|
+
const body = Buffer.from(JSON.stringify({ error: "upgrade_required" }));
|
|
59
|
+
send(response, 426, makeHeaders("application/json; charset=utf-8", body), body);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
64
|
+
if (isUnsafeRendererPath(rawPathname(request.url))) {
|
|
65
|
+
send(response, 404, NOT_FOUND_HEADERS, NOT_FOUND_BODY);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const asset = matchRendererAsset(staticRenderer, url.pathname);
|
|
69
|
+
if (!asset) {
|
|
70
|
+
send(response, 404, NOT_FOUND_HEADERS, NOT_FOUND_BODY);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (request.method === "HEAD") {
|
|
74
|
+
send(response, 200, asset.headers, Buffer.alloc(0));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
streamAsset(response, asset, request.headers);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
send(response, 404, NOT_FOUND_HEADERS, NOT_FOUND_BODY);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const body = Buffer.from(JSON.stringify({ error: "server_error", message: String(error?.message ?? error) }));
|
|
84
|
+
send(response, 500, makeHeaders("application/json; charset=utf-8", body), body);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
attachElectronBridge(server, { cwd, codexPath, appServer });
|
|
88
|
+
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
server.once("error", reject);
|
|
91
|
+
server.listen(port, host, resolve);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return server;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let browserPreloadAsset = null;
|
|
98
|
+
|
|
99
|
+
async function readBrowserPreload() {
|
|
100
|
+
if (browserPreloadAsset) return browserPreloadAsset;
|
|
101
|
+
const body = await readFile(PRELOAD_PATH);
|
|
102
|
+
const hash = createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
103
|
+
browserPreloadAsset = {
|
|
104
|
+
body,
|
|
105
|
+
headers: {
|
|
106
|
+
"cache-control": "no-cache",
|
|
107
|
+
"content-length": String(body.length),
|
|
108
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
109
|
+
etag: `"${hash}"`,
|
|
110
|
+
"x-content-type-options": "nosniff",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return browserPreloadAsset;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rawPathname(requestUrl = "/") {
|
|
117
|
+
return String(requestUrl).split("?", 1)[0] || "/";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createHealthReader({ codexPath }) {
|
|
121
|
+
let cachedAt = 0;
|
|
122
|
+
let cachedValue = null;
|
|
123
|
+
return () => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (cachedValue && now - cachedAt < HEALTH_CACHE_MS) {
|
|
126
|
+
return cachedValue;
|
|
127
|
+
}
|
|
128
|
+
cachedValue = readHealth({ codexPath });
|
|
129
|
+
cachedAt = now;
|
|
130
|
+
return cachedValue;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readHealth({ codexPath }) {
|
|
135
|
+
const command = codexPath || "codex";
|
|
136
|
+
const versionResult = runCodex(command, ["--version"]);
|
|
137
|
+
const remoteResult = runCodex(command, ["remote-control", "--help"]);
|
|
138
|
+
const appServerResult = runCodex(command, ["app-server", "--help"]);
|
|
139
|
+
const versionText = `${versionResult.stdout}${versionResult.stderr}`.trim();
|
|
140
|
+
const version = extractVersion(versionText);
|
|
141
|
+
return {
|
|
142
|
+
ok: versionResult.status === 0 && remoteResult.status === 0 && appServerResult.status === 0,
|
|
143
|
+
codex: {
|
|
144
|
+
path: command,
|
|
145
|
+
versionText,
|
|
146
|
+
version,
|
|
147
|
+
minimumVersion: MIN_CODEX_VERSION,
|
|
148
|
+
remoteControlAvailable: remoteResult.status === 0,
|
|
149
|
+
appServerAvailable: appServerResult.status === 0,
|
|
150
|
+
error: versionResult.error || remoteResult.error || appServerResult.error || null,
|
|
151
|
+
},
|
|
152
|
+
server: {
|
|
153
|
+
name: "Codex WebApp",
|
|
154
|
+
mode: "local-first",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runCodex(command, args) {
|
|
160
|
+
const result = spawnSync(command, args, {
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
status: result.status,
|
|
166
|
+
stdout: result.stdout ?? "",
|
|
167
|
+
stderr: result.stderr ?? "",
|
|
168
|
+
error: result.error ? result.error.message : null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeHeaders(contentType, body) {
|
|
173
|
+
return {
|
|
174
|
+
"cache-control": "no-store",
|
|
175
|
+
"content-length": String(body.length),
|
|
176
|
+
"content-type": contentType,
|
|
177
|
+
"x-content-type-options": "nosniff",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function send(response, status, headers, body) {
|
|
182
|
+
response.writeHead(status, headers);
|
|
183
|
+
response.end(body);
|
|
184
|
+
}
|