142 lines4,700 bytesCodeBlame
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
8export 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
27export 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
36export 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. */
93export 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. */
99export 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. */
104export 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
119type Store = Pick<Storage, "getItem" | "setItem">;
120
121export function dismissKey(base: string): string {
122 return `g1t:checklist-dismissed:${base.toLowerCase()}`;
123}
124
125export 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
133export 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}