170 lines
4.9 KiB
JavaScript
170 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require("fs/promises");
|
|
const path = require("path");
|
|
const { extractRawDate, readFrontmatter, writeFrontmatter } = require("./lib/weather/frontmatter");
|
|
const { resolveArticleDate } = require("./lib/weather/time");
|
|
const { fetchWeather, hasConfiguredProvider, mergeWeather } = require("./lib/weather/providers");
|
|
const { loadWeatherConfig } = require("./lib/weather/config");
|
|
|
|
const CONTENT_ROOT = path.resolve("content");
|
|
|
|
async function collectMarkdownFiles(rootDir) {
|
|
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
const files = [];
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(rootDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
const nested = await collectMarkdownFiles(fullPath);
|
|
files.push(...nested);
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile()) continue;
|
|
|
|
if (!entry.name.endsWith(".md")) continue;
|
|
if (entry.name === "_index.md") continue;
|
|
|
|
files.push(fullPath);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
async function resolveTargets(args) {
|
|
if (args.length === 0) {
|
|
return collectMarkdownFiles(CONTENT_ROOT);
|
|
}
|
|
|
|
const targets = new Set();
|
|
|
|
for (const input of args) {
|
|
const resolved = path.resolve(input);
|
|
|
|
try {
|
|
const stat = await fs.stat(resolved);
|
|
|
|
if (stat.isDirectory()) {
|
|
const nested = await collectMarkdownFiles(resolved);
|
|
nested.forEach((file) => targets.add(file));
|
|
continue;
|
|
}
|
|
|
|
if (stat.isFile()) {
|
|
if (!resolved.endsWith(".md")) continue;
|
|
if (path.basename(resolved) === "_index.md") continue;
|
|
targets.add(resolved);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Skipping ${input}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
return Array.from(targets);
|
|
}
|
|
|
|
async function processFile(filePath, config, { force = false } = {}) {
|
|
const frontmatter = await readFrontmatter(filePath);
|
|
|
|
if (!frontmatter) {
|
|
return { status: "no-frontmatter" };
|
|
}
|
|
|
|
const existingWeather = frontmatter.doc.has("weather") ? frontmatter.doc.get("weather") : null;
|
|
|
|
if (existingWeather && !force) {
|
|
return { status: "already-set" };
|
|
}
|
|
|
|
const dateValue = frontmatter.doc.get("date");
|
|
if (!dateValue) {
|
|
return { status: "no-date" };
|
|
}
|
|
|
|
const rawDate = extractRawDate(frontmatter.frontmatterText);
|
|
const targetDate = resolveArticleDate(dateValue, rawDate, config);
|
|
|
|
if (!targetDate) {
|
|
return { status: "invalid-date" };
|
|
}
|
|
|
|
const weather = await fetchWeather(targetDate, config);
|
|
|
|
let finalWeather = {};
|
|
if (existingWeather && typeof existingWeather === "object") {
|
|
finalWeather = JSON.parse(JSON.stringify(existingWeather));
|
|
}
|
|
|
|
if (weather) {
|
|
const added = mergeWeather(finalWeather, weather, weather.source?.[0]);
|
|
|
|
if (!added && Object.keys(finalWeather).length === 0) {
|
|
finalWeather = weather;
|
|
}
|
|
}
|
|
|
|
frontmatter.doc.set("weather", finalWeather);
|
|
|
|
await writeFrontmatter(filePath, frontmatter.doc, frontmatter.body);
|
|
|
|
return {
|
|
status: weather ? "updated" : "empty",
|
|
sources: weather?.source || [],
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const cliArgs = process.argv.slice(2);
|
|
const force = cliArgs.includes("--force") || cliArgs.includes("-f");
|
|
const pathArgs = cliArgs.filter((arg) => arg !== "--force" && arg !== "-f");
|
|
|
|
const config = loadWeatherConfig();
|
|
if (!hasConfiguredProvider(config)) {
|
|
console.error("No weather provider configured. Update tools/config.json (weather.providers) before running this script.");
|
|
process.exit(1);
|
|
}
|
|
const files = await resolveTargets(pathArgs);
|
|
|
|
if (files.length === 0) {
|
|
console.log("No matching markdown files found.");
|
|
return;
|
|
}
|
|
|
|
let updated = 0;
|
|
let skipped = 0;
|
|
|
|
for (const file of files) {
|
|
const relativePath = path.relative(process.cwd(), file);
|
|
|
|
try {
|
|
const result = await processFile(file, config, { force });
|
|
|
|
switch (result.status) {
|
|
case "updated":
|
|
updated += 1;
|
|
console.log(`✔ Added weather to ${relativePath} (${result.sources.join(", ") || "unknown source"})`);
|
|
break;
|
|
case "empty":
|
|
updated += 1;
|
|
console.log(`• Added empty weather to ${relativePath}`);
|
|
break;
|
|
default:
|
|
skipped += 1;
|
|
console.log(`↷ Skipped ${relativePath} (${result.status})`);
|
|
}
|
|
} catch (error) {
|
|
skipped += 1;
|
|
console.error(`✖ Failed ${relativePath}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nSummary: ${updated} updated, ${skipped} skipped.`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|