forked from
standard.site/standard.site
Standard.site landing page built in Next.js
1import { AtpAgent } from "@atproto/api";
2import { readFileSync, readdirSync, statSync } from "fs";
3import { join, dirname } from "path";
4import { fileURLToPath } from "url";
5
6const __dirname = dirname(fileURLToPath(import.meta.url));
7const COLLECTION = "site.standard.document";
8const CONTENT_DIR = join(__dirname, "../content");
9
10function extractKeepRkeys(dir: string): Set<string> {
11 const rkeys = new Set<string>();
12
13 for (const entry of readdirSync(dir)) {
14 const fullPath = join(dir, entry);
15 if (statSync(fullPath).isDirectory()) {
16 for (const rkey of extractKeepRkeys(fullPath)) {
17 rkeys.add(rkey);
18 }
19 } else if (entry.endsWith(".mdx") || entry.endsWith(".md")) {
20 const content = readFileSync(fullPath, "utf-8");
21 const match = content.match(
22 /atUri:\s*["']at:\/\/[^/]+\/site\.standard\.document\/([^"'\s]+)["']/
23 );
24 if (match) rkeys.add(match[1]);
25 }
26 }
27
28 return rkeys;
29}
30
31async function main() {
32 const handle = process.env.ATP_IDENTIFIER;
33 const password = process.env.ATP_APP_PASSWORD;
34
35 if (!handle || !password) {
36 console.error("ATP_IDENTIFIER and ATP_APP_PASSWORD must be set.");
37 process.exit(1);
38 }
39
40 const keepRkeys = extractKeepRkeys(CONTENT_DIR);
41 console.log(`Found ${keepRkeys.size} records to keep from frontmatter`);
42
43 const agent = new AtpAgent({ service: "https://bsky.social" });
44 await agent.login({ identifier: handle, password });
45
46 const did = agent.session!.did;
47 console.log(`Logged in as ${did}`);
48
49 // List all records
50 const duplicates: { rkey: string; title: string }[] = [];
51 let cursor: string | undefined;
52
53 do {
54 const res = await agent.com.atproto.repo.listRecords({
55 repo: did,
56 collection: COLLECTION,
57 limit: 100,
58 cursor,
59 });
60
61 for (const record of res.data.records) {
62 const rkey = record.uri.split("/").pop()!;
63 if (!keepRkeys.has(rkey)) {
64 const value = record.value as Record<string, unknown>;
65 duplicates.push({ rkey, title: (value.title as string) || "(untitled)" });
66 }
67 }
68
69 cursor = res.data.cursor;
70 } while (cursor);
71
72 if (duplicates.length === 0) {
73 console.log("No duplicates found — PDS is clean.");
74 return;
75 }
76
77 console.log(`Removing ${duplicates.length} duplicate(s)...`);
78
79 for (const dup of duplicates) {
80 await agent.com.atproto.repo.deleteRecord({
81 repo: did,
82 collection: COLLECTION,
83 rkey: dup.rkey,
84 });
85 console.log(` ✅ Deleted: ${dup.title} (${dup.rkey})`);
86 }
87
88 console.log(`Removed ${duplicates.length} duplicate(s).`);
89}
90
91main().catch((err) => {
92 console.error("Error:", err);
93 process.exit(1);
94});