chore: exclude local resource artifacts from main sync
This commit is contained in:
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const DEFAULT_CAPTURE_STORAGE_KEY = 'work-app.token-access.registered-token';
|
||||
const DEFAULT_CAPTURE_BASE_URL = 'https://test.sm-home.cloud/';
|
||||
const DEFAULT_CAPTURE_BASE_URL = 'https://preview.sm-home.cloud/';
|
||||
|
||||
function stripWrappingQuotes(value) {
|
||||
if (!value) {
|
||||
|
||||
0
scripts/capture-component-screenshot.mjs
Executable file → Normal file
0
scripts/capture-component-screenshot.mjs
Executable file → Normal file
0
scripts/capture-feature-screenshot.mjs
Executable file → Normal file
0
scripts/capture-feature-screenshot.mjs
Executable file → Normal file
0
scripts/capture-fullscreen-toggle-screenshot.mjs
Executable file → Normal file
0
scripts/capture-fullscreen-toggle-screenshot.mjs
Executable file → Normal file
0
scripts/capture-menu-screenshot.mjs
Executable file → Normal file
0
scripts/capture-menu-screenshot.mjs
Executable file → Normal file
0
scripts/capture-plan-board-mobile-screenshot.mjs
Executable file → Normal file
0
scripts/capture-plan-board-mobile-screenshot.mjs
Executable file → Normal file
0
scripts/capture-search-command-screenshot.mjs
Executable file → Normal file
0
scripts/capture-search-command-screenshot.mjs
Executable file → Normal file
0
scripts/capture-settings-screenshot.mjs
Executable file → Normal file
0
scripts/capture-settings-screenshot.mjs
Executable file → Normal file
25
scripts/prepare-app-dist.mjs
Normal file
25
scripts/prepare-app-dist.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const outDir = resolve(cwd, process.env.APP_DIST_DIR?.trim() || 'app-dist');
|
||||
const backupPrefix = 'assets.root-owned-backup-';
|
||||
|
||||
if (!existsSync(outDir)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(outDir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith(backupPrefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPath = join(outDir, entry.name);
|
||||
|
||||
try {
|
||||
rmSync(targetPath, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[prepare-app-dist] skipped ${basename(targetPath)}: ${message}`);
|
||||
}
|
||||
}
|
||||
185
scripts/preview-test-app-watch.mjs
Normal file
185
scripts/preview-test-app-watch.mjs
Normal file
@@ -0,0 +1,185 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const viteBin = resolve(rootDir, 'node_modules/vite/bin/vite.js');
|
||||
const serveScript = resolve(rootDir, 'scripts/serve-app-dist.mjs');
|
||||
const appDistDir = '/tmp/ai-code-test-app-dist';
|
||||
const buildEnv = {
|
||||
...process.env,
|
||||
VITE_FILTER_PUBLIC_DIR: 'true',
|
||||
VITE_DISABLE_MODULE_PRELOAD: 'true',
|
||||
};
|
||||
const serveEnv = {
|
||||
...process.env,
|
||||
APP_DIST_DIR: appDistDir,
|
||||
};
|
||||
|
||||
function log(message) {
|
||||
console.log(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`);
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
console.error(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`);
|
||||
}
|
||||
|
||||
function runNodeScript(args, env, options = {}) {
|
||||
return spawn(process.execPath, args, {
|
||||
cwd: rootDir,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function pipePrefixedLines(stream, writer, prefix, onLine) {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = '';
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
const lines = buffer.split(/\r?\n/u);
|
||||
buffer = lines.pop() ?? '';
|
||||
|
||||
for (const line of lines) {
|
||||
onLine?.(line);
|
||||
writer.write(`${prefix}${line}\n`);
|
||||
}
|
||||
});
|
||||
stream.on('end', () => {
|
||||
if (!buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
onLine?.(buffer);
|
||||
writer.write(`${prefix}${buffer}\n`);
|
||||
buffer = '';
|
||||
});
|
||||
}
|
||||
|
||||
function attachWatchBuildLogging(child) {
|
||||
let rebuildStartedAt = null;
|
||||
let serviceWorkerBuildPending = false;
|
||||
|
||||
const handleLine = (line) => {
|
||||
const normalized = line.replace(/\u001b\[[0-9;]*m/g, '').trim();
|
||||
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized.startsWith('Building src/sw.js service worker')) {
|
||||
serviceWorkerBuildPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized.includes('building client environment for production')) {
|
||||
if (serviceWorkerBuildPending) {
|
||||
serviceWorkerBuildPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
rebuildStartedAt = Date.now();
|
||||
log('watch rebuild started');
|
||||
return;
|
||||
}
|
||||
|
||||
const builtMatch = normalized.match(/^built in (\d+)ms\.$/u);
|
||||
if (!builtMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reportedMs = Number.parseInt(builtMatch[1], 10);
|
||||
const wallMs = rebuildStartedAt === null ? null : Date.now() - rebuildStartedAt;
|
||||
const durationText = wallMs === null ? `vite=${reportedMs}ms` : `vite=${reportedMs}ms, wall=${wallMs}ms`;
|
||||
log(`watch rebuild completed (${durationText})`);
|
||||
rebuildStartedAt = null;
|
||||
};
|
||||
|
||||
pipePrefixedLines(child.stdout, process.stdout, '[preview:watch:vite] ', handleLine);
|
||||
pipePrefixedLines(child.stderr, process.stderr, '[preview:watch:vite] ', handleLine);
|
||||
}
|
||||
|
||||
function waitForExit(child) {
|
||||
return new Promise((resolveExit, rejectExit) => {
|
||||
child.once('error', rejectExit);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolveExit();
|
||||
return;
|
||||
}
|
||||
|
||||
rejectExit(new Error(`process exited with code ${code ?? 'null'}${signal ? ` (signal: ${signal})` : ''}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function terminateChild(child, signal = 'SIGTERM') {
|
||||
if (child.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
// Ignore termination races during shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundChildren = [];
|
||||
let shuttingDown = false;
|
||||
|
||||
function shutdown(exitCode = 0) {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
shuttingDown = true;
|
||||
backgroundChildren.forEach((child) => terminateChild(child));
|
||||
setTimeout(() => {
|
||||
backgroundChildren.forEach((child) => terminateChild(child, 'SIGKILL'));
|
||||
process.exit(exitCode);
|
||||
}, 5_000).unref();
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => shutdown(0));
|
||||
process.on('SIGTERM', () => shutdown(0));
|
||||
|
||||
try {
|
||||
log('initial build started');
|
||||
await waitForExit(runNodeScript([viteBin, 'build', '--outDir', appDistDir], buildEnv));
|
||||
log('initial build completed');
|
||||
|
||||
const watchBuild = runNodeScript([viteBin, 'build', '--watch', '--clearScreen', 'false', '--outDir', appDistDir], buildEnv, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
});
|
||||
const previewServer = runNodeScript([serveScript], serveEnv);
|
||||
backgroundChildren.push(watchBuild, previewServer);
|
||||
attachWatchBuildLogging(watchBuild);
|
||||
log('preview server and build watcher are running');
|
||||
|
||||
watchBuild.once('exit', (code, signal) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
logError(`build watcher stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`);
|
||||
shutdown(code && code > 0 ? code : 1);
|
||||
});
|
||||
|
||||
previewServer.once('exit', (code, signal) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
logError(`preview server stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`);
|
||||
shutdown(code && code > 0 ? code : 1);
|
||||
});
|
||||
} catch (error) {
|
||||
logError('failed to prepare preview build');
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
3
scripts/run-plan-codex-once.mjs
Executable file → Normal file
3
scripts/run-plan-codex-once.mjs
Executable file → Normal file
@@ -562,10 +562,11 @@ async function wait(ms) {
|
||||
}
|
||||
|
||||
async function prepareWritableCodexHome(tempDir) {
|
||||
const writableCodexHome = process.env.CODEX_HOME?.trim() || path.join(tempDir, '.codex');
|
||||
const writableCodexHome = path.join(tempDir, '.codex');
|
||||
const sourceCodexHome =
|
||||
process.env.PLAN_CODEX_TEMPLATE_HOME?.trim() ||
|
||||
process.env.CODEX_HOME_TEMPLATE?.trim() ||
|
||||
process.env.CODEX_HOME?.trim() ||
|
||||
path.join(process.env.HOME ?? '/root', '.codex');
|
||||
|
||||
await mkdir(writableCodexHome, { recursive: true });
|
||||
|
||||
@@ -51,7 +51,100 @@ const CODEX_HOME_RUNTIME_PATHS = [
|
||||
'version.json',
|
||||
];
|
||||
const CHAT_SESSION_RESOURCE_DIR_MODE = 0o777;
|
||||
const CODEX_LIVE_EVENT_HISTORY_LIMIT = 400;
|
||||
const CODEX_LIVE_FINISHED_RETENTION_MS = Math.max(
|
||||
60_000,
|
||||
Number(process.env.SERVER_COMMAND_RUNNER_CODEX_FINISHED_RETENTION_MS?.trim() || `${10 * 60 * 1000}`),
|
||||
);
|
||||
const activeCodexExecutions = new Map();
|
||||
const recentCodexExecutions = new Map();
|
||||
|
||||
function createCodexExecutionRecord({ requestId, child, tempDir }) {
|
||||
return {
|
||||
requestId,
|
||||
child,
|
||||
tempDir,
|
||||
subscribers: new Set(),
|
||||
history: [],
|
||||
completed: false,
|
||||
cleanupTimer: null,
|
||||
};
|
||||
}
|
||||
|
||||
function registerSubscriberClose(record, response) {
|
||||
response.on('close', () => {
|
||||
record.subscribers.delete(response);
|
||||
});
|
||||
}
|
||||
|
||||
function attachCodexExecutionSubscriber(record, response) {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/x-ndjson; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
});
|
||||
record.subscribers.add(response);
|
||||
registerSubscriberClose(record, response);
|
||||
|
||||
for (const payload of record.history) {
|
||||
sendJsonLine(response, payload);
|
||||
}
|
||||
|
||||
if (record.completed) {
|
||||
response.end();
|
||||
record.subscribers.delete(response);
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastCodexExecutionEvent(record, payload) {
|
||||
record.history.push(payload);
|
||||
if (record.history.length > CODEX_LIVE_EVENT_HISTORY_LIMIT) {
|
||||
record.history.splice(0, record.history.length - CODEX_LIVE_EVENT_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
for (const response of record.subscribers) {
|
||||
try {
|
||||
sendJsonLine(response, payload);
|
||||
} catch {
|
||||
record.subscribers.delete(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeCodexExecutionSubscribers(record) {
|
||||
for (const response of record.subscribers) {
|
||||
try {
|
||||
response.end();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
record.subscribers.clear();
|
||||
}
|
||||
|
||||
function scheduleCodexExecutionCleanup(record) {
|
||||
if (record.cleanupTimer) {
|
||||
clearTimeout(record.cleanupTimer);
|
||||
}
|
||||
|
||||
record.cleanupTimer = setTimeout(async () => {
|
||||
recentCodexExecutions.delete(record.requestId);
|
||||
await rm(record.tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}, CODEX_LIVE_FINISHED_RETENTION_MS);
|
||||
record.cleanupTimer.unref?.();
|
||||
}
|
||||
|
||||
function finalizeCodexExecution(record) {
|
||||
if (record.completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
record.completed = true;
|
||||
activeCodexExecutions.delete(record.requestId);
|
||||
recentCodexExecutions.set(record.requestId, record);
|
||||
closeCodexExecutionSubscribers(record);
|
||||
scheduleCodexExecutionCleanup(record);
|
||||
}
|
||||
|
||||
async function ensureWorldWritableDirectory(absolutePath) {
|
||||
await mkdir(absolutePath, { recursive: true, mode: CHAT_SESSION_RESOURCE_DIR_MODE });
|
||||
@@ -559,6 +652,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingExecution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);
|
||||
|
||||
if (existingExecution) {
|
||||
attachCodexExecutionSubscriber(existingExecution, response);
|
||||
broadcastCodexExecutionEvent(existingExecution, {
|
||||
type: 'attached',
|
||||
requestId,
|
||||
completed: existingExecution.completed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await validateCodexExecutionRuntime(repoPath, codexBin);
|
||||
await ensureWritableChatSessionDirectories(repoPath, sessionId);
|
||||
|
||||
@@ -568,16 +673,10 @@ async function runCodexLiveExecution(payload, response) {
|
||||
let stderrTail = '';
|
||||
let jsonLineBuffer = '';
|
||||
let completedText = '';
|
||||
let responseClosed = false;
|
||||
let idleTimer = null;
|
||||
let executionTimer = null;
|
||||
let terminationRequested = false;
|
||||
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/x-ndjson; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
});
|
||||
|
||||
const child = spawn(
|
||||
codexBin,
|
||||
['exec', '--model', CODEX_LIVE_MODEL, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
@@ -594,22 +693,20 @@ async function runCodexLiveExecution(payload, response) {
|
||||
},
|
||||
);
|
||||
|
||||
activeCodexExecutions.set(requestId, {
|
||||
const executionRecord = createCodexExecutionRecord({
|
||||
requestId,
|
||||
child,
|
||||
tempDir,
|
||||
});
|
||||
sendJsonLine(response, {
|
||||
activeCodexExecutions.set(requestId, executionRecord);
|
||||
attachCodexExecutionSubscriber(executionRecord, response);
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'started',
|
||||
pid: child.pid ?? null,
|
||||
configuredIdleTimeoutSeconds: Math.round(configuredIdleTimeoutMs / 1000),
|
||||
configuredMaxExecutionSeconds: Math.round(configuredMaxExecutionMs / 1000),
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
activeCodexExecutions.delete(requestId);
|
||||
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
};
|
||||
|
||||
const clearExecutionTimers = () => {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
@@ -630,14 +727,10 @@ async function runCodexLiveExecution(payload, response) {
|
||||
terminationRequested = true;
|
||||
clearExecutionTimers();
|
||||
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message,
|
||||
});
|
||||
response.end();
|
||||
responseClosed = true;
|
||||
}
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message,
|
||||
});
|
||||
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
@@ -685,7 +778,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
if (activityLog) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'activity',
|
||||
line: activityLog,
|
||||
});
|
||||
@@ -696,7 +789,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
});
|
||||
@@ -705,7 +798,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
if (deltaText) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'delta',
|
||||
text: deltaText,
|
||||
});
|
||||
@@ -715,11 +808,6 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return false;
|
||||
};
|
||||
|
||||
response.on('close', () => {
|
||||
responseClosed = true;
|
||||
clearExecutionTimers();
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
refreshIdleTimer();
|
||||
const text = String(chunk);
|
||||
@@ -740,7 +828,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
}
|
||||
|
||||
if (!line.startsWith('{') && !isIgnorableCodexDiagnosticLine(line)) {
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'stdout',
|
||||
line,
|
||||
});
|
||||
@@ -761,7 +849,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'stderr',
|
||||
line,
|
||||
});
|
||||
@@ -770,15 +858,15 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
child.on('error', async (error) => {
|
||||
clearExecutionTimers();
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
|
||||
await cleanup();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'finished',
|
||||
exitCode: null,
|
||||
});
|
||||
finalizeCodexExecution(executionRecord);
|
||||
});
|
||||
|
||||
child.on('close', async (code) => {
|
||||
@@ -788,18 +876,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
handleCodexJsonLine(trailingLine);
|
||||
}
|
||||
|
||||
if (!responseClosed) {
|
||||
if (code !== 0) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message: summarizeCodexOutput(`${stderrTail}\n${completedText}\n${stdoutTail}`),
|
||||
});
|
||||
}
|
||||
|
||||
response.end();
|
||||
if (code !== 0) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message: summarizeCodexOutput(`${stderrTail}\n${completedText}\n${stdoutTail}`),
|
||||
});
|
||||
}
|
||||
|
||||
await cleanup();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'finished',
|
||||
exitCode: code ?? null,
|
||||
});
|
||||
finalizeCodexExecution(executionRecord);
|
||||
});
|
||||
|
||||
child.stdin?.end(prompt);
|
||||
@@ -958,6 +1046,28 @@ const server = createServer(async (request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/attach$/);
|
||||
if (request.method === 'POST' && attachMatch) {
|
||||
const requestId = decodeURIComponent(attachMatch[1]);
|
||||
const execution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);
|
||||
|
||||
if (!execution) {
|
||||
sendJson(response, 404, {
|
||||
attached: false,
|
||||
message: '재부착할 Codex 작업을 찾지 못했습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
attachCodexExecutionSubscriber(execution, response);
|
||||
broadcastCodexExecutionEvent(execution, {
|
||||
type: 'attached',
|
||||
requestId,
|
||||
completed: execution.completed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cancelMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/cancel$/);
|
||||
if (request.method === 'POST' && cancelMatch) {
|
||||
const requestId = decodeURIComponent(cancelMatch[1]);
|
||||
|
||||
67
scripts/serve-app-dist.mjs
Executable file → Normal file
67
scripts/serve-app-dist.mjs
Executable file → Normal file
@@ -1,13 +1,15 @@
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import { extname, isAbsolute, join, normalize } from 'node:path';
|
||||
import { createServer } from 'node:http';
|
||||
import { connect as connectNet } from 'node:net';
|
||||
import { Readable } from 'node:stream';
|
||||
import { connect as connectTls } from 'node:tls';
|
||||
|
||||
const port = Number(process.env.PORT ?? 5173);
|
||||
const distDirName = process.env.APP_DIST_DIR ?? 'app-dist';
|
||||
const rootDir = normalize(isAbsolute(distDirName) ? distDirName : join(process.cwd(), distDirName));
|
||||
const workServerUrl = new URL(process.env.WORK_SERVER_URL ?? 'http://127.0.0.1:3100');
|
||||
const proxyPrefixes = ['/api', '/.codex_chat'];
|
||||
const proxyPrefixes = ['/api', '/.codex_chat', '/ws/chat'];
|
||||
|
||||
const mimeTypes = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
@@ -65,7 +67,8 @@ function resolvePath(urlPath) {
|
||||
}
|
||||
|
||||
function shouldProxyRequest(urlPath = '/') {
|
||||
return proxyPrefixes.some((prefix) => urlPath === prefix || urlPath.startsWith(`${prefix}/`));
|
||||
const normalizedPath = urlPath.split('?')[0] ?? urlPath;
|
||||
return proxyPrefixes.some((prefix) => normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`));
|
||||
}
|
||||
|
||||
function readRequestBody(request) {
|
||||
@@ -155,6 +158,66 @@ const server = createServer(async (request, response) => {
|
||||
createReadStream(resolvedPath).pipe(response);
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!shouldProxyRequest(request.url ?? '/')) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const upstreamPort = Number(
|
||||
workServerUrl.port || (workServerUrl.protocol === 'https:' ? '443' : '80'),
|
||||
);
|
||||
const upstreamSocket =
|
||||
workServerUrl.protocol === 'https:'
|
||||
? connectTls(upstreamPort, workServerUrl.hostname, { servername: workServerUrl.hostname })
|
||||
: connectNet(upstreamPort, workServerUrl.hostname);
|
||||
|
||||
upstreamSocket.on('connect', () => {
|
||||
const headerLines = Object.entries(request.headers)
|
||||
.flatMap(([key, value]) => {
|
||||
if (value == null || key.toLowerCase() === 'host') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => `${key}: ${item}\r\n`)
|
||||
: [`${key}: ${value}\r\n`];
|
||||
})
|
||||
.join('');
|
||||
|
||||
const requestLine = `${request.method ?? 'GET'} ${request.url ?? '/'} HTTP/${request.httpVersion}\r\n`;
|
||||
upstreamSocket.write(`${requestLine}host: ${workServerUrl.host}\r\n${headerLines}\r\n`);
|
||||
|
||||
if (head?.length) {
|
||||
upstreamSocket.write(head);
|
||||
}
|
||||
});
|
||||
|
||||
upstreamSocket.on('data', (chunk) => {
|
||||
socket.write(chunk);
|
||||
});
|
||||
|
||||
upstreamSocket.on('end', () => {
|
||||
socket.end();
|
||||
});
|
||||
|
||||
upstreamSocket.on('error', () => {
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
upstreamSocket.write(chunk);
|
||||
});
|
||||
|
||||
socket.on('end', () => {
|
||||
upstreamSocket.end();
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
upstreamSocket.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(`${distDirName} server listening on http://0.0.0.0:${port}`);
|
||||
});
|
||||
|
||||
0
scripts/server-command-runner-supervisor.sh
Executable file → Normal file
0
scripts/server-command-runner-supervisor.sh
Executable file → Normal file
0
scripts/worklog-capture-utils.mjs
Executable file → Normal file
0
scripts/worklog-capture-utils.mjs
Executable file → Normal file
Reference in New Issue
Block a user