1import { useEffect, useRef, useState } from "react";
2import { Line, sleep, TermChrome, useSpinner } from "./_terminal";
3
4const RESOURCES = [
5 { id: "Bucket", type: "Cloudflare.R2.Bucket" },
6 { id: "Worker", type: "Cloudflare.Worker" },
7];
8
9const OUTPUTS: { key: string; value: string }[] = [
10 { key: "bucketName", value: '"myapp-dev_sam-bucket-a3f1"' },
11 {
12 key: "url",
13 value: '"https://myapp-worker-dev_sam-abc123.workers.dev"',
14 },
15];
16
17const STAGE = "dev_sam";
18const COMMAND = `alchemy deploy --stage ${STAGE}`;
19
20type RowStatus = "pending" | "creating" | "created";
21
22export default function StackOutputsTerminal({
23 maxLines = 12,
24}: {
25 maxLines?: number;
26}) {
27 const [cmd, setCmd] = useState("");
28 const [caret, setCaret] = useState(false);
29 const [rows, setRows] = useState<
30 { id: string; type: string; status: RowStatus }[]
31 >([]);
32 const [showOutputs, setShowOutputs] = useState(false);
33 const cancelRef = useRef(false);
34
35 useEffect(() => {
36 cancelRef.current = false;
37 const aborted = () => cancelRef.current;
38
39 const typeCmd = async (text: string) => {
40 setCmd("");
41 setCaret(true);
42 for (let i = 1; i <= text.length; i++) {
43 if (aborted()) return;
44 setCmd(text.slice(0, i));
45 await sleep(28 + Math.random() * 22);
46 }
47 await sleep(160);
48 setCaret(false);
49 };
50
51 const update = (id: string, status: RowStatus) =>
52 setRows((rs) => rs.map((r) => (r.id === id ? { ...r, status } : r)));
53
54 const run = async () => {
55 while (!aborted()) {
56 setCmd("");
57 setCaret(false);
58 setRows([]);
59 setShowOutputs(false);
60 await sleep(900);
61 if (aborted()) return;
62
63 await typeCmd(COMMAND);
64 if (aborted()) return;
65
66 await sleep(220);
67 setRows(
68 RESOURCES.map((r) => ({ ...r, status: "pending" as RowStatus })),
69 );
70 await sleep(260);
71
72 for (const r of RESOURCES) {
73 if (aborted()) return;
74 update(r.id, "creating");
75 await sleep(620);
76 if (aborted()) return;
77 update(r.id, "created");
78 await sleep(140);
79 }
80
81 await sleep(260);
82 setShowOutputs(true);
83 await sleep(4200);
84 }
85 };
86
87 run();
88 return () => {
89 cancelRef.current = true;
90 };
91 }, []);
92
93 const anyInFlight = rows.some((r) => r.status === "creating");
94 const spinner = useSpinner(anyInFlight);
95 const accent = "var(--alc-accent-bright)";
96
97 return (
98 <TermChrome
99 title={`alchemy · ${STAGE}`}
100 badge="DEPLOY"
101 badgeColor={accent}
102 maxLines={maxLines}
103 >
104 <Line>
105 <span style={{ color: accent }}>$ </span>
106 {cmd}
107 {caret && <span style={{ color: "var(--alc-fg-invert)" }}>▍</span>}
108 </Line>
109 {rows.length > 0 && (
110 <>
111 <Line> </Line>
112 {rows.map((r) => {
113 const inFlight = r.status === "creating";
114 const done = r.status === "created";
115 const icon = inFlight ? spinner : done ? "✓" : "+";
116 return (
117 <Line key={r.id}>
118 <span
119 style={{
120 color: accent,
121 width: "1.2em",
122 display: "inline-block",
123 }}
124 >
125 {icon}
126 </span>
127 <span
128 style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}
129 >
130 {r.id}
131 </span>
132 <span style={{ color: "var(--alc-code-comment)" }}>
133 {` (${r.type})`}
134 </span>
135 {inFlight && (
136 <span style={{ color: accent, marginLeft: 6 }}>
137 {r.status}
138 </span>
139 )}
140 </Line>
141 );
142 })}
143 </>
144 )}
145 {showOutputs && (
146 <>
147 <Line> </Line>
148 <Line>
149 <span style={{ color: accent }}>✓ </span>
150 <span>deployed in </span>
151 <span style={{ color: "var(--alc-fg-invert)", fontWeight: 600 }}>
152 3.4s
153 </span>
154 </Line>
155 <Line> </Line>
156 <Line>
157 <span style={{ color: "var(--alc-code-comment)" }}>outputs:</span>
158 </Line>
159 <Line>
160 <span style={{ color: "var(--alc-fg-invert)" }}>{"{"}</span>
161 </Line>
162 {OUTPUTS.map((o) => (
163 <Line key={o.key}>
164 <span style={{ color: "var(--alc-fg-invert)" }}>{" "}</span>
165 <span style={{ color: "var(--alc-fg-invert)" }}>{o.key}</span>
166 <span style={{ color: "var(--alc-code-comment)" }}>: </span>
167 <span style={{ color: accent }}>{o.value}</span>
168 <span style={{ color: "var(--alc-fg-invert)" }}>,</span>
169 </Line>
170 ))}
171 <Line>
172 <span style={{ color: "var(--alc-fg-invert)" }}>{"}"}</span>
173 </Line>
174 </>
175 )}
176 </TermChrome>
177 );
178}