Files
my-vault/.scripts/metadata-converge.mjs
T

137 lines
3.8 KiB
JavaScript
Raw Normal View History

#!/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}`)