#!/usr/bin/env node import { execSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' const DRY_RUN = process.argv.includes('--dry-run') const SKIP_CREATED = process.argv.includes('--skip-created') const SKIP_STATUS = process.argv.includes('--skip-status') const STATUS_MAP = { active: 'active', Active: 'active', archive: 'archived', '进行中': 'active', '待执行': 'draft', '完成': 'done', 'needs-review': 'active', conditional: 'draft', accepted: 'done', } const PARA_DIRS = ['00_Inbox', '01_Projects', '02_Areas', '03_Resources'] function getActiveNotes() { const results = [] for (const dir of PARA_DIRS) { try { const out = execSync(`find ${dir} -name '*.md' -type f`, { encoding: 'utf8' }) results.push(...out.split('\n').filter(Boolean)) } catch { /* dir may not exist */ } } return results } function getGitCreatedDate(filepath) { try { const out = execSync( `git log --follow --format=%ai -- "${filepath}" | tail -1`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } ).trim() if (out) return out.split(' ')[0] } catch { /* no git history */ } return null } function getFileMtime(filepath) { try { const out = execSync(`stat -c %Y "${filepath}"`, { encoding: 'utf8' }).trim() const d = new Date(parseInt(out) * 1000) return d.toISOString().split('T')[0] } catch { return null } } function parseFrontmatter(content) { if (!content.startsWith('---\n')) return null const end = content.indexOf('\n---', 4) if (end === -1) return null const fmText = content.slice(4, end) const lines = fmText.split('\n') const fm = {} for (const line of lines) { const m = line.match(/^([a-zA-Z_-]+):\s*(.*)$/) if (m) fm[m[1]] = m[2] } return { fm, fmText, end } } let createdCount = 0 let statusCount = 0 let noFrontmatterCount = 0 const notes = getActiveNotes() console.log(`Scanning ${notes.length} active notes...`) for (const filepath of notes) { const content = readFileSync(filepath, 'utf8') const parsed = parseFrontmatter(content) if (!parsed) { if (!SKIP_CREATED) { const date = getGitCreatedDate(filepath) || getFileMtime(filepath) if (date) { createdCount++ if (DRY_RUN) { console.log(` +frontmatter: ${filepath} (created: ${date})`) } else { const newContent = `---\ncreated: ${date}\n---\n${content}` writeFileSync(filepath, newContent, 'utf8') } } } else { noFrontmatterCount++ } continue } const { fm, fmText, end } = parsed let newFmText = fmText let changed = false if (!SKIP_CREATED && !fm.created) { const date = getGitCreatedDate(filepath) || getFileMtime(filepath) if (date) { newFmText = `created: ${date}\n${newFmText}` createdCount++ changed = true if (DRY_RUN) console.log(` +created: ${filepath} (${date})`) } } if (!SKIP_STATUS && fm.status) { const rawStatus = fm.status.replace(/^["']|["']$/g, '').trim() const normalized = STATUS_MAP[rawStatus] if (normalized && normalized !== rawStatus) { newFmText = newFmText.replace( new RegExp(`^status:\\s*${rawStatus.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm'), `status: ${normalized}` ) statusCount++ changed = true if (DRY_RUN) console.log(` ~status: ${filepath} (${rawStatus} → ${normalized})`) } } if (changed && !DRY_RUN) { const newContent = `---\n${newFmText}${content.slice(end)}` writeFileSync(filepath, newContent, 'utf8') } } console.log(`\n--- ${DRY_RUN ? 'DRY RUN' : 'APPLIED'} ---`) console.log(`Notes without frontmatter (skipped): ${noFrontmatterCount}`) console.log(`created added: ${createdCount}`) console.log(`status normalized: ${statusCount}`)