chore: exclude local resource artifacts from main sync

This commit is contained in:
2026-05-15 10:16:45 +09:00
parent 442879313f
commit d38d022872
504 changed files with 17074 additions and 3642 deletions

View File

@@ -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
View File

0
scripts/capture-feature-screenshot.mjs Executable file → Normal file
View File

0
scripts/capture-fullscreen-toggle-screenshot.mjs Executable file → Normal file
View File

0
scripts/capture-menu-screenshot.mjs Executable file → Normal file
View File

0
scripts/capture-plan-board-mobile-screenshot.mjs Executable file → Normal file
View File

0
scripts/capture-search-command-screenshot.mjs Executable file → Normal file
View File

0
scripts/capture-settings-screenshot.mjs Executable file → Normal file
View File

View 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}`);
}
}

View 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
View 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 });

View File

@@ -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
View 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
View File

0
scripts/worklog-capture-utils.mjs Executable file → Normal file
View File