chore: sync backend and deployment changes
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { inferSourceChangeScreenTitle } from '../src/services/chat-room-service.js';
|
||||
|
||||
const APPLY_FLAG = '--apply';
|
||||
const repoRootPath = path.resolve(process.cwd(), '../../..');
|
||||
const codexLiveRootPath = path.join(repoRootPath, 'resource', 'Codex Live');
|
||||
const genericScreenRootPath = path.join(codexLiveRootPath, 'Codex Live');
|
||||
|
||||
type FeaturePlan = {
|
||||
featureName: string;
|
||||
sourcePath: string;
|
||||
targetLabel: string;
|
||||
targetPath: string;
|
||||
filePaths: string[];
|
||||
};
|
||||
|
||||
function normalizeWhitespace(value: string) {
|
||||
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getScreenLabelFromTitle(title: string) {
|
||||
const segments = String(title ?? '')
|
||||
.split('/')
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return segments.at(-1) ?? '';
|
||||
}
|
||||
|
||||
function extractSourcePathsFromSpec(text: string) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
Array.from(text.matchAll(/`((?:src|etc|docs|public|scripts)\/[^`]+)`/g), (match) => normalizeWhitespace(match[1])),
|
||||
),
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
function containsPattern(values: string[], pattern: RegExp) {
|
||||
return values.some((value) => pattern.test(normalizeWhitespace(value)));
|
||||
}
|
||||
|
||||
function inferScreenLabelFromFeatureMetadata(args: {
|
||||
featureName: string;
|
||||
filePaths: string[];
|
||||
specTexts: string[];
|
||||
}) {
|
||||
const featureName = normalizeWhitespace(args.featureName);
|
||||
const filePaths = args.filePaths.map((value) => normalizeWhitespace(value));
|
||||
const specTexts = args.specTexts.map((value) => normalizeWhitespace(value));
|
||||
|
||||
if (
|
||||
containsPattern(filePaths, /(?:resourceManagerApi|resource-manager-service|resource-manager|ResourceManagementPage)/iu) ||
|
||||
containsPattern([featureName], /(?:resource manager|리소스 관리|리소스 경로|리소스 가이드|이미지 생성 CLI)/iu)
|
||||
) {
|
||||
return '리소스 관리';
|
||||
}
|
||||
|
||||
if (
|
||||
containsPattern(filePaths, /(?:ChatSourceChangesPage|chat-room-service)/iu) ||
|
||||
containsPattern([featureName], /(?:변경 이력|source change|source-changes)/iu)
|
||||
) {
|
||||
return '변경 이력';
|
||||
}
|
||||
|
||||
if (
|
||||
containsPattern(filePaths, /(?:PreviewAppOverlay|PreviewAppWindow|previewRuntime|appUpdate)/iu) ||
|
||||
containsPattern([featureName], /(?:모바일 앱 열기|Preview App)/iu)
|
||||
) {
|
||||
return '모바일 앱 열기';
|
||||
}
|
||||
|
||||
if (
|
||||
containsPattern(filePaths, /(?:MainHeader|HeaderMessageCenter|MainLayout\.css)/iu) ||
|
||||
containsPattern([featureName], /(?:헤더)/iu)
|
||||
) {
|
||||
return '헤더 표시';
|
||||
}
|
||||
|
||||
if (
|
||||
containsPattern(
|
||||
filePaths,
|
||||
/(?:MainChatPanel|ChatConversationView|mainChatPanel|ChatActivityChecklist|chatUtils)/iu,
|
||||
) ||
|
||||
containsPattern(
|
||||
[featureName],
|
||||
/(?:채팅 말풍선|시스템 카드|말풍선|prompt|즉시전송|즉시 접수|답변 이동|활동 로그|첨부 파일|채팅방|MainChatPanel|ChatConversationView|mainChatPanel)/iu,
|
||||
)
|
||||
) {
|
||||
return '채팅 말풍선';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferScreenLabelFromSpec(args: {
|
||||
featureName: string;
|
||||
filePaths: string[];
|
||||
specTexts: string[];
|
||||
}) {
|
||||
const inferredTitle = inferSourceChangeScreenTitle(args.filePaths, 'Codex Live / Codex Live');
|
||||
const inferredLabel = getScreenLabelFromTitle(inferredTitle);
|
||||
|
||||
if (inferredLabel && inferredLabel !== 'Codex Live' && inferredLabel !== '새 대화') {
|
||||
return inferredLabel;
|
||||
}
|
||||
|
||||
const metadataLabel = inferScreenLabelFromFeatureMetadata(args);
|
||||
|
||||
if (metadataLabel) {
|
||||
return metadataLabel;
|
||||
}
|
||||
|
||||
const normalizedFeatureName = normalizeWhitespace(args.featureName);
|
||||
const hasHeaderSpecificFile = args.filePaths.some((filePath) =>
|
||||
/^(?:src\/app\/main\/MainHeader\.(?:ts|tsx)|src\/app\/main\/HeaderMessageCenter\.(?:ts|tsx|css))$/u.test(filePath),
|
||||
);
|
||||
const hasOnlyHeaderLayoutFiles =
|
||||
args.filePaths.length > 0 &&
|
||||
args.filePaths.every((filePath) =>
|
||||
/^(?:src\/app\/main\/MainLayout\.css|src\/app\/main\/HeaderMessageCenter\.css)$/u.test(filePath),
|
||||
);
|
||||
const hasPreviewSpecificFile = args.filePaths.some((filePath) =>
|
||||
/^(?:src\/app\/main\/PreviewAppOverlay\.(?:ts|tsx)|src\/app\/main\/PreviewAppWindow\.(?:ts|tsx)|src\/app\/main\/previewRuntime\.(?:ts|tsx|js)|src\/app\/main\/appUpdate\.(?:ts|tsx|js))$/u.test(
|
||||
filePath,
|
||||
),
|
||||
);
|
||||
|
||||
if (/^(?:preview\b|동영상 preview\b)/iu.test(normalizedFeatureName) || hasPreviewSpecificFile) {
|
||||
return '모바일 앱 열기';
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:헤더|테마|앱 설정|알림 뱃지|헤더 표시)/u.test(normalizedFeatureName) &&
|
||||
(hasHeaderSpecificFile || hasOnlyHeaderLayoutFiles || args.filePaths.length === 0)
|
||||
) {
|
||||
return '헤더 표시';
|
||||
}
|
||||
|
||||
return 'Codex Live';
|
||||
}
|
||||
|
||||
async function exists(targetPath: string) {
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readFeaturePlans() {
|
||||
if (!(await exists(genericScreenRootPath))) {
|
||||
return [] as FeaturePlan[];
|
||||
}
|
||||
|
||||
const featureEntries = await fs.readdir(genericScreenRootPath, { withFileTypes: true });
|
||||
const plans: FeaturePlan[] = [];
|
||||
|
||||
for (const featureEntry of featureEntries) {
|
||||
if (!featureEntry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const featureName = featureEntry.name;
|
||||
const featurePath = path.join(genericScreenRootPath, featureName);
|
||||
const datedEntries = (await fs.readdir(featurePath, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
|
||||
const specTexts: string[] = [];
|
||||
const filePaths = new Set<string>();
|
||||
|
||||
for (const datedEntry of datedEntries) {
|
||||
const specPath = path.join(featurePath, datedEntry, 'docs', 'feature-spec.md');
|
||||
|
||||
if (!(await exists(specPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const specText = await fs.readFile(specPath, 'utf8');
|
||||
specTexts.push(specText);
|
||||
|
||||
extractSourcePathsFromSpec(specText).forEach((filePath) => {
|
||||
filePaths.add(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
const targetLabel = inferScreenLabelFromSpec({
|
||||
featureName,
|
||||
filePaths: Array.from(filePaths),
|
||||
specTexts,
|
||||
});
|
||||
|
||||
plans.push({
|
||||
featureName,
|
||||
sourcePath: featurePath,
|
||||
targetLabel,
|
||||
targetPath: path.join(codexLiveRootPath, targetLabel, featureName),
|
||||
filePaths: Array.from(filePaths),
|
||||
});
|
||||
}
|
||||
|
||||
return plans.sort((left, right) => left.featureName.localeCompare(right.featureName, 'ko'));
|
||||
}
|
||||
|
||||
async function moveDirectoryContents(sourcePath: string, targetPath: string): Promise<void> {
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
|
||||
if (!(await exists(targetPath))) {
|
||||
await fs.rename(sourcePath, targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceEntries = await fs.readdir(sourcePath, { withFileTypes: true });
|
||||
|
||||
for (const sourceEntry of sourceEntries) {
|
||||
const nextSourcePath = path.join(sourcePath, sourceEntry.name);
|
||||
const nextTargetPath = path.join(targetPath, sourceEntry.name);
|
||||
|
||||
if (sourceEntry.isDirectory()) {
|
||||
await moveDirectoryContents(nextSourcePath, nextTargetPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await exists(nextTargetPath)) {
|
||||
throw new Error(`대상 파일이 이미 존재합니다: ${path.relative(repoRootPath, nextTargetPath)}`);
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(nextTargetPath), { recursive: true });
|
||||
await fs.rename(nextSourcePath, nextTargetPath);
|
||||
}
|
||||
|
||||
await fs.rm(sourcePath, { recursive: false });
|
||||
}
|
||||
|
||||
async function applyMoves(plans: FeaturePlan[]) {
|
||||
const applied: Array<{ featureName: string; from: string; to: string }> = [];
|
||||
|
||||
for (const plan of plans) {
|
||||
if (plan.targetLabel === 'Codex Live') {
|
||||
continue;
|
||||
}
|
||||
|
||||
await moveDirectoryContents(plan.sourcePath, plan.targetPath);
|
||||
applied.push({
|
||||
featureName: plan.featureName,
|
||||
from: path.relative(repoRootPath, plan.sourcePath),
|
||||
to: path.relative(repoRootPath, plan.targetPath),
|
||||
});
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
try {
|
||||
const plans = await readFeaturePlans();
|
||||
const movablePlans = plans.filter((plan) => plan.targetLabel !== 'Codex Live');
|
||||
|
||||
const summary = {
|
||||
mode: process.argv.includes(APPLY_FLAG) ? 'apply' : 'dry-run',
|
||||
totalFeatureCount: plans.length,
|
||||
movableFeatureCount: movablePlans.length,
|
||||
groupedTargets: movablePlans.reduce<Record<string, number>>((accumulator, plan) => {
|
||||
accumulator[plan.targetLabel] = (accumulator[plan.targetLabel] ?? 0) + 1;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
moves: movablePlans.map((plan) => ({
|
||||
featureName: plan.featureName,
|
||||
from: path.relative(repoRootPath, plan.sourcePath),
|
||||
to: path.relative(repoRootPath, plan.targetPath),
|
||||
filePaths: plan.filePaths,
|
||||
})),
|
||||
};
|
||||
|
||||
if (!process.argv.includes(APPLY_FLAG)) {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const applied = await applyMoves(plans);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
...summary,
|
||||
appliedCount: applied.length,
|
||||
applied,
|
||||
}, null, 2));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user