// Seed the chalky.town account with a well-rounded batch of demo tasks: // movies, tv shows, standup comedy, sports, chores, and uncolored misc. // Mixes dated + "someday" tasks, descriptions, subtasks, completed tasks, // and recurring tasks across every frequency (daily/weekly/monthly/yearly, // both "day" and "weekday" monthly modes, plus a recUntil example). // // Usage (never hardcode the secret): // export CHALKY_APP_PASSWORD='xxxx-xxxx-xxxx-xxxx' // node scripts/seed-tasks.mjs # write the records // node scripts/seed-tasks.mjs --dry-run # print what would be written, no auth needed // // The app password is created at https://bsky.app/settings/app-passwords (or the // PDS's own settings) for the chalky.town account. It's only used to auth to the // account's own PDS. This only ADDS records — it never deletes existing tasks. import { AtpAgent } from "@atproto/api"; const HANDLE = process.env.CHALKY_HANDLE ?? "chalky.town"; const PASSWORD = process.env.CHALKY_APP_PASSWORD; const SERVICE = process.env.CHALKY_PDS ?? "https://bag.laugh.town"; const DRY_RUN = process.argv.includes("--dry-run"); if (!PASSWORD && !DRY_RUN) { console.error( "Missing CHALKY_APP_PASSWORD. Export an app password for the chalky.town\n" + "account first: export CHALKY_APP_PASSWORD='xxxx-xxxx-xxxx-xxxx'", ); process.exit(1); } // ---- TID generator, mirrors src/lib/store/tid.ts (13-char base32, monotonic) ---- const B32 = "234567abcdefghijklmnopqrstuvwxyz"; let lastTid = 0n; const clockId = BigInt(Math.floor(Math.random() * 1024)); function tid() { let micros = BigInt(Date.now()) * 1000n; const packed = (micros << 10n) | clockId; let v = packed <= lastTid ? lastTid + 1n : packed; lastTid = v; let s = ""; for (let i = 0; i < 13; i++) { s = B32[Number(v & 31n)] + s; v >>= 5n; } return s; } // ---- date helpers, mirror src/lib/date.ts (local calendar, not UTC) ---- function dayKey(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function addDays(d, n) { const r = new Date(d); r.setDate(r.getDate() + n); return r; } /** Next date on/after `from` that falls on weekday `dow` (0=Sun...6=Sat). */ function nextDow(from, dow) { const d = new Date(from); d.setDate(d.getDate() + ((dow - d.getDay() + 7) % 7)); return d; } /** The n-th (1-based) occurrence of weekday `dow` in the given month. */ function nthWeekdayOfMonth(year, month, dow, n) { const first = new Date(year, month, 1); const offset = (dow - first.getDay() + 7) % 7; return new Date(year, month, 1 + offset + (n - 1) * 7); } const today = new Date(); today.setHours(0, 0, 0, 0); const NOW = new Date().toISOString(); // Anchor for the monthly "2nd Tuesday" example: this month's if still ahead, else next month's. let triviaAnchor = nthWeekdayOfMonth(today.getFullYear(), today.getMonth(), 2, 2); if (triviaAnchor < today) { triviaAnchor = nthWeekdayOfMonth(today.getFullYear(), today.getMonth() + 1, 2, 2); } function mkSubtasks(items) { return items.map(([text, done]) => { const s = { id: tid(), text, done }; if (done) s.completedAt = NOW; return s; }); } function completedAtFor(date) { const d = new Date(date); d.setHours(19, 0, 0, 0); return d.toISOString(); } // ---- task definitions ---- // `when`: a Date for a scheduled day, or null for the "Someday" bucket. // `rec`: optional recurrence {freq, interval, byWeekday?, monthly?, byMonthDay?, byMonth?, until?}, anchored at `when`. const tasks = [ // ---------------- Movies (salmon) ---------------- { color: "salmon", title: "Watch Dune: Part Two", description: "Long overdue rewatch before the sequel news drops.", when: addDays(today, 2), subtasks: mkSubtasks([ ["Order popcorn", false], ["Invite Sam", true], ["Pick a showtime", false], ]), }, { color: "salmon", title: "Movie night", description: "Pick something nobody's seen yet.", when: nextDow(today, 5), // Friday rec: { freq: "weekly", interval: 1, byWeekday: [5] }, subtasks: mkSubtasks([ ["Pick a movie", false], ["Make snacks", false], ]), }, { color: "salmon", title: "Finish the Letterboxd 2026 watchlist", description: "Currently at 14/50.", when: null, }, { color: "salmon", title: "Return library DVD before the late fee", when: addDays(today, 1), }, { color: "salmon", title: "Rank the Best Picture nominees", description: "Before the ceremony airs.", when: null, subtasks: mkSubtasks([ ["Oppenheimer", true], ["Poor Things", true], ["The Zone of Interest", false], ["Barbie", true], ["Killers of the Flower Moon", false], ]), }, { color: "salmon", title: "Christopher Nolan marathon", when: addDays(today, 9), subtasks: mkSubtasks([ ["Memento", false], ["Inception", false], ["Interstellar", false], ["Oppenheimer", false], ]), }, { color: "salmon", title: "Watch the new Bond trailer", when: addDays(today, -1), completed: true, }, // ---------------- TV Shows (chardonnay) ---------------- { color: "chardonnay", title: "Watch the new episode", description: "Whatever the group chat is currently obsessed with.", when: nextDow(today, 4), // Thursday rec: { freq: "weekly", interval: 1, byWeekday: [4] }, }, { color: "chardonnay", title: "Finish Season 3 of Severance", when: addDays(today, 4), subtasks: mkSubtasks([ ["Ep 7", true], ["Ep 8", true], ["Ep 9", false], ["Ep 10 (finale)", false], ]), }, { color: "chardonnay", title: "Start that new sci-fi series everyone's talking about", when: null, }, { color: "chardonnay", title: "Prune the watchlist", description: "Cut anything abandoned for 6+ months.", when: null, }, { color: "chardonnay", title: "Watch the series finale", when: addDays(today, -2), completed: true, }, { color: "chardonnay", title: "TV trivia night at the bar", description: "Team name: still undecided.", when: triviaAnchor, rec: { freq: "monthly", interval: 1, monthly: "weekday" }, }, // ---------------- Standup Comedy (witch-haze) ---------------- { color: "witch-haze", title: "Get tickets for the comedy show", description: "Doors at 7, show at 8.", when: addDays(today, 6), subtasks: mkSubtasks([ ["Compare ticket prices", false], ["Pick seats", false], ["Buy tickets", false], ]), }, { color: "witch-haze", title: "Watch a new stand-up special", when: nextDow(today, 0), // Sunday rec: { freq: "weekly", interval: 1, byWeekday: [0], until: "2026-12-31" }, }, { color: "witch-haze", title: "Write down that bit idea from last night", when: null, }, { color: "witch-haze", title: "Listen to a comedy podcast episode", when: nextDow(today, 3), // Wednesday rec: { freq: "weekly", interval: 1, byWeekday: [3] }, }, { color: "witch-haze", title: "Watch the Nate Bargatze special", when: addDays(today, -4), completed: true, }, { color: "witch-haze", title: "Work on 5 minutes of new material", description: "Just get it out of your head and onto paper.", when: null, subtasks: mkSubtasks([ ["Draft the bit", false], ["Test it at an open mic", false], ["Refine based on what landed", false], ]), }, // ---------------- Sports (mint-green) ---------------- { color: "mint-green", title: "Gym session", when: nextDow(today, 1), // Monday rec: { freq: "weekly", interval: 1, byWeekday: [1, 3, 5] }, // Mon/Wed/Fri }, { color: "mint-green", title: "Watch the game", when: addDays(today, 0), subtasks: mkSubtasks([ ["Order pizza", false], ["Text the group chat", false], ]), }, { color: "mint-green", title: "Check gym membership renewal", when: today, rec: { freq: "monthly", interval: 1, monthly: "day", byMonthDay: today.getDate() }, }, { color: "mint-green", title: "Sign up for the fall league", description: "Deadline's usually mid-August.", when: null, }, { color: "mint-green", title: "Run 5k", when: addDays(today, -1), completed: true, subtasks: mkSubtasks([ ["Warm up", true], ["Run", true], ["Stretch", true], ]), }, { color: "mint-green", title: "Watch the Super Bowl", description: "Halftime show > the game, don't @ me.", when: today, rec: { freq: "yearly", interval: 1, monthly: "day", byMonth: 2, byMonthDay: 8 }, }, // ---------------- Chores (melrose) ---------------- { color: "melrose", title: "Make the bed", when: today, rec: { freq: "daily", interval: 1 }, }, { color: "melrose", title: "Take out the trash", when: nextDow(today, 2), // Tuesday rec: { freq: "weekly", interval: 1, byWeekday: [2, 5] }, // Tue/Fri }, { color: "melrose", title: "Deep clean the bathroom", when: today, rec: { freq: "monthly", interval: 1, monthly: "day", byMonthDay: today.getDate() }, }, { color: "melrose", title: "Fix the leaky faucet", description: "Kitchen sink, driving me insane.", when: addDays(today, 3), subtasks: mkSubtasks([ ["Buy replacement parts", false], ["Shut off the water", false], ["Replace the washer", false], ]), }, { color: "melrose", title: "Do the laundry", when: today, completed: true, }, { color: "melrose", title: "Organize the garage", description: "One shelf at a time.", when: null, subtasks: mkSubtasks([ ["Tools", false], ["Sports gear", false], ["Holiday decorations", false], ]), }, { color: "melrose", title: "Change the smoke detector batteries", description: "Do it when the clocks change.", when: today, rec: { freq: "yearly", interval: 1, monthly: "day", byMonth: 11, byMonthDay: 1 }, }, // ---------------- Misc (uncolored) ---------------- { color: null, title: "Renew passport", description: "Expires in March — don't wait until the last minute.", when: null, }, { color: null, title: "Call mom", when: nextDow(today, 0), // Sunday rec: { freq: "weekly", interval: 1, byWeekday: [0] }, }, { color: null, title: "Buy a birthday gift", when: addDays(today, 5), subtasks: mkSubtasks([ ["Figure out what they actually want", false], ["Order it", false], ["Wrap it", false], ]), }, { color: null, title: "Read that book everyone recommended", when: null, }, { color: null, title: "Back up the laptop", when: today, rec: { freq: "monthly", interval: 1, monthly: "day", byMonthDay: today.getDate() }, }, { color: null, title: "Submit tax documents", when: addDays(today, -10), completed: true, }, { color: null, title: "Build a birdhouse", when: null, }, ]; // ---- serialize to town.chalky.task records, assigning per-bucket `order` ---- const ORDER_STEP = 1024; const orderByBucket = new Map(); function nextOrder(bucket) { const cur = orderByBucket.get(bucket) ?? 0; const next = cur + ORDER_STEP; orderByBucket.set(bucket, next); return next; } const records = tasks.map((t) => { const bucket = t.when ? dayKey(t.when) : "someday"; const rkey = tid(); /** @type {any} */ const rec = { $type: "town.chalky.task", title: t.title, completed: !!t.completed, order: nextOrder(bucket), createdAt: NOW, updatedAt: NOW, }; if (t.description) rec.description = t.description; if (t.when) rec.day = dayKey(t.when); if (t.color) rec.color = t.color; if (t.subtasks?.length) rec.subtasks = t.subtasks; if (t.rec) { rec.isRecurring = true; rec.recFrequency = t.rec.freq; rec.recInterval = t.rec.interval; if (t.rec.byWeekday) rec.recByWeekday = t.rec.byWeekday; if (t.rec.monthly) rec.recMonthly = t.rec.monthly; if (t.rec.byMonthDay) rec.recByMonthDay = t.rec.byMonthDay; if (t.rec.byMonth) rec.recByMonth = t.rec.byMonth; if (t.rec.until) rec.recUntil = t.rec.until; } else if (t.completed) { rec.completedAt = completedAtFor(t.when ?? today); } return { rkey, record: rec }; }); console.log(`Built ${records.length} task records.`); const byColor = records.reduce((m, r) => { const k = r.record.color ?? "uncolored"; m[k] = (m[k] ?? 0) + 1; return m; }, {}); console.log("By color:", byColor); console.log(`Recurring: ${records.filter((r) => r.record.isRecurring).length}`); console.log(`Completed: ${records.filter((r) => r.record.completed).length}`); console.log(`With subtasks: ${records.filter((r) => r.record.subtasks).length}`); console.log(`Someday: ${records.filter((r) => !r.record.day).length}`); if (DRY_RUN) { console.log("\n--- sample record ---"); console.log(JSON.stringify(records[0], null, 2)); console.log("\nDry run only. Set CHALKY_APP_PASSWORD and re-run without --dry-run."); process.exit(0); } const agent = new AtpAgent({ service: SERVICE }); await agent.login({ identifier: HANDLE, password: PASSWORD }); const did = agent.session?.did; console.log(`Signed in as ${HANDLE} (${did}) on ${SERVICE}`); for (const { rkey, record } of records) { const res = await agent.com.atproto.repo.putRecord({ repo: did, collection: "town.chalky.task", rkey, record, validate: false, }); console.log(` ${record.title} -> ${res.data.uri}`); } console.log(`\nDone. Wrote ${records.length} tasks to ${HANDLE}'s repo.`);