pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/checklist.ts
| 1 | /** |
| 2 | * The steps that take a project to production, each worked out from what |
| 3 | * the project has done, with where to do the ones it has not. Shown on the |
| 4 | * project's overview to its members until they are all done or it is |
| 5 | * dismissed. |
| 6 | */ |
| 7 | |
| 8 | export type ChecklistFacts = { |
| 9 | /** The project's address, `/<workspace>/<project>`. */ |
| 10 | base: string; |
| 11 | /** Its default branch has a commit, or its source is a mirror. */ |
| 12 | hasCode: boolean; |
| 13 | /** Deployments are switched on for it. */ |
| 14 | deploysEnabled: boolean; |
| 15 | /** A production build has gone live, now or before. */ |
| 16 | productionDeployed: boolean; |
| 17 | /** Custom domains it has; null when they could not be read. */ |
| 18 | domains: number | null; |
| 19 | /** A preview has been built for a branch or pull request. */ |
| 20 | previewOpened: boolean; |
| 21 | /** An AGENTS.md or CLAUDE.md at the root of its default branch; null when unknown. */ |
| 22 | instructions: boolean | null; |
| 23 | /** g1t-agent has been given an issue here. */ |
| 24 | agentAssigned: boolean; |
| 25 | }; |
| 26 | |
| 27 | export type ChecklistItem = { |
| 28 | key: "code" | "deploy" | "domain" | "preview" | "instructions" | "agent"; |
| 29 | title: string; |
| 30 | detail: string; |
| 31 | done: boolean; |
| 32 | to: string; |
| 33 | action: string; |
| 34 | }; |
| 35 | |
| 36 | export function productionChecklist(facts: ChecklistFacts): ChecklistItem[] { |
| 37 | const { base } = facts; |
| 38 | return [ |
| 39 | { |
| 40 | key: "code", |
| 41 | title: "Connect a source or push code", |
| 42 | detail: "Push an existing project, or have your coding agent start one.", |
| 43 | done: facts.hasCode, |
| 44 | to: `${base}/code`, |
| 45 | action: "Push code", |
| 46 | }, |
| 47 | { |
| 48 | key: "deploy", |
| 49 | title: "Deploy to production", |
| 50 | detail: facts.deploysEnabled |
| 51 | ? "Production builds from the default branch on every push." |
| 52 | : "Deployments are off until you turn them on. Then production builds from the default branch on every push.", |
| 53 | done: facts.productionDeployed, |
| 54 | to: facts.deploysEnabled ? `${base}/deployments` : `${base}/settings/deployments`, |
| 55 | action: facts.deploysEnabled ? "Deployments" : "Turn on", |
| 56 | }, |
| 57 | { |
| 58 | key: "domain", |
| 59 | title: "Add a custom domain", |
| 60 | detail: "Serve production from a domain of your own.", |
| 61 | done: (facts.domains ?? 0) > 0, |
| 62 | to: `${base}/settings/domains`, |
| 63 | action: "Add", |
| 64 | }, |
| 65 | { |
| 66 | key: "preview", |
| 67 | title: "Open a preview", |
| 68 | detail: "Every pull request gets a live preview of its branch.", |
| 69 | done: facts.previewOpened, |
| 70 | to: `${base}/pulls/new`, |
| 71 | action: "New pull request", |
| 72 | }, |
| 73 | { |
| 74 | key: "instructions", |
| 75 | title: "Set up repository instructions", |
| 76 | detail: "Commit an AGENTS.md with how to build and test; every agent run reads it.", |
| 77 | done: facts.instructions === true, |
| 78 | to: `${base}/agents#instructions`, |
| 79 | action: "How", |
| 80 | }, |
| 81 | { |
| 82 | key: "agent", |
| 83 | title: "Assign a first issue to g1t-agent", |
| 84 | detail: "It opens a pull request, makes the change and sees it through checks and review.", |
| 85 | done: facts.agentAssigned, |
| 86 | to: `${base}/issues/new`, |
| 87 | action: "New issue", |
| 88 | }, |
| 89 | ]; |
| 90 | } |
| 91 | |
| 92 | /** `3/6`, for the card's heading. */ |
| 93 | export function progress(items: ChecklistItem[]): { done: number; total: number; complete: boolean } { |
| 94 | const done = items.filter((item) => item.done).length; |
| 95 | return { done, total: items.length, complete: done === items.length }; |
| 96 | } |
| 97 | |
| 98 | /** A repository's root lists instructions for agents. */ |
| 99 | export function hasInstructions(names: string[]): boolean { |
| 100 | return names.some((name) => /^(agents|claude)\.md$/i.test(name)); |
| 101 | } |
| 102 | |
| 103 | /** Whether g1t-agent has worked here, from its runs, pull requests or issues. */ |
| 104 | export function agentWasAssigned(input: { |
| 105 | runAgents: string[]; |
| 106 | pullAgents: string[]; |
| 107 | issues: { assignees: string[]; agent: string | null }[]; |
| 108 | }): boolean { |
| 109 | const isAgent = (name: string | null) => name?.toLowerCase() === "g1t-agent"; |
| 110 | return ( |
| 111 | input.runAgents.some(isAgent) || |
| 112 | input.pullAgents.some(isAgent) || |
| 113 | input.issues.some((issue) => isAgent(issue.agent) || issue.assignees.some(isAgent)) |
| 114 | ); |
| 115 | } |
| 116 | |
| 117 | // --- Dismissing it, per project, in this browser ----------------------------- |
| 118 | |
| 119 | type Store = Pick<Storage, "getItem" | "setItem">; |
| 120 | |
| 121 | export function dismissKey(base: string): string { |
| 122 | return `g1t:checklist-dismissed:${base.toLowerCase()}`; |
| 123 | } |
| 124 | |
| 125 | export function isDismissed(storage: () => Store | null | undefined, base: string): boolean { |
| 126 | try { |
| 127 | return storage()?.getItem(dismissKey(base)) === "1"; |
| 128 | } catch { |
| 129 | return false; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | export function dismiss(storage: () => Store | null | undefined, base: string): boolean { |
| 134 | try { |
| 135 | const store = storage(); |
| 136 | if (!store) return false; |
| 137 | store.setItem(dismissKey(base), "1"); |
| 138 | return true; |
| 139 | } catch { |
| 140 | return false; |
| 141 | } |
| 142 | } |