chore: sync backend and deployment changes
This commit is contained in:
95
scripts/guard-staged-assets.mjs
Executable file
95
scripts/guard-staged-assets.mjs
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1';
|
||||
const maxBinarySizeBytes = 5 * 1024 * 1024;
|
||||
const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i;
|
||||
const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i;
|
||||
|
||||
const blockedAssetPrefixes = ['public/assets/'];
|
||||
|
||||
function getStagedPaths() {
|
||||
const output = execFileSync(
|
||||
'git',
|
||||
['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
|
||||
{ cwd: repoRoot, encoding: 'utf8' }
|
||||
);
|
||||
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function toDisplaySize(bytes) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function isBlockedAssetPath(filePath) {
|
||||
return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const stagedPaths = getStagedPaths();
|
||||
const violations = [];
|
||||
|
||||
for (const relativePath of stagedPaths) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
let stats;
|
||||
|
||||
try {
|
||||
stats = statSync(absolutePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!stats.isFile()) continue;
|
||||
|
||||
const baseName = path.basename(relativePath);
|
||||
const isTmpCapture = tmpCapturePattern.test(baseName);
|
||||
const isBinaryAsset = binaryAssetPattern.test(relativePath);
|
||||
const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes;
|
||||
const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath);
|
||||
|
||||
if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue;
|
||||
|
||||
const reasons = [];
|
||||
if (isTmpCapture) reasons.push('temporary capture file');
|
||||
if (isBlockedAsset) reasons.push('asset path under public/assets');
|
||||
if (isOversizedBinary) {
|
||||
reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`);
|
||||
}
|
||||
|
||||
violations.push({
|
||||
relativePath,
|
||||
size: stats.size,
|
||||
reasons,
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length === 0 || allowAssetCommit) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error('Blocked commit: staged asset files need an explicit override.');
|
||||
console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.');
|
||||
console.error('');
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -59,6 +59,11 @@ const CODEX_LIVE_FINISHED_RETENTION_MS = Math.max(
|
||||
const activeCodexExecutions = new Map();
|
||||
const recentCodexExecutions = new Map();
|
||||
|
||||
function resolveCodexLiveModel(value) {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return /^[A-Za-z0-9._:-]+$/u.test(normalized) ? normalized : CODEX_LIVE_MODEL;
|
||||
}
|
||||
|
||||
function createCodexExecutionRecord({ requestId, child, tempDir }) {
|
||||
return {
|
||||
requestId,
|
||||
@@ -134,6 +139,99 @@ function scheduleCodexExecutionCleanup(record) {
|
||||
record.cleanupTimer.unref?.();
|
||||
}
|
||||
|
||||
function normalizeCodexUsageMetricValue(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(value));
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const normalized = Number(value);
|
||||
if (Number.isFinite(normalized)) {
|
||||
return Math.max(0, Math.round(normalized));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractCodexUsageSnapshot(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usage =
|
||||
parsed.usage && typeof parsed.usage === 'object'
|
||||
? parsed.usage
|
||||
: parsed.response &&
|
||||
typeof parsed.response === 'object' &&
|
||||
parsed.response !== null &&
|
||||
parsed.response.usage &&
|
||||
typeof parsed.response.usage === 'object'
|
||||
? parsed.response.usage
|
||||
: null;
|
||||
|
||||
if (!usage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const input = normalizeCodexUsageMetricValue(usage.input_tokens);
|
||||
const output = normalizeCodexUsageMetricValue(usage.output_tokens);
|
||||
const cached = normalizeCodexUsageMetricValue(usage.cached_input_tokens);
|
||||
const reasoning = normalizeCodexUsageMetricValue(usage.reasoning_output_tokens ?? usage.reasoning_tokens);
|
||||
const total =
|
||||
normalizeCodexUsageMetricValue(usage.total_tokens) ??
|
||||
normalizeCodexUsageMetricValue(usage.totalTokens) ??
|
||||
[input ?? 0, output ?? 0].reduce((sum, value) => sum + value, 0);
|
||||
|
||||
if (input === null && output === null && cached === null && reasoning === null && total === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
tokenTotals: {
|
||||
total: total ?? 0,
|
||||
input: input ?? 0,
|
||||
output: output ?? 0,
|
||||
cached: cached ?? 0,
|
||||
reasoning: reasoning ?? 0,
|
||||
},
|
||||
totalTokens: total ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function extractCodexUsageSnapshotFromText(output) {
|
||||
const text = String(output ?? '');
|
||||
|
||||
if (!text.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
const line = lines[index] ?? '';
|
||||
|
||||
if (!line.startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const usageSnapshot = extractCodexUsageSnapshot(JSON.parse(line));
|
||||
|
||||
if (usageSnapshot) {
|
||||
return usageSnapshot;
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed JSON lines during fallback scan
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function finalizeCodexExecution(record) {
|
||||
if (record.completed) {
|
||||
return;
|
||||
@@ -642,6 +740,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
const resourceDir = path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource');
|
||||
const uploadDir = path.join(resourceDir, 'uploads');
|
||||
const codexBin = process.env.SERVER_COMMAND_RUNNER_CODEX_BIN?.trim() || process.env.PLAN_CODEX_BIN?.trim() || 'codex';
|
||||
const codexModel = resolveCodexLiveModel(payload?.model);
|
||||
const configuredIdleTimeoutMs = resolveCodexLiveIdleTimeoutMs(payload?.idleTimeoutSeconds);
|
||||
const configuredMaxExecutionMs = resolveCodexLiveMaxExecutionMs(payload?.maxExecutionSeconds, configuredIdleTimeoutMs);
|
||||
|
||||
@@ -673,13 +772,14 @@ async function runCodexLiveExecution(payload, response) {
|
||||
let stderrTail = '';
|
||||
let jsonLineBuffer = '';
|
||||
let completedText = '';
|
||||
let streamedUsageSnapshot = null;
|
||||
let idleTimer = null;
|
||||
let executionTimer = null;
|
||||
let terminationRequested = false;
|
||||
|
||||
const child = spawn(
|
||||
codexBin,
|
||||
['exec', '--model', CODEX_LIVE_MODEL, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
['exec', '--model', codexModel, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
{
|
||||
cwd: repoPath,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
@@ -703,6 +803,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'started',
|
||||
pid: child.pid ?? null,
|
||||
model: codexModel,
|
||||
configuredIdleTimeoutSeconds: Math.round(configuredIdleTimeoutMs / 1000),
|
||||
configuredMaxExecutionSeconds: Math.round(configuredMaxExecutionMs / 1000),
|
||||
});
|
||||
@@ -785,16 +886,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
}
|
||||
|
||||
const { completedText: nextCompletedText, deltaText } = extractCodexStreamText(parsed);
|
||||
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const usageSnapshot = extractCodexUsageSnapshot(parsed);
|
||||
|
||||
if (deltaText) {
|
||||
refreshIdleTimer();
|
||||
@@ -802,10 +894,28 @@ async function runCodexLiveExecution(payload, response) {
|
||||
type: 'delta',
|
||||
text: deltaText,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (usageSnapshot) {
|
||||
streamedUsageSnapshot = usageSnapshot;
|
||||
refreshIdleTimer();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'usage',
|
||||
usageSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
usageSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
return Boolean(deltaText || usageSnapshot || nextCompletedText || activityLog);
|
||||
};
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
@@ -876,6 +986,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
handleCodexJsonLine(trailingLine);
|
||||
}
|
||||
|
||||
if (!streamedUsageSnapshot) {
|
||||
const fallbackUsageSnapshot = extractCodexUsageSnapshotFromText([stdoutTail, trailingLine].filter(Boolean).join('\n'));
|
||||
|
||||
if (fallbackUsageSnapshot) {
|
||||
streamedUsageSnapshot = fallbackUsageSnapshot;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'usage',
|
||||
usageSnapshot: fallbackUsageSnapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
|
||||
@@ -25,6 +25,53 @@ const mimeTypes = {
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
function canListenOnPort(candidatePort, host = '0.0.0.0') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const probeServer = createServer();
|
||||
|
||||
probeServer.once('error', (error) => {
|
||||
probeServer.close(() => {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'EADDRINUSE') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
probeServer.once('listening', () => {
|
||||
probeServer.close((closeError) => {
|
||||
if (closeError) {
|
||||
reject(closeError);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
probeServer.listen(candidatePort, host);
|
||||
});
|
||||
}
|
||||
|
||||
async function findAvailablePort(initialPort, host = '0.0.0.0', maxAttempts = 20) {
|
||||
for (let offset = 0; offset < maxAttempts; offset += 1) {
|
||||
const candidatePort = initialPort + offset;
|
||||
const available = await canListenOnPort(candidatePort, host);
|
||||
|
||||
if (available) {
|
||||
return candidatePort;
|
||||
}
|
||||
|
||||
if (offset === 0) {
|
||||
console.warn(`Port ${initialPort} is in use, trying another one...`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No available port found from ${initialPort} to ${initialPort + maxAttempts - 1}.`);
|
||||
}
|
||||
|
||||
function resolveCacheControl(resolvedPath, extension) {
|
||||
const normalizedPath = resolvedPath.replace(/\\/g, '/');
|
||||
|
||||
@@ -218,6 +265,9 @@ server.on('upgrade', (request, socket, head) => {
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(`${distDirName} server listening on http://0.0.0.0:${port}`);
|
||||
const host = '0.0.0.0';
|
||||
const resolvedPort = await findAvailablePort(port, host);
|
||||
|
||||
server.listen(resolvedPort, host, () => {
|
||||
console.log(`${distDirName} server listening on http://${host}:${resolvedPort}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user