#!/usr/bin/env node // πŸ€– tools/marketing-shots/seed-team.mjs β€” seed the "The Commons" civic-tech // demo space. // // Seeds a running local lchat instance (started by serve.sh / `./tools/dev // shots up`) with a multi-thread team space that matches the marketing web // frame (website .../hero/team-web.png): the space is "The Commons", its // flagship all-members thread "The commons 🌱" carries the exact web-frame // conversation (Aoife / Marcus / Devi + a reply + reactions), and five more // team threads give the Mac/iPhone thread list a realistic spread. // // Everything is FICTIONAL. Seeding goes through the real E2EE protocol (see // demo-lib.mjs) β€” the server only ever sees ciphertext. // // Usage: node tools/marketing-shots/seed-team.mjs --base http://127.0.0.1:8848 --admin // --base the running instance (default http://127.0.0.1:8848, or $LCHAT_BASE) // --admin its admin token (default $LCHAT_ADMIN_TOKEN) // --link-origin origin to print the device link on (default = --base) // // Idempotency: this always seeds a FRESH set of threads/messages. serve.sh // starts from a clean DB each run, so re-seeding a clean DB is the norm; running // it twice against the SAME DB would duplicate the space (harmless for captures, // but not intended). It rewrites .demo-state.json and mints a fresh device link. import { provisionMember, createThread, postMessage, postReaction, mintDeviceLink, writeState, today, } from "./demo-lib.mjs"; function arg(name, def) { const i = process.argv.indexOf(`--${name}`); return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : def; } const base = arg("base", process.env.LCHAT_BASE || "http://127.0.0.1:8848"); const adminToken = arg("admin", process.env.LCHAT_ADMIN_TOKEN || ""); const linkOrigin = arg("link-origin", base); if (!adminToken) { console.error("seed-team: no admin token (pass --admin or set LCHAT_ADMIN_TOKEN)"); process.exit(1); } async function main() { // Members (6). Cass is the operator's signed-in identity (admin); the other // five are the people whose messages fill the space. console.error("seed-team: provisioning members…"); const cass = await provisionMember(base, adminToken, "Cass", { grantsAdmin: true }); const aoife = await provisionMember(base, adminToken, "Aoife"); const marcus = await provisionMember(base, adminToken, "Marcus"); const devi = await provisionMember(base, adminToken, "Devi"); const niamh = await provisionMember(base, adminToken, "Niamh"); const tomas = await provisionMember(base, adminToken, "TomΓ‘s"); const everyone = [cass, aoife, marcus, devi, niamh, tomas]; // Threads are created + populated in ascending recency so "The commons 🌱" // ends up the most-recently-active β†’ top of the list (like the family shot's // "Everyone"). Cass never posts LAST in a thread, so no preview reads "You:". console.error("seed-team: creating threads + messages…"); // ── Watercooler β˜• ────────────────────────────────────────────── { const t = await createThread(base, "Watercooler β˜•", cass, everyone); await postMessage(base, t, aoife, "anyone tried the new place on Dame St?", today(8, 2)); await postMessage(base, t, tomas, "coffee run in 10, who's in β˜•", today(8, 6)); } // ── Roadmap ──────────────────────────────────────────────────── { const t = await createThread(base, "Roadmap", cass, everyone); await postMessage(base, t, marcus, "what's the one thing that unblocks the most people?", today(8, 15)); await postMessage(base, t, niamh, "moved the transparency portal to next sprint", today(8, 20)); } // ── Field notes πŸ““ ────────────────────────────────────────────── { const t = await createThread(base, "Field notes πŸ““", cass, everyone); await postMessage(base, t, niamh, "librarian in Phibsboro wants the bin map printed as a poster", today(8, 25)); await postMessage(base, t, devi, "three councillors asked where the data came from today πŸ““", today(8, 30)); } // ── Shipping πŸš€ ──────────────────────────────────────────────── { const t = await createThread(base, "Shipping πŸš€", cass, everyone); await postMessage(base, t, marcus, "bin map's passing lighthouse, merging now", today(8, 34)); await postMessage(base, t, aoife, "bin map v2 is live β€” dark mode + cycle lanes πŸš€", today(8, 38)); } // ── Standup ──────────────────────────────────────────────────── // A richer thread with a Cass (own) bubble + a reaction, so it's a good // ALTERNATIVE for the Mac open-thread (variety from the web frame). { const t = await createThread(base, "Standup", cass, everyone); await postMessage(base, t, marcus, "y'day: shipped the bin map. today: vote-day banner. blockers: none", today(8, 40)); const mine = await postMessage(base, t, cass, "nice β€” I'll take the councillor data requests", today(8, 42)); await postReaction(base, t, marcus, mine, cass, "πŸ‘", today(8, 43)); await postMessage(base, t, devi, "scraper's green again after the site redesign", today(8, 45)); await postMessage(base, t, marcus, "pushed the vote-day banner. live now", today(8, 47)); } // ── The commons 🌱 (flagship β€” matches team-web.png exactly) ──── const commons = await createThread(base, "The commons 🌱", cass, everyone); await postMessage(base, commons, aoife, "housing numbers came out as another 94-page pdf πŸ™ƒ", today(9, 12)); const marcusMsg = await postMessage(base, commons, marcus, "the bin map shipped in four days. 3,000 users.", today(9, 13)); // Reactions on Marcus's message β†’ ❀️ Γ—1, πŸ”₯ Γ—2 (as in the web frame). Posted // BEFORE the final message so the thread's last row is a normal message // (a trailing reaction would blank the list preview). await postReaction(base, commons, aoife, marcusMsg, marcus, "❀️", today(9, 13)); await postReaction(base, commons, devi, marcusMsg, marcus, "πŸ”₯", today(9, 13)); await postReaction(base, commons, niamh, marcusMsg, marcus, "πŸ”₯", today(9, 14)); // Cass's reply to Marcus (the orange/own bubble with the quoted context). await postMessage(base, commons, cass, "ugly and useful beats perfect and pending", today(9, 15), { replyTo: { id: marcusMsg.id, name: "Marcus", snippet: "the bin map shipped in four days. 3,000 users." }, }); // Devi's message is LAST β†’ the list preview reads "Devi: scraper tonight then…" await postMessage(base, commons, devi, "scraper tonight then. map live before the vote 🌱", today(9, 16)); // Persist what device-link.mjs needs to re-mint links for Cass. writeState({ base, adminToken, seededAt: new Date().toISOString(), spaceName: "The Commons", cass: { userId: cass.userId, publicKey: cass.publicKey, privateKey: cass.privateKey }, members: everyone.map((m) => ({ name: m.name, userId: m.userId })), }); const link = await mintDeviceLink(base, adminToken, cass, linkOrigin, "You"); console.error("seed-team: done."); console.log(link); // stdout = just the link, so callers can capture it } main().catch((e) => { console.error("seed-team:", e.message); process.exit(1); });