#!/usr/bin/env node /** * Plane-TODO Sync Script * * Synchronizes TODO.md with Plane issues (MITHOME project). * Plane is the authoritative source; TODO.md is a local mirror. * * Usage: * node plane-sync.js --list # list Plane projects * node plane-sync.js --dry-run # preview changes, write nothing * node plane-sync.js --yes # auto-accept recommended fixes * node plane-sync.js # interactive sync * * Config precedence: .env > .mcp.json (mcpServers.plane.env) */ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const { getConfig, loadDotEnv } = require('./scripts/plane/config'); const { PlaneClient } = require('./scripts/plane/client'); const { SECTION_LABEL, parseTodoFile, regenerate } = require('./scripts/plane/todo'); const { buildDiscrepancies, buildFinalSections } = require('./scripts/plane/sync'); const TODO_FILE = path.join(__dirname, 'TODO.md'); function parseArgs() { const args = process.argv.slice(2); const options = { dryRun: false, verbose: false, list: false, yes: false, help: false, projectId: null }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--dry-run') options.dryRun = true; else if (arg === '--verbose') options.verbose = true; else if (arg === '--list') options.list = true; else if (arg === '--yes') options.yes = true; else if (arg === '--help' || arg === '-h') options.help = true; else if (arg === '--project') options.projectId = args[++i]; else if (arg.startsWith('--project=')) options.projectId = arg.split('=')[1]; else { console.error(`❌ Unknown option: ${arg}`); options.help = true; } } return options; } function printHelp() { console.log(` 🔄 Plane-TODO Sync Tool Használat: node plane-sync.js [opciók] Opciók: --list Plane projektek listázása --dry-run Előnézet — nem ír semmit --yes Javasolt javítások automatikus elfogadása (nem interaktív) --project= Projekt ID felülírása (alap: MITHOME) --verbose Részletes kimenet --help Ez a súgó Példa: node plane-sync.js --dry-run --verbose node plane-sync.js --yes `); } function printReport(buildData) { const { mismatches, newIssues, orphans, cancelled } = buildData; if (mismatches.length) { console.log('\n⚠️ Státusz eltérések (Plane vs TODO.md):'); for (const m of mismatches) { console.log(` ${m.id} — Plane: ${SECTION_LABEL[m.toSection]}, TODO.md: ${SECTION_LABEL[m.fromSection]} (${m.name})`); } } if (newIssues.length) { console.log('\n🆕 Új issue-k a Plane-ben (TODO.md-ből hiányoznak):'); for (const n of newIssues) console.log(` ${n.id} — ${n.name}`); } if (orphans.length) { console.log('\n👻 TODO.md-ben szerepel, de Plane-ben NEM létezik:'); for (const o of orphans) console.log(` ${o.id} — ${o.name}`); } if (cancelled.length) { console.log('\n🚫 Cancelled (nem kerülnek a TODO.md-be):'); for (const c of cancelled) console.log(` ${c.id} — ${c.name}`); } } function ask(question, defaultYes) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const suffix = defaultYes ? ' [Y/n]' : ' [y/N]'; return new Promise(resolve => { rl.question(`${question}${suffix}: `, answer => { rl.close(); const a = answer.trim().toLowerCase(); if (a === '') return resolve(defaultYes); resolve(a === 'y' || a === 'yes'); }); }); } // DECISION: non-TTY (CI, piping) behaves like --yes to avoid hanging prompts async function resolveDecisions(buildData, options) { const decisions = { moveToPlane: new Set(), keepOrphan: new Set(), addNew: new Set() }; const nonInteractive = options.yes || !process.stdin.isTTY; for (const m of buildData.mismatches) { const accept = nonInteractive || (await ask(`\n${m.id}: Plane=${SECTION_LABEL[m.toSection]}, TODO.md=${SECTION_LABEL[m.fromSection]}. TODO.md frissítése?`, true)); if (accept) decisions.moveToPlane.add(m.id); } if (buildData.orphans.length) { const list = buildData.orphans.map(o => o.id).join(', '); const keep = nonInteractive || (await ask(`\n${list}: nincs Plane-ben. Megtartás a TODO.md-ben?`, true)); if (keep) buildData.orphans.forEach(o => decisions.keepOrphan.add(o.id)); } if (buildData.newIssues.length) { const list = buildData.newIssues.map(n => n.id).join(', '); const add = nonInteractive || (await ask(`\n${list}: új a Plane-ben. Hozzáadás a TODO.md-hez?`, true)); if (add) buildData.newIssues.forEach(n => decisions.addNew.add(n.id)); } return decisions; } function printSummary(buildData, decisions, modePrefix) { const moved = buildData.mismatches.filter(m => decisions.moveToPlane.has(m.id)).length; const orphansKept = buildData.orphans.filter(o => decisions.keepOrphan.has(o.id)).length; const newAdded = buildData.newIssues.filter(n => decisions.addNew.has(n.id)).length; console.log(`\n${modePrefix}📊 Összegzés: ${moved} áthelyezve, ${newAdded} hozzáadva, ${orphansKept}/${buildData.orphans.length} orphan megtartva`); } async function listProjects(client) { const projects = await client.getProjects(); console.log(`\n📋 Found ${projects.length} Plane projects:\n`); for (const project of projects) { console.log(`${project.identifier} — ${project.name}`); console.log(` ID: ${project.id}`); console.log(''); } } async function syncMain(options, config) { if (!config.apiKey || config.apiKey.includes('your_')) { throw new Error(`PLANE_API_KEY hiányzik. Add meg a .mcp.json-ban vagy .env-ben.`); } if (!config.workspace) throw new Error('PLANE_WORKSPACE_SLUG hiányzik'); const client = new PlaneClient({ ...config, verbose: options.verbose }); const modePrefix = options.dryRun ? '(DRY RUN) ' : ''; console.log(`${modePrefix}🔐 API kapcsolat ellenőrzése...`); await client.getProjects(); console.log(`${modePrefix}✅ API connection validated`); console.log(`${modePrefix}📥 Plane issue-k lekérése (${config.projectIdentifier})...`); const issues = await client.getIssues(config.projectId); console.log(`${modePrefix}📥 ${issues.length} issue található`); const content = fs.readFileSync(TODO_FILE, 'utf8'); const todo = parseTodoFile(content); const buildData = buildDiscrepancies(issues, todo.rows, config.projectIdentifier); printReport(buildData); const totalIssues = buildData.mismatches.length + buildData.newIssues.length + buildData.orphans.length; if (totalIssues === 0) { console.log(`${modePrefix}✅ TODO.md szinkronban van a Plane-nel.`); return; } const decisions = await resolveDecisions(buildData, options); printSummary(buildData, decisions, modePrefix); const final = buildFinalSections(buildData, decisions); const newContent = regenerate(content, final); if (options.dryRun) { console.log(`${modePrefix}🔍 DRY RUN: nincs változtatás írva.`); return; } const backupPath = `${TODO_FILE}.backup.${Date.now()}`; fs.copyFileSync(TODO_FILE, backupPath); fs.writeFileSync(TODO_FILE, newContent); console.log(`${modePrefix}💾 Biztonsági mentés: ${path.basename(backupPath)}`); console.log(`${modePrefix}✅ TODO.md frissítve.`); } async function main() { loadDotEnv(); const options = parseArgs(); if (options.help) { printHelp(); return; } const config = getConfig(); config.projectId = options.projectId || config.projectId; try { const client = new PlaneClient({ ...config, verbose: options.verbose }); if (options.list) { await listProjects(client); return; } await syncMain(options, config); } catch (error) { console.error('❌', error.message); process.exit(1); } } if (require.main === module) { main(); } module.exports = { parseArgs, printHelp, printReport, resolveDecisions, printSummary, syncMain };