97 lines
4.5 KiB
JavaScript
97 lines
4.5 KiB
JavaScript
// Rename leftover v1-named .tasks files → yyyy-mm-dd-<slug>.md (date from git history).
|
||
// Scope: files with NO block in STATUS.md (archived-block companions, graves, spec files).
|
||
// Special: .admin/kreknin-repair-md3-rebuild → LIVE task → restore block + number 830.
|
||
// Companion files (foo-plan.md where foo has a block) are skipped.
|
||
// Skipped repos: meeting-room (merge conflict), projects-meta-mcp (archived),
|
||
// yt-tools (untracked+github), projects-wiki (dup clone), alpha (no git).
|
||
import { execSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
|
||
const ROOT = "C:/Users/vitya/projects";
|
||
const APPLY = process.argv.includes("--apply");
|
||
const HEADER = /^##\s+\S+\s+\[(?:#\d+\s+)?([^\]]+)\]/gm;
|
||
const SKIP = new Set(["meeting-room", "projects-meta-mcp", "yt-tools", "projects-wiki", "alpha"]);
|
||
const SPECIAL = new Set(["kreknin-repair-md3-rebuild"]); // live → restore block + number 830
|
||
|
||
function git(repoDir, args) {
|
||
try {
|
||
return execSync(`git -C "${repoDir}" ${args}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
||
} catch { return ""; }
|
||
}
|
||
|
||
function firstAddDate(repoDir, rel) {
|
||
const out = git(repoDir, `log --follow --diff-filter=A --format=%aI -- "${rel}"`);
|
||
const lines = out.split("\n").filter(Boolean);
|
||
if (!lines.length) return null;
|
||
return lines[lines.length - 1].slice(0, 10);
|
||
}
|
||
|
||
function collect() {
|
||
const dirs = new Set(fs.readdirSync(ROOT).filter(d => { try { return fs.statSync(path.join(ROOT, d)).isDirectory(); } catch { return false; } }));
|
||
[".common", ".wiki", ".workshop", ".factory", ".admin"].forEach(d => dirs.add(d));
|
||
const plan = [];
|
||
for (const repo of dirs) {
|
||
if (SKIP.has(repo)) continue;
|
||
const td = path.join(ROOT, repo, ".tasks");
|
||
if (!fs.existsSync(path.join(td, "STATUS.md"))) continue;
|
||
const status = fs.readFileSync(path.join(td, "STATUS.md"), "utf8");
|
||
const blocks = new Set([...status.matchAll(HEADER)].map(m => m[1]));
|
||
for (const sub of ["", "done"]) {
|
||
const dir = sub ? path.join(td, "done") : td;
|
||
if (!fs.existsSync(dir)) continue;
|
||
for (const f of fs.readdirSync(dir)) {
|
||
if (!f.endsWith(".md") || /^\d{4}/.test(f)) continue;
|
||
if (["STATUS.md", "ARCHIVE.md", "NEXT_SESSION.md"].includes(f)) continue;
|
||
const slug = f.slice(0, -3);
|
||
if (blocks.has(slug)) continue;
|
||
if (SPECIAL.has(slug)) continue;
|
||
const base = slug.replace(/-(plan|spec|design|docs|notes|research|draft|review)$/, "");
|
||
if (base !== slug && blocks.has(base)) continue;
|
||
const rel = path.relative(ROOT, path.join(dir, f)).replace(/\\/g, "/");
|
||
const date = firstAddDate(path.join(ROOT, repo), rel) || fs.statSync(path.join(dir, f)).mtime.toISOString().slice(0, 10);
|
||
plan.push({ repo, dir: sub, slug, old: f, date });
|
||
}
|
||
}
|
||
}
|
||
return plan;
|
||
}
|
||
|
||
function main() {
|
||
const plan = collect();
|
||
const byRepo = {};
|
||
for (const p of plan) byRepo[p.repo] = (byRepo[p.repo] || 0) + 1;
|
||
console.log(`файлов к переименованию: ${plan.length}`);
|
||
for (const [r, n] of Object.entries(byRepo).sort((a, b) => b[1] - a[1])) console.log(` ${r.padEnd(20)} ${n}`);
|
||
if (!APPLY) {
|
||
console.log("\ndry-run (--apply для выполнения). Примеры:");
|
||
for (const p of plan.slice(0, 8)) console.log(` ${p.repo}/${p.dir ? "done/" : ""}${p.old} → ${p.date}-${p.slug}.md`);
|
||
return;
|
||
}
|
||
const byRepoFiles = {};
|
||
for (const p of plan) (byRepoFiles[p.repo] = byRepoFiles[p.repo] || []).push(p);
|
||
for (const [repo, files] of Object.entries(byRepoFiles)) {
|
||
const repoDir = path.join(ROOT, repo);
|
||
if (!fs.existsSync(path.join(repoDir, ".git"))) { console.log(`skip ${repo}: no git`); continue; }
|
||
const conflicted = git(repoDir, "status --porcelain").split("\n").some(l => /^(UU|AA|DD|AU|UA|DU|UD)/.test(l));
|
||
if (conflicted) { console.log(`skip ${repo}: conflicted`); continue; }
|
||
let n = 0;
|
||
for (const p of files) {
|
||
const dir = p.dir ? path.join(repoDir, ".tasks", "done") : path.join(repoDir, ".tasks");
|
||
const src = path.join(dir, p.old);
|
||
const dst = path.join(dir, `${p.date}-${p.slug}.md`);
|
||
if (!fs.existsSync(src)) continue;
|
||
fs.renameSync(src, dst);
|
||
n++;
|
||
}
|
||
if (n) {
|
||
git(repoDir, "add -- .tasks/");
|
||
git(repoDir, `commit -m "meta(tasks): rename ${n} legacy .tasks files to v2 date-prefix names" -q`);
|
||
console.log(`renamed+committed: ${repo} (${n})`);
|
||
}
|
||
}
|
||
console.log("\nСПЕЦИАЛЬНО (вне батча): kreknin-repair-md3-rebuild → блок + #830.");
|
||
}
|
||
|
||
main();
|