CG-Feedback-Monitor/server/scripts/packTeamZip.ts
Lars d894c3c8e4 V2-Stand: UX-Dashboard, Personen v2, Starter-Paket und Dexie-Legacy entfernt.
Sichert den lokalen Entwicklungsstand nach Gitea: Nav/Dashboard, Personen-Excel/Archiv, Team-Setup, Assignment-Übergabe und C6/C7/T2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 14:17:25 +02:00

121 lines
4.5 KiB
TypeScript

/**
* Erzeugt ein versandfertiges ZIP der V2-Team-Version (ohne node_modules, .env, Dev-DB, .git).
* Schreibt nach dist-release/ — Ordner ist gitignored; Produktiv wird nicht angefasst.
*
* Usage: npm run pack:zip
*/
import { mkdirSync, existsSync, rmSync, readFileSync } from 'node:fs'
import { dirname, join, resolve, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execFileSync } from 'node:child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const projectRoot = resolve(join(__dirname, '../..'))
const outDir = join(projectRoot, 'dist-release')
const stamp = new Date().toISOString().slice(0, 10)
const zipName = `AssignmentMonitor-Team-${stamp}.zip`
const zipPath = join(outDir, zipName)
const stageName = 'AssigmentMonitorV2'
const stageRoot = join(outDir, '_stage', stageName)
const rootNorm = projectRoot.toLowerCase()
if (!rootNorm.includes('assigmentmonitorv2')) {
throw new Error('[pack:zip] SAFETY: Skript nur innerhalb AssigmentMonitorV2 ausführen.')
}
const EXCLUDE_DIR_NAMES = new Set([
'node_modules',
'.git',
'dist',
'dist-release',
'dist-ssr',
'.vite',
'.claude',
'.vscode',
'server\\data',
'server/data',
'_quarantine',
'_stage',
])
const EXCLUDE_FILE_NAMES = new Set([
'.env',
'.env.local',
'dist-release-log.txt',
])
function shouldSkip(absPath: string): boolean {
const rel = relative(projectRoot, absPath).split(/[/\\]/).join('/')
if (!rel || rel.startsWith('..')) return true
if (rel === 'server/data' || rel.startsWith('server/data/')) return true
if (rel === 'dist-release' || rel.startsWith('dist-release/')) return true
const parts = rel.split('/')
if (parts.some(p => EXCLUDE_DIR_NAMES.has(p) || p.startsWith('_quarantine'))) return true
const base = parts[parts.length - 1] ?? ''
if (EXCLUDE_FILE_NAMES.has(base)) return true
if (base.endsWith('.tsbuildinfo')) return true
if (base.startsWith('.env.') && base.endsWith('.local')) return true
return false
}
async function copyTree(srcDir: string, destDir: string): Promise<number> {
const { readdirSync, statSync, mkdirSync: mkdir, copyFileSync: cp } = await import('node:fs')
let count = 0
mkdir(destDir, { recursive: true })
for (const name of readdirSync(srcDir)) {
const src = join(srcDir, name)
if (shouldSkip(src)) continue
const dest = join(destDir, name)
const st = statSync(src)
if (st.isDirectory()) {
count += await copyTree(src, dest)
} else if (st.isFile()) {
cp(src, dest)
count++
}
}
return count
}
function zipWithPowerShell(sourceFolder: string, targetZip: string): void {
// Compress-Archive braucht den Inhalt so, dass der Root-Ordner im ZIP liegt
const parent = dirname(sourceFolder)
const leaf = sourceFolder.split(/[/\\]/).pop()
const ps = `
$ErrorActionPreference = 'Stop'
if (Test-Path -LiteralPath '${targetZip.replace(/'/g, "''")}') { Remove-Item -LiteralPath '${targetZip.replace(/'/g, "''")}' -Force }
Compress-Archive -Path (Join-Path '${parent.replace(/'/g, "''")}' '${leaf}') -DestinationPath '${targetZip.replace(/'/g, "''")}' -CompressionLevel Optimal
`
execFileSync('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'inherit' })
}
mkdirSync(outDir, { recursive: true })
const stageParent = join(outDir, '_stage')
if (existsSync(stageParent)) rmSync(stageParent, { recursive: true, force: true })
mkdirSync(stageRoot, { recursive: true })
console.log('[pack:zip] Kopiere Projektdateien (ohne DB, .env, node_modules, .git) …')
const files = await copyTree(projectRoot, stageRoot)
console.log(`[pack:zip] ${files} Dateien → Staging`)
// Sicherstellen, dass Einrichtungsdateien im Root der ZIP liegen
if (!existsSync(join(stageRoot, 'EINRICHTUNG.md'))) {
throw new Error('[pack:zip] EINRICHTUNG.md fehlt im Staging — Abbruch.')
}
if (!existsSync(join(stageRoot, 'docs', 'starter', 'starter-structure.json'))) {
throw new Error('[pack:zip] starter-structure.json fehlt — zuerst npm run starter:export')
}
if (!existsSync(join(stageRoot, '.env.example'))) {
throw new Error('[pack:zip] .env.example fehlt')
}
console.log(`[pack:zip] Erzeuge ${zipName} …`)
zipWithPowerShell(stageRoot, zipPath)
rmSync(stageParent, { recursive: true, force: true })
const sizeMb = (readFileSync(zipPath).byteLength / (1024 * 1024)).toFixed(2)
console.log(`[pack:zip] Fertig: ${zipPath}`)
console.log(`[pack:zip] Größe: ${sizeMb} MB`)
console.log('[pack:zip] An Kollegen: ZIP schicken + auf EINRICHTUNG.md verweisen.')
console.log('[pack:zip] Produktiv (AssigmentMonitor) wurde nicht angefasst.')