Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
215 linesCodeBlameRaw

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 running1/**
2 * The Invoices and Audit log pages' arithmetic and wording: filters read
3 * from the address, totals, links from billing's account ids to sudo's
4 * pages, and paging. No Workers imports, so it can be tested under Node.
5 */
6import type { AdminAction, InvoiceSummary } from "@g1t/contracts";
7
8// --- Invoices -----------------------------------------------------------------
9
10export const INVOICE_STATUSES = ["open", "overdue", "failed", "paid", "void"] as const;
11export type InvoiceStatusFilter = (typeof INVOICE_STATUSES)[number];
12
13export function parseInvoiceStatus(value: string | null): InvoiceStatusFilter | null {
14 return INVOICE_STATUSES.find((status) => status === value) ?? null;
15}
16
17/** A month as `<input type="month">` sends it: `2026-10`. */
18export function parseMonth(value: string | null): string | null {
19 if (!value || !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) return null;
20 return value;
21}
22
23/** Not paid and not void: what is still owed. */
24export function isOutstanding(status: string): boolean {
25 return status === "open" || status === "overdue" || status === "failed";
26}
27
28export type InvoiceTotals = { count: number; amountMicros: number; paidMicros: number; outstandingMicros: number; outstanding: number };
29
30export function invoiceTotals(invoices: Pick<InvoiceSummary, "amountMicros" | "status">[]): InvoiceTotals {
31 const totals: InvoiceTotals = { count: invoices.length, amountMicros: 0, paidMicros: 0, outstandingMicros: 0, outstanding: 0 };
32 for (const invoice of invoices) {
33 if (invoice.status === "void") continue;
34 totals.amountMicros += invoice.amountMicros;
35 if (invoice.status === "paid") totals.paidMicros += invoice.amountMicros;
36 if (isOutstanding(invoice.status)) {
37 totals.outstandingMicros += invoice.amountMicros;
38 totals.outstanding += 1;
39 }
40 }
41 return totals;
42}
43
44/** A link to the invoices list with these filters; the defaults are left out. */
45export function invoicesHref({ status, month }: { status?: string | null; month?: string | null }): string {
46 const params = new URLSearchParams();
47 if (status) params.set("status", status);
48 if (month) params.set("month", month);
49 const query = params.toString();
50 return query ? `/invoices?${query}` : "/invoices";
51}
52
53/** Only https links to Stripe are followed. */
54export function safeUrl(url: string | null | undefined): string | null {
55 return url && url.startsWith("https://") ? url : null;
56}
57
58// --- Accounts -------------------------------------------------------------------
59
60const SLUG = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
61const ENTERPRISE = /^ent_[a-z0-9_-]{1,80}$/;
62
63/**
64 * The sudo page for one of billing's account ids: `ws_<slug>` is the
65 * workspace's, `ent_…` the enterprise's. Null for anything else, such as
66 * Stripe's own lines.
67 */
68export function accountPath(account: string | null | undefined): string | null {
69 const id = (account ?? "").trim().toLowerCase();
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)70 // Shared invite links (identity's shared_invites.rs): `invites` is a reserved name, never a workspace's.
71 if (id === SHARED_INVITES_ACCOUNT) return "/invites?tab=shared";
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 running72 const status = /^(incident|maintenance):([a-z0-9-]{1,64})$/.exec(id);
73 if (status) return status[1] === "incident" ? `/incidents/${status[2]}` : `/incidents/maintenance/${status[2]}`;
74 if (ENTERPRISE.test(id)) return `/enterprises/${encodeURIComponent(id)}`;
75 if (id.startsWith("ws_") && SLUG.test(id.slice(3))) return `/workspaces/${encodeURIComponent(id.slice(3))}`;
76 return null;
77}
78
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)79/** Where sudo's audit log files shared invite links. */
80export const SHARED_INVITES_ACCOUNT = "ws_invites";
81
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 running82/** What to call an account: a workspace by its slug, an enterprise by its name when known. */
83export function accountName(account: string, names: Map<string, string> = new Map()): string {
84 if (names.has(account)) return names.get(account) as string;
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.85 // The retired model catalogue, in older entries.
86 if (account === "models") return "Model catalogue (retired)";
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)87 if (account === SHARED_INVITES_ACCOUNT) return "Shared invite links";
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 running88 if (account.startsWith("incident:")) return "Incident";
89 if (account.startsWith("maintenance:")) return "Maintenance";
90 if (account.startsWith("ws_")) return account.slice(3);
91 if (ENTERPRISE.test(account)) return "an enterprise";
92 return account;
93}
94
95// --- Audit ----------------------------------------------------------------------
96
97/** Each kind of change, as staff read it. */
98export const AUDIT_ACTIONS: Record<string, string> = {
99 terms: "Terms changed",
100 allowances: "Plan, pools and caps changed",
101 payment: "Bank transfer recorded",
102 goodwill: "Goodwill credit",
103 request: "Request answered",
104 create: "Enterprise created",
105 attach: "Workspace added",
106 detach: "Workspace removed",
107 credit: "Credit issued",
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's108 reset: "Billing reset (testing)",
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 running109 billing_link: "Billing link made",
110 billing_email: "Invoice email set",
111 invoice: "Invoice sent",
112 dispute: "Payment disputed",
113 sales: "Sales record changed",
114 note: "Sales note added",
115 stripe: "From Stripe",
116 webhook: "Stripe webhook registered",
117 close: "Billing closed",
118 // From identity: deleted workspaces staff restored or purged.
119 workspace_restored: "Workspace restored",
120 workspace_purged: "Workspace purged",
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)121 // From identity: accounts staff deleted, restored or purged.
122 account_deleted: "Account deleted",
123 account_restored: "Account restored",
124 account_purged: "Account purged",
Merge branch 'worktree-agent-a8385d293d42c913a'125 // From identity: workspace aliases staff set or removed.
126 alias_added: "Alias added",
127 alias_removed: "Alias removed",
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)128 // From identity: shared invite links staff made or revoked, filed under `ws_invites`.
129 shared_invite_created: "Shared invite link made",
130 shared_invite_revoked: "Shared invite link revoked",
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 running131 // From the status page (apps/status), merged in by the Audit log page.
132 incident_declared: "Incident declared",
133 incident_detected: "Incident detected",
134 incident_update: "Incident update",
135 incident_note: "Incident note",
136 incident_resolved: "Incident resolved",
137 incident_roles: "Incident roles",
138 incident_published: "Incident published",
139 incident_dismissed: "Draft dismissed",
140 incident_followup: "Incident follow-up",
141 postmortem_saved: "Postmortem saved",
142 postmortem_published: "Postmortem published",
143 postmortem_unpublished: "Postmortem taken down",
144 maintenance_scheduled: "Maintenance scheduled",
145 maintenance_update: "Maintenance update",
146 maintenance_started: "Maintenance started",
147 maintenance_completed: "Maintenance completed",
148 maintenance_cancelled: "Maintenance cancelled",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging149 credit_revoked: "Credit revoked",
150 credit_expired: "Credit expired",
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.151 // The model catalogue, retired with g1t's own agents: older entries still name these.
Merge branch 'main' into actions-toolkit-oidc-artifacts152 models_discovered: "Models found or gone",
153 model_approved: "Model approved",
154 model_retired: "Model retired",
155 model_restored: "Model restored",
156 model_default: "Model default changed",
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 running157};
158
159/**
160 * The status page's audit lines as the Audit log lists billing's: the
161 * account is `incident:<id>` or `maintenance:<id>`, which link to their
162 * pages in sudo.
163 */
164export function statusAuditActions(entries: { id: string; at: string; by: string; action: string; target: string; detail: string }[]): AdminAction[] {
165 return entries.map((e) => ({
166 id: `status-${e.id}`,
167 account: `${e.action.startsWith("maintenance_") ? "maintenance" : "incident"}:${e.target}`,
168 action: e.action,
169 detail: e.detail,
170 by: e.by,
171 createdAt: e.at,
172 }));
173}
174
175/** Billing's lines and the status page's, newest first, one page's worth. */
176export function mergeAudit(a: AdminAction[], b: AdminAction[], page = AUDIT_PAGE): AdminAction[] {
177 return [...a, ...b].sort((x, y) => y.createdAt.localeCompare(x.createdAt)).slice(0, page);
178}
179
180export function actionLabel(action: string): string {
181 return AUDIT_ACTIONS[action] ?? action.replace(/_/g, " ").replace(/^./, (char) => char.toUpperCase());
182}
183
184export const AUDIT_PAGE = 100;
185
186/** Where the next, older page starts: after the last line, if this page was full. */
187export function olderBefore(actions: Pick<AdminAction, "createdAt">[], page = AUDIT_PAGE): string | null {
188 return actions.length >= page ? (actions.at(-1)?.createdAt ?? null) : null;
189}
190
191/** A staff email filter: lowercased, at most one address's length. */
192export function parseBy(value: string | null): string | null {
193 const by = (value ?? "").trim().toLowerCase();
194 return by && by.length <= 254 && !/\s/.test(by) ? by : null;
195}
196
197export function parseAction(value: string | null): string | null {
198 const action = (value ?? "").trim();
199 return /^[a-z_]{1,40}$/.test(action) ? action : null;
200}
201
202/** An RFC 3339 time to page from. */
203export function parseBefore(value: string | null): string | null {
204 if (!value || value.length > 40 || Number.isNaN(new Date(value).getTime())) return null;
205 return value;
206}
207
208export function auditHref({ by, action, before }: { by?: string | null; action?: string | null; before?: string | null }): string {
209 const params = new URLSearchParams();
210 if (by) params.set("by", by);
211 if (action) params.set("action", action);
212 if (before) params.set("before", before);
213 const query = params.toString();
214 return query ? `/audit?${query}` : "/audit";
215}

This file's history is long; its oldest lines are credited to the oldest commit read.