Files
AriWeb-Suite/scripts/import-sources.mjs

73 lines
7.7 KiB
JavaScript

/**
* scripts/import-sources.mjs — 참조 저장소(Webmaker·AriMail)의 소스를 modules/ 로 가져오고 IMPORTED_SOURCES.md manifest 를 만든다.
*
* 원칙(개발 지시 §0·§38)
* - 원본 저장소·커밋을 고정하고, 파일마다 원본 경로·대상 경로·수정 여부(해시 비교)·라이선스 검토 결과를 기록한다.
* - 문서·보고서·샘플·PDF 처럼 실행에 필요 없는 파일은 가져오지 않는다(EXCLUDE).
* - 이미 가져온 파일을 다시 실행해도 Ariweb Suite 쪽 수정본을 덮어쓰지 않는다(수정된 파일은 'modified' 로 표시하고 건너뜀). --force 로 원본으로 되돌릴 수 있다.
*
* 사용: node scripts/import-sources.mjs [--force] [--only webmaker|arimail] [--allow-dirty]
* 원본 위치는 환경 변수 WEBMAKER_SRC / ARIMAIL_SRC (기본: ~/work/WebHoting_AI_Agent, ~/work/mail-gateway 의 git 체크아웃).
* --allow-dirty (0.2.0): 원본 체크아웃이 고정 커밋 위에 아직 커밋되지 않은 변경(작업 트리)을 가진 상태도 받아들인다 — 참조 저장소와 Suite 를 같은 작업자가
* 나란히 개발할 때(예: Webmaker 1.8.0 직접 편집) 쓴다. 추적되지 않은 새 파일도 가져오고, upstream{version·branch·headCommit·workingTreeDirty} 를 함께 기록한다.
* commit 칸은 변하지 않는 기준(불변 baseline) 커밋이며, 작업 트리 변경이 원본 저장소에 커밋되면 pin 을 그 커밋으로 올리고 다시 실행한다.
* --only 로 한 소스만 다시 만들 때 다른 소스의 기존 기록은 그대로 유지한다.
*/
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, copyFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2); const force = args.includes('--force'); const only = args.includes('--only') ? args[args.indexOf('--only') + 1] : null; const allowDirty = args.includes('--allow-dirty');
const SOURCES = [
{ name: 'webmaker', repo: 'https://source.ibit.kr/OZ-Group/WebHosting_AI_agent', src: process.env.WEBMAKER_SRC || join(homedir(), 'work', 'WebHoting_AI_Agent'), dest: 'modules/webmaker', pin: '4b8c4d0de0f69025001a9e0a28a58a94e1a1f088',
exclude: [/^reports\//, /^sample\//, /\.pdf$/, /^Dev_Order/, /^WebHosting_AI_agent_/, /^CHANGELOG\.md$/, /^ERRORS\.md$/, /^\.gitignore$/], license: 'OZ-Group 내부 전용(private: true, 별도 LICENSE 없음) — 같은 조직의 Ariweb Suite 에서 재사용 허용. 외부 배포 불가.' },
{ name: 'arimail', repo: 'https://source.ibit.kr/OZ-Group/AriMail', src: process.env.ARIMAIL_SRC || join(homedir(), 'work', 'mail-gateway'), dest: 'modules/arimail', pin: 'a07f69f1ffb6667de6e66b3d1ec43c1c66fab924',
exclude: [/^CHANGELOG\.md$/, /^\.gitignore$/, /^deploy\/sites\//], license: 'OZ-Group 내부 전용(private: true, 별도 LICENSE 없음) — 같은 조직의 Ariweb Suite 에서 재사용 허용. 외부 배포 불가. haraka/ 는 npm Haraka(MIT) 를 설치 시점에 내려받는 래퍼만 포함.' },
];
const sha = (f) => createHash('sha256').update(readFileSync(f)).digest('hex').slice(0, 16);
const prevManifest = existsSync(join(ROOT, 'IMPORTED_SOURCES.json')) ? JSON.parse(readFileSync(join(ROOT, 'IMPORTED_SOURCES.json'), 'utf8')) : { sources: [] };
const manifest = { generatedAt: new Date().toISOString(), sources: [] };
for (const s of SOURCES) {
if (only && only !== s.name) { const keep = prevManifest.sources.find((x) => x.name === s.name); if (keep) manifest.sources.push(keep); continue; } // --only: 다른 소스의 기존 기록 유지
const head = execSync('git rev-parse HEAD', { cwd: s.src }).toString().trim();
if (head !== s.pin) throw new Error(`${s.name}: 원본 체크아웃 커밋 ${head} 이 고정 커밋 ${s.pin} 과 다릅니다. git -C ${s.src} checkout ${s.pin}`);
const dirty = execSync('git status --porcelain', { cwd: s.src }).toString().trim().length > 0;
if (dirty && !allowDirty) throw new Error(`${s.name}: 원본 체크아웃에 커밋되지 않은 변경이 있습니다. 커밋한 뒤 pin 을 올리거나, 나란히 개발 중이면 --allow-dirty 로 실행하세요`);
// 추적 파일 + (--allow-dirty 일 때) 아직 추적되지 않은 새 파일. .gitignore 대상(data/·node_modules/ 등)은 제외
const lsArgs = allowDirty ? 'git ls-files -z --cached --others --exclude-standard' : 'git ls-files -z';
const files = [...new Set(execSync(lsArgs, { cwd: s.src }).toString().split('\0').filter(Boolean))].sort().filter((f) => !s.exclude.some((re) => re.test(f)));
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: s.src }).toString().trim();
let version = null; try { version = JSON.parse(readFileSync(join(s.src, 'package.json'), 'utf8')).version || null; } catch (_) {}
const entry = { name: s.name, repo: s.repo, commit: head, dest: s.dest, license: s.license,
upstream: { version, branch, headCommit: head, workingTreeDirty: dirty, note: dirty ? '고정 커밋(commit) 위에 커밋되지 않은 작업 트리 변경을 함께 가져왔다. 원본 저장소에 커밋되면 pin 을 그 커밋으로 올려 다시 실행한다.' : '고정 커밋의 깨끗한 체크아웃' },
files: [] };
for (const f of files) {
const from = join(s.src, f); const to = join(ROOT, s.dest, f);
if (!statSync(from).isFile()) continue;
const origHash = sha(from);
let status;
if (!existsSync(to)) { mkdirSync(dirname(to), { recursive: true }); copyFileSync(from, to); status = 'imported'; }
else if (sha(to) === origHash) status = 'unchanged';
else if (force) { copyFileSync(from, to); status = 'restored'; }
else status = 'modified';
entry.files.push({ from: f, to: `${s.dest}/${f}`, status, origHash });
}
manifest.sources.push(entry);
}
// manifest 파일(기계용)과 사람용 표를 함께 쓴다. 사람용 표의 '수정 여부' 는 git 의 현재 상태(해시 비교)로 다시 계산되므로 언제 실행해도 사실과 같다.
writeFileSync(join(ROOT, 'IMPORTED_SOURCES.json'), JSON.stringify(manifest, null, 1));
let md = `# 도입 소스 기록(IMPORTED_SOURCES)\n\n생성: ${manifest.generatedAt} — \`node scripts/import-sources.mjs\` 가 만든다. 파일별 '수정' 은 원본 커밋과 현재 파일의 해시 비교 결과이며, 수정 이유는 VERSION_HISTORY.md 의 "도입 소스" 절과 각 파일 머리 주석의 \`[Ariweb Suite 변경]\` 표시로 설명한다.\n\n`;
for (const e of manifest.sources) {
const mod = e.files.filter((f) => f.status === 'modified').length;
const up = e.upstream ? `- 원본 버전(package.json): ${e.upstream.version || '-'} · 브랜치: \`${e.upstream.branch || '-'}\` · 작업 트리: ${e.upstream.workingTreeDirty ? '**커밋되지 않은 변경 포함(--allow-dirty)** — ' + e.upstream.note : '깨끗함'}\n` : '';
md += `## ${e.name}\n\n- 원본 저장소: ${e.repo}\n- 고정 커밋(불변 기준): \`${e.commit}\`\n${up}- 대상 경로: \`${e.dest}/\`\n- 파일 수: ${e.files.length} (수정 ${mod})\n- 라이선스 검토: ${e.license}\n\n| 원본 경로 | 대상 경로 | 수정 | 원본 해시 |\n|---|---|---|---|\n`;
for (const f of e.files) md += `| ${f.from} | ${f.to} | ${f.status === 'modified' ? '**수정**' : f.status === 'restored' ? '원본 복원' : '그대로'} | ${f.origHash} |\n`;
md += '\n';
}
writeFileSync(join(ROOT, 'IMPORTED_SOURCES.md'), md);
for (const e of manifest.sources) console.log(`${e.name}: ${e.files.length} 파일 (${e.files.filter((f) => f.status === 'imported').length} 새로 가져옴, ${e.files.filter((f) => f.status === 'modified').length} 수정됨)`);