| 1 | #!/usr/bin/env node |
| 2 | // π€ tools/marketing-shots/seed-team.mjs β seed the "The Commons" civic-tech |
| 3 | // demo space. |
| 4 | // |
| 5 | // Seeds a running local lchat instance (started by serve.sh / `./tools/dev |
| 6 | // shots up`) with a multi-thread team space that matches the marketing web |
| 7 | // frame (website .../hero/team-web.png): the space is "The Commons", its |
| 8 | // flagship all-members thread "The commons π±" carries the exact web-frame |
| 9 | // conversation (Aoife / Marcus / Devi + a reply + reactions), and five more |
| 10 | // team threads give the Mac/iPhone thread list a realistic spread. |
| 11 | // |
| 12 | // Everything is FICTIONAL. Seeding goes through the real E2EE protocol (see |
| 13 | // demo-lib.mjs) β the server only ever sees ciphertext. |
| 14 | // |
| 15 | // Usage: node tools/marketing-shots/seed-team.mjs --base http://127.0.0.1:8848 --admin <token> |
| 16 | // --base the running instance (default http://127.0.0.1:8848, or $LCHAT_BASE) |
| 17 | // --admin its admin token (default $LCHAT_ADMIN_TOKEN) |
| 18 | // --link-origin origin to print the device link on (default = --base) |
| 19 | // |
| 20 | // Idempotency: this always seeds a FRESH set of threads/messages. serve.sh |
| 21 | // starts from a clean DB each run, so re-seeding a clean DB is the norm; running |
| 22 | // it twice against the SAME DB would duplicate the space (harmless for captures, |
| 23 | // but not intended). It rewrites .demo-state.json and mints a fresh device link. |
| 24 | |
| 25 | import { |
| 26 | provisionMember, |
| 27 | createThread, |
| 28 | postMessage, |
| 29 | postReaction, |
| 30 | mintDeviceLink, |
| 31 | writeState, |
| 32 | today, |
| 33 | } from "./demo-lib.mjs"; |
| 34 | |
| 35 | function arg(name, def) { |
| 36 | const i = process.argv.indexOf(`--${name}`); |
| 37 | return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : def; |
| 38 | } |
| 39 | |
| 40 | const base = arg("base", process.env.LCHAT_BASE || "http://127.0.0.1:8848"); |
| 41 | const adminToken = arg("admin", process.env.LCHAT_ADMIN_TOKEN || ""); |
| 42 | const linkOrigin = arg("link-origin", base); |
| 43 | if (!adminToken) { |
| 44 | console.error("seed-team: no admin token (pass --admin <token> or set LCHAT_ADMIN_TOKEN)"); |
| 45 | process.exit(1); |
| 46 | } |
| 47 | |
| 48 | async function main() { |
| 49 | // Members (6). Cass is the operator's signed-in identity (admin); the other |
| 50 | // five are the people whose messages fill the space. |
| 51 | console.error("seed-team: provisioning membersβ¦"); |
| 52 | const cass = await provisionMember(base, adminToken, "Cass", { grantsAdmin: true }); |
| 53 | const aoife = await provisionMember(base, adminToken, "Aoife"); |
| 54 | const marcus = await provisionMember(base, adminToken, "Marcus"); |
| 55 | const devi = await provisionMember(base, adminToken, "Devi"); |
| 56 | const niamh = await provisionMember(base, adminToken, "Niamh"); |
| 57 | const tomas = await provisionMember(base, adminToken, "TomΓ‘s"); |
| 58 | const everyone = [cass, aoife, marcus, devi, niamh, tomas]; |
| 59 | |
| 60 | // Threads are created + populated in ascending recency so "The commons π±" |
| 61 | // ends up the most-recently-active β top of the list (like the family shot's |
| 62 | // "Everyone"). Cass never posts LAST in a thread, so no preview reads "You:". |
| 63 | console.error("seed-team: creating threads + messagesβ¦"); |
| 64 | |
| 65 | // ββ Watercooler β ββββββββββββββββββββββββββββββββββββββββββββββ |
| 66 | { |
| 67 | const t = await createThread(base, "Watercooler β", cass, everyone); |
| 68 | await postMessage(base, t, aoife, "anyone tried the new place on Dame St?", today(8, 2)); |
| 69 | await postMessage(base, t, tomas, "coffee run in 10, who's in β", today(8, 6)); |
| 70 | } |
| 71 | |
| 72 | // ββ Roadmap ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| 73 | { |
| 74 | const t = await createThread(base, "Roadmap", cass, everyone); |
| 75 | await postMessage(base, t, marcus, "what's the one thing that unblocks the most people?", today(8, 15)); |
| 76 | await postMessage(base, t, niamh, "moved the transparency portal to next sprint", today(8, 20)); |
| 77 | } |
| 78 | |
| 79 | // ββ Field notes π ββββββββββββββββββββββββββββββββββββββββββββββ |
| 80 | { |
| 81 | const t = await createThread(base, "Field notes π", cass, everyone); |
| 82 | await postMessage(base, t, niamh, "librarian in Phibsboro wants the bin map printed as a poster", today(8, 25)); |
| 83 | await postMessage(base, t, devi, "three councillors asked where the data came from today π", today(8, 30)); |
| 84 | } |
| 85 | |
| 86 | // ββ Shipping π ββββββββββββββββββββββββββββββββββββββββββββββββ |
| 87 | { |
| 88 | const t = await createThread(base, "Shipping π", cass, everyone); |
| 89 | await postMessage(base, t, marcus, "bin map's passing lighthouse, merging now", today(8, 34)); |
| 90 | await postMessage(base, t, aoife, "bin map v2 is live β dark mode + cycle lanes π", today(8, 38)); |
| 91 | } |
| 92 | |
| 93 | // ββ Standup ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| 94 | // A richer thread with a Cass (own) bubble + a reaction, so it's a good |
| 95 | // ALTERNATIVE for the Mac open-thread (variety from the web frame). |
| 96 | { |
| 97 | const t = await createThread(base, "Standup", cass, everyone); |
| 98 | await postMessage(base, t, marcus, "y'day: shipped the bin map. today: vote-day banner. blockers: none", today(8, 40)); |
| 99 | const mine = await postMessage(base, t, cass, "nice β I'll take the councillor data requests", today(8, 42)); |
| 100 | await postReaction(base, t, marcus, mine, cass, "π", today(8, 43)); |
| 101 | await postMessage(base, t, devi, "scraper's green again after the site redesign", today(8, 45)); |
| 102 | await postMessage(base, t, marcus, "pushed the vote-day banner. live now", today(8, 47)); |
| 103 | } |
| 104 | |
| 105 | // ββ The commons π± (flagship β matches team-web.png exactly) ββββ |
| 106 | const commons = await createThread(base, "The commons π±", cass, everyone); |
| 107 | await postMessage(base, commons, aoife, "housing numbers came out as another 94-page pdf π", today(9, 12)); |
| 108 | const marcusMsg = await postMessage(base, commons, marcus, "the bin map shipped in four days. 3,000 users.", today(9, 13)); |
| 109 | // Reactions on Marcus's message β β€οΈ Γ1, π₯ Γ2 (as in the web frame). Posted |
| 110 | // BEFORE the final message so the thread's last row is a normal message |
| 111 | // (a trailing reaction would blank the list preview). |
| 112 | await postReaction(base, commons, aoife, marcusMsg, marcus, "β€οΈ", today(9, 13)); |
| 113 | await postReaction(base, commons, devi, marcusMsg, marcus, "π₯", today(9, 13)); |
| 114 | await postReaction(base, commons, niamh, marcusMsg, marcus, "π₯", today(9, 14)); |
| 115 | // Cass's reply to Marcus (the orange/own bubble with the quoted context). |
| 116 | await postMessage(base, commons, cass, "ugly and useful beats perfect and pending", today(9, 15), { |
| 117 | replyTo: { id: marcusMsg.id, name: "Marcus", snippet: "the bin map shipped in four days. 3,000 users." }, |
| 118 | }); |
| 119 | // Devi's message is LAST β the list preview reads "Devi: scraper tonight thenβ¦" |
| 120 | await postMessage(base, commons, devi, "scraper tonight then. map live before the vote π±", today(9, 16)); |
| 121 | |
| 122 | // Persist what device-link.mjs needs to re-mint links for Cass. |
| 123 | writeState({ |
| 124 | base, |
| 125 | adminToken, |
| 126 | seededAt: new Date().toISOString(), |
| 127 | spaceName: "The Commons", |
| 128 | cass: { userId: cass.userId, publicKey: cass.publicKey, privateKey: cass.privateKey }, |
| 129 | members: everyone.map((m) => ({ name: m.name, userId: m.userId })), |
| 130 | }); |
| 131 | |
| 132 | const link = await mintDeviceLink(base, adminToken, cass, linkOrigin, "You"); |
| 133 | console.error("seed-team: done."); |
| 134 | console.log(link); // stdout = just the link, so callers can capture it |
| 135 | } |
| 136 | |
| 137 | main().catch((e) => { |
| 138 | console.error("seed-team:", e.message); |
| 139 | process.exit(1); |
| 140 | }); |
| 141 | |