137 lines
3.8 KiB
JavaScript
137 lines
3.8 KiB
JavaScript
#!/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}`)
|