Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 1 | /** |
| Anyone on g1t can invite a friend again: Invite a friend, in the account menu with how many are left beside it, opens Settings → Invites to g1t with a dialog for their email address and a note. The friend gets an email from them whose link opens sign-up with the address filled in and, because it came from that email, confirmed; their account starts with a workspace of its own, and the page lists every invite sent with where it stands (Pending, confirming their email, Joined as @them), with Copy link, Send again and Revoke on each. It is word of mouth, never a gate: sign-ups stay open to everyone, nothing asks for an invite, and running out says friends can still sign up without one. | 2 | * The Invites page: every invite, to find and revoke one, with invites to |
| 3 | * g1t from Invite a friend on their own tab, and more of those for a | |
| 4 | * person. Anyone may register, so there is no waitlist and no shared link | |
| 5 | * to manage; an invite is never needed to sign up. No Workers imports, so | |
| 6 | * it can be tested under Node. Identity checks everything again. | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 7 | */ |
| 8 | ||
| Anyone on g1t can invite a friend again: Invite a friend, in the account menu with how many are left beside it, opens Settings → Invites to g1t with a dialog for their email address and a note. The friend gets an email from them whose link opens sign-up with the address filled in and, because it came from that email, confirmed; their account starts with a workspace of its own, and the page lists every invite sent with where it stands (Pending, confirming their email, Joined as @them), with Copy link, Send again and Revoke on each. It is word of mouth, never a gate: sign-ups stay open to everyone, nothing asks for an invite, and running out says friends can still sign up without one. | 9 | /** The page's tabs: every invite, or only invites to g1t (Invite a friend). */ |
| 10 | export type InvitesTab = "all" | "g1t"; | |
| 11 | ||
| 12 | export function invitesTab(raw: string | null | undefined): InvitesTab { | |
| 13 | return raw === "g1t" ? "g1t" : "all"; | |
| 14 | } | |
| 15 | ||
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 16 | /** The page's address, with its search. */ |
| 17 | export function invitesHref(params: Record<string, string | null | undefined> = {}): string { | |
| 18 | const search = new URLSearchParams(); | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 19 | for (const [key, value] of Object.entries(params)) if (value) search.set(key, value); |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 20 | const query = search.toString(); |
| 21 | return query ? `/invites?${query}` : "/invites"; | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 22 | } |
| 23 | ||
| 24 | /** The flash messages the page redirects back with (`?done=`). */ | |
| 25 | export const INVITES_DONE: Record<string, string> = { | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 26 | revoked: "Invite revoked. It stops working at once.", |
| Anyone on g1t can invite a friend again: Invite a friend, in the account menu with how many are left beside it, opens Settings → Invites to g1t with a dialog for their email address and a note. The friend gets an email from them whose link opens sign-up with the address filled in and, because it came from that email, confirmed; their account starts with a workspace of its own, and the page lists every invite sent with where it stands (Pending, confirming their email, Joined as @them), with Copy link, Send again and Revoke on each. It is word of mouth, never a gate: sign-ups stay open to everyone, nothing asks for an invite, and running out says friends can still sign up without one. | 27 | granted: "Granted. It applies at once.", |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 28 | }; |
| 29 | ||
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 30 | /** The flash for `?done=`. */ |
| 31 | export function doneMessage(done: string | null): string | null { | |
| 32 | return done ? (INVITES_DONE[done] ?? null) : null; | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 33 | } |
| 34 | ||
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 35 | /** What an account made with a shared link, before registration opened, says about where it came from. */ |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 36 | export function joinedThrough(label: string): string { |
| 37 | return `Joined through ${label}`; | |
| 38 | } | |
| Anyone on g1t can invite a friend again: Invite a friend, in the account menu with how many are left beside it, opens Settings → Invites to g1t with a dialog for their email address and a note. The friend gets an email from them whose link opens sign-up with the address filled in and, because it came from that email, confirmed; their account starts with a workspace of its own, and the page lists every invite sent with where it stands (Pending, confirming their email, Joined as @them), with Copy link, Send again and Revoke on each. It is word of mouth, never a gate: sign-ups stay open to everyone, nothing asks for an invite, and running out says friends can still sign up without one. | 39 | |
| 40 | /** The most invites one grant gives or takes back (`MAX_INVITE_GRANT` in the contracts). */ | |
| 41 | export const MAX_GRANT = 1000; | |
| 42 | ||
| 43 | const USERNAME = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/; | |
| 44 | ||
| 45 | export type Grant = { username: string; amount: number; note: string }; | |
| 46 | ||
| 47 | /** A grant of invites to g1t, as typed: who, how many (negative takes back), and why. */ | |
| 48 | export function parseGrant(form: { get(name: string): unknown }): { ok: true; value: Grant } | { ok: false; error: string } { | |
| 49 | const read = (name: string) => { | |
| 50 | const value = form.get(name); | |
| 51 | return typeof value === "string" ? value.trim() : ""; | |
| 52 | }; | |
| 53 | const username = read("username").replace(/^@/, "").toLowerCase(); | |
| 54 | if (!USERNAME.test(username)) return { ok: false, error: `“${read("username")}” is not a username.` }; | |
| 55 | const raw = read("amount"); | |
| 56 | if (!/^-?\d+$/.test(raw)) return { ok: false, error: "Give a whole number of invites, such as 10, or -5 to take some back." }; | |
| 57 | const amount = Number(raw); | |
| 58 | if (amount === 0 || Math.abs(amount) > MAX_GRANT) return { ok: false, error: `Grant between 1 and ${MAX_GRANT} invites.` }; | |
| 59 | const note = read("note"); | |
| 60 | if (note.length < 3) return { ok: false, error: "Say why, for the record." }; | |
| 61 | return { ok: true, value: { username, amount, note: note.slice(0, 500) } }; | |
| 62 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.