@liiift-studio/deploy-vercel-from-sanity 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,768 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ vercelDeploy: () => vercelDeploy,
24
+ vercelDeploySchema: () => vercelDeploySchema
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var import_sanity4 = require("sanity");
28
+ var import_icons6 = require("@sanity/icons");
29
+
30
+ // src/components/DeployTool.tsx
31
+ var import_react4 = require("react");
32
+ var import_sanity2 = require("sanity");
33
+ var import_ui5 = require("@sanity/ui");
34
+ var import_icons4 = require("@sanity/icons");
35
+
36
+ // src/components/DeployItem.tsx
37
+ var import_react2 = require("react");
38
+ var import_ui3 = require("@sanity/ui");
39
+ var import_icons2 = require("@sanity/icons");
40
+
41
+ // src/lib/api.ts
42
+ var BASE = "https://api.vercel.com";
43
+ async function vercelFetch(path, token, init) {
44
+ const res = await fetch(`${BASE}${path}`, {
45
+ ...init,
46
+ headers: {
47
+ Authorization: `Bearer ${token}`,
48
+ "Content-Type": "application/json",
49
+ ...init?.headers
50
+ }
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => res.statusText);
54
+ throw new Error(`Vercel API ${res.status}: ${text}`);
55
+ }
56
+ return res.json();
57
+ }
58
+ async function listDeployments(opts) {
59
+ const params = new URLSearchParams({
60
+ projectId: opts.projectId,
61
+ "meta-deployHookId": opts.hookId,
62
+ limit: String(opts.limit ?? 10)
63
+ });
64
+ if (opts.teamId) params.set("teamId", opts.teamId);
65
+ const data = await vercelFetch(
66
+ `/v6/deployments?${params}`,
67
+ opts.token
68
+ );
69
+ return data.deployments ?? [];
70
+ }
71
+ async function cancelDeployment(opts) {
72
+ const params = opts.teamId ? `?teamId=${opts.teamId}` : "";
73
+ await vercelFetch(`/v12/deployments/${opts.deploymentId}/cancel${params}`, opts.token, {
74
+ method: "PATCH"
75
+ });
76
+ }
77
+ async function triggerDeploy(hookUrl) {
78
+ const res = await fetch(hookUrl, { method: "POST" });
79
+ if (!res.ok) throw new Error(`Deploy hook returned ${res.status}`);
80
+ }
81
+
82
+ // src/lib/helpers.ts
83
+ function parseHookUrl(url) {
84
+ try {
85
+ const path = new URL(url).pathname;
86
+ const parts = path.split("/").filter(Boolean);
87
+ return {
88
+ projectId: parts[3] ?? "",
89
+ hookId: parts[4] ?? ""
90
+ };
91
+ } catch {
92
+ return { projectId: "", hookId: "" };
93
+ }
94
+ }
95
+ var ACTIVE_STATES = /* @__PURE__ */ new Set([
96
+ "QUEUED",
97
+ "INITIALIZING",
98
+ "BUILDING"
99
+ ]);
100
+ function isActiveState(state) {
101
+ return !!state && ACTIVE_STATES.has(state);
102
+ }
103
+ function formatDuration(seconds) {
104
+ if (seconds < 60) return `${seconds}s`;
105
+ const m = Math.floor(seconds / 60);
106
+ const s = seconds % 60;
107
+ return `${m}m ${s}s`;
108
+ }
109
+ function timeAgo(ms) {
110
+ const diff = Math.floor((Date.now() - ms) / 1e3);
111
+ if (diff < 60) return `${diff}s ago`;
112
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
113
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
114
+ return `${Math.floor(diff / 86400)}d ago`;
115
+ }
116
+ function safeHref(url) {
117
+ if (!url) return void 0;
118
+ try {
119
+ const parsed = new URL(url);
120
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
121
+ return url;
122
+ } catch {
123
+ return void 0;
124
+ }
125
+ }
126
+ function shortSha(sha) {
127
+ return sha ? sha.slice(0, 7) : "";
128
+ }
129
+ function stateLabel(state) {
130
+ switch (state) {
131
+ case "READY":
132
+ return { label: "Ready", tone: "positive" };
133
+ case "BUILDING":
134
+ return { label: "Building", tone: "caution" };
135
+ case "QUEUED":
136
+ return { label: "Queued", tone: "caution" };
137
+ case "INITIALIZING":
138
+ return { label: "Initializing", tone: "caution" };
139
+ case "ERROR":
140
+ return { label: "Error", tone: "critical" };
141
+ case "CANCELED":
142
+ return { label: "Canceled", tone: "default" };
143
+ case "LOADING":
144
+ return { label: "Loading\u2026", tone: "default" };
145
+ default:
146
+ return { label: "Unknown", tone: "default" };
147
+ }
148
+ }
149
+
150
+ // src/components/StatusBadge.tsx
151
+ var import_ui = require("@sanity/ui");
152
+ var import_jsx_runtime = require("react/jsx-runtime");
153
+ function StatusBadge({ state, showSpinner }) {
154
+ const { label, tone } = stateLabel(state);
155
+ const spinning = showSpinner && (state === "QUEUED" || state === "INITIALIZING" || state === "BUILDING");
156
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ui.Flex, { align: "center", gap: 2, children: [
157
+ spinning && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Spinner, { muted: true }),
158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Badge, { tone, mode: "outline", children: label })
159
+ ] });
160
+ }
161
+
162
+ // src/components/DeployHistory.tsx
163
+ var import_react = require("react");
164
+ var import_ui2 = require("@sanity/ui");
165
+ var import_icons = require("@sanity/icons");
166
+ var import_jsx_runtime2 = require("react/jsx-runtime");
167
+ function DeployHistory({ target, token, onClose }) {
168
+ const [deployments, setDeployments] = (0, import_react.useState)([]);
169
+ const [loading, setLoading] = (0, import_react.useState)(true);
170
+ const [error, setError] = (0, import_react.useState)(null);
171
+ const { projectId, hookId } = parseHookUrl(target.url);
172
+ const load = (0, import_react.useCallback)(async () => {
173
+ setLoading(true);
174
+ setError(null);
175
+ try {
176
+ const data = await listDeployments({ projectId, hookId, token, teamId: target.teamId, limit: 10 });
177
+ setDeployments(data);
178
+ } catch (err) {
179
+ setError(err instanceof Error ? err.message : "Failed to load history");
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ }, [projectId, hookId, token, target.teamId]);
184
+ (0, import_react.useEffect)(() => {
185
+ load();
186
+ }, [load]);
187
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
188
+ import_ui2.Dialog,
189
+ {
190
+ header: `${target.name} \u2014 Deployment History`,
191
+ id: "deploy-history",
192
+ onClose,
193
+ width: 2,
194
+ footer: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { padding: 3, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Button, { text: "Close", icon: import_icons.CloseIcon, mode: "ghost", onClick: onClose }) }),
195
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Box, { padding: 4, children: [
196
+ loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Flex, { justify: "center", padding: 6, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Spinner, { muted: true }) }),
197
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Card, { tone: "critical", padding: 4, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, children: error }) }),
198
+ !loading && !error && deployments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Card, { tone: "transparent", padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, muted: true, align: "center", children: "No deployments found for this hook." }) }),
199
+ !loading && deployments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Stack, { space: 2, children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Card, { padding: 3, radius: 2, tone: "transparent", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Flex, { gap: 3, children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 2, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 0, weight: "semibold", muted: true, children: "Preview URL" }) }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 0, weight: "semibold", muted: true, children: "Status" }) }),
203
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 2, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 0, weight: "semibold", muted: true, children: "Branch \xB7 Commit" }) }),
204
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 0, weight: "semibold", muted: true, children: "Deployed" }) }),
205
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { style: { width: 64 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 0, weight: "semibold", muted: true, children: "Logs" }) })
206
+ ] }) }),
207
+ deployments.map((d) => {
208
+ const { label, tone } = stateLabel(d.state);
209
+ const branch = d.meta?.githubCommitRef ?? "\u2014";
210
+ const sha = shortSha(d.meta?.githubCommitSha);
211
+ const message = d.meta?.githubCommitMessage?.split("\n")[0] ?? "";
212
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Card, { padding: 3, radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Flex, { gap: 3, align: "center", children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 2, style: { overflow: "hidden" }, children: d.url ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
214
+ "a",
215
+ {
216
+ href: `https://${d.url}`,
217
+ target: "_blank",
218
+ rel: "noreferrer",
219
+ style: { color: "inherit" },
220
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, style: { textDecoration: "underline" }, children: d.url.length > 36 ? `${d.url.slice(0, 36)}\u2026` : d.url })
221
+ }
222
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, muted: true, children: "\u2014" }) }),
223
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Badge, { tone, mode: "outline", children: label }) }),
224
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 2, style: { overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Stack, { space: 1, children: [
225
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, children: branch }),
226
+ sha && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui2.Text, { size: 0, muted: true, children: [
227
+ sha,
228
+ message ? ` \xB7 ${message.slice(0, 40)}${message.length > 40 ? "\u2026" : ""}` : ""
229
+ ] })
230
+ ] }) }),
231
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { flex: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, muted: true, children: timeAgo(d.created) }) }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Box, { style: { width: 64 }, children: safeHref(d.inspectorUrl) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: safeHref(d.inspectorUrl), target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
233
+ import_ui2.Button,
234
+ {
235
+ text: "Logs",
236
+ mode: "ghost",
237
+ tone: "default",
238
+ icon: import_icons.LaunchIcon,
239
+ style: { fontSize: "12px" }
240
+ }
241
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui2.Text, { size: 1, muted: true, children: "\u2014" }) })
242
+ ] }) }, d.uid);
243
+ })
244
+ ] })
245
+ ] })
246
+ }
247
+ );
248
+ }
249
+
250
+ // src/components/DeployItem.tsx
251
+ var import_jsx_runtime3 = require("react/jsx-runtime");
252
+ var POLL_INTERVAL_MS = 5e3;
253
+ function DeployItem({ target, token, onDelete }) {
254
+ const { projectId, hookId } = parseHookUrl(target.url);
255
+ const [deployments, setDeployments] = (0, import_react2.useState)([]);
256
+ const [loadingInitial, setLoadingInitial] = (0, import_react2.useState)(true);
257
+ const [triggering, setTriggering] = (0, import_react2.useState)(false);
258
+ const [canceling, setCanceling] = (0, import_react2.useState)(false);
259
+ const [deployError, setDeployError] = (0, import_react2.useState)(null);
260
+ const [showHistory, setShowHistory] = (0, import_react2.useState)(false);
261
+ const [elapsed, setElapsed] = (0, import_react2.useState)(0);
262
+ const latest = deployments[0];
263
+ const isActive = triggering || isActiveState(latest?.state);
264
+ const fetchDeployments = (0, import_react2.useCallback)(async () => {
265
+ if (!projectId || !hookId || !token) return;
266
+ try {
267
+ const data = await listDeployments({ projectId, hookId, token, teamId: target.teamId });
268
+ setDeployments(data);
269
+ } catch (err) {
270
+ console.error("deploy-vercel-from-sanity: fetch error", err);
271
+ }
272
+ }, [projectId, hookId, token, target.teamId]);
273
+ (0, import_react2.useEffect)(() => {
274
+ fetchDeployments().finally(() => setLoadingInitial(false));
275
+ }, [fetchDeployments]);
276
+ (0, import_react2.useEffect)(() => {
277
+ if (!isActive) return;
278
+ const id = setInterval(fetchDeployments, POLL_INTERVAL_MS);
279
+ return () => clearInterval(id);
280
+ }, [isActive, fetchDeployments]);
281
+ (0, import_react2.useEffect)(() => {
282
+ if (triggering && latest && latest.state !== void 0) {
283
+ setTriggering(false);
284
+ }
285
+ }, [triggering, latest]);
286
+ const timerRef = (0, import_react2.useRef)(null);
287
+ (0, import_react2.useEffect)(() => {
288
+ if (isActive) {
289
+ const start = latest?.created ?? Date.now();
290
+ setElapsed(Math.floor((Date.now() - start) / 1e3));
291
+ timerRef.current = setInterval(() => {
292
+ setElapsed(Math.floor((Date.now() - start) / 1e3));
293
+ }, 1e3);
294
+ } else {
295
+ setElapsed(0);
296
+ if (timerRef.current) clearInterval(timerRef.current);
297
+ }
298
+ return () => {
299
+ if (timerRef.current) clearInterval(timerRef.current);
300
+ };
301
+ }, [isActive, latest?.created]);
302
+ const deploy = (0, import_react2.useCallback)(async () => {
303
+ setDeployError(null);
304
+ setTriggering(true);
305
+ try {
306
+ await triggerDeploy(target.url);
307
+ setTimeout(fetchDeployments, 2e3);
308
+ } catch (err) {
309
+ setTriggering(false);
310
+ setDeployError(err instanceof Error ? err.message : "Deploy failed");
311
+ }
312
+ }, [target.url, fetchDeployments]);
313
+ const cancel = (0, import_react2.useCallback)(async () => {
314
+ if (!latest?.uid) return;
315
+ setCanceling(true);
316
+ try {
317
+ await cancelDeployment({ deploymentId: latest.uid, token, teamId: target.teamId });
318
+ await fetchDeployments();
319
+ } catch (err) {
320
+ console.error("deploy-vercel-from-sanity: cancel error", err);
321
+ } finally {
322
+ setCanceling(false);
323
+ }
324
+ }, [latest?.uid, token, target.teamId, fetchDeployments]);
325
+ const branch = latest?.meta?.githubCommitRef;
326
+ const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
327
+ const sha = shortSha(latest?.meta?.githubCommitSha);
328
+ const creator = latest?.creator?.username;
329
+ const deployedAt = latest?.created ? timeAgo(latest.created) : null;
330
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
331
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Card, { padding: 4, radius: 2, shadow: 1, tone: "default", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 4, children: [
332
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "flex-start", justify: "space-between", gap: 3, children: [
333
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 2, flex: 1, children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 2, weight: "semibold", children: target.name }),
335
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { gap: 2, wrap: "wrap", children: [
336
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, children: projectId ? `${projectId.slice(0, 18)}\u2026` : "\u2014" }),
337
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, children: "\xB7" }),
338
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Text, { size: 0, muted: true, children: [
339
+ "Hook: ",
340
+ hookId || "\u2014"
341
+ ] }),
342
+ target.teamId && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
343
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, children: "\xB7" }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Text, { size: 0, muted: true, children: [
345
+ "Team: ",
346
+ target.teamId
347
+ ] })
348
+ ] })
349
+ ] })
350
+ ] }),
351
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
352
+ import_ui3.MenuButton,
353
+ {
354
+ button: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Button, { mode: "ghost", icon: import_icons2.EllipsisVerticalIcon }),
355
+ id: `menu-${target._id}`,
356
+ menu: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Menu, { children: [
357
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
358
+ import_ui3.MenuItem,
359
+ {
360
+ text: "History",
361
+ icon: import_icons2.HistoryIcon,
362
+ onClick: () => setShowHistory(true)
363
+ }
364
+ ),
365
+ safeHref(latest?.inspectorUrl) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
366
+ import_ui3.MenuItem,
367
+ {
368
+ text: "Build logs",
369
+ icon: import_icons2.LaunchIcon,
370
+ as: "a",
371
+ href: safeHref(latest?.inspectorUrl),
372
+ target: "_blank",
373
+ rel: "noreferrer"
374
+ }
375
+ ),
376
+ !target.disableDeleteAction && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
377
+ import_ui3.MenuItem,
378
+ {
379
+ text: "Delete",
380
+ icon: import_icons2.TrashIcon,
381
+ tone: "critical",
382
+ onClick: () => onDelete(target)
383
+ }
384
+ )
385
+ ] }),
386
+ popover: { placement: "bottom-end" }
387
+ }
388
+ )
389
+ ] }),
390
+ loadingInitial ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 2, children: [
391
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
392
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: "Loading\u2026" })
393
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 3, children: [
394
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 3, wrap: "wrap", children: [
395
+ triggering ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 2, children: [
396
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
397
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "caution", mode: "outline", children: "Triggering\u2026" })
398
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state, showSpinner: true }),
399
+ isActive && elapsed > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
400
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.ClockIcon, {}),
401
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: formatDuration(elapsed) })
402
+ ] }),
403
+ !isActive && deployedAt && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: deployedAt }),
404
+ branch && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
405
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: "\xB7" }),
406
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "default", mode: "outline", children: branch })
407
+ ] }),
408
+ sha && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
409
+ import_ui3.Tooltip,
410
+ {
411
+ content: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Box, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: commitMsg ?? sha }) }),
412
+ portal: true,
413
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, style: { cursor: "default" }, children: sha })
414
+ }
415
+ ),
416
+ creator && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Text, { size: 1, muted: true, children: [
417
+ "by ",
418
+ creator
419
+ ] }),
420
+ latest?.url && latest.state === "READY" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
421
+ "a",
422
+ {
423
+ href: `https://${latest.url}`,
424
+ target: "_blank",
425
+ rel: "noreferrer",
426
+ style: { color: "inherit" },
427
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
428
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.LaunchIcon, {}),
429
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: "Preview" })
430
+ ] })
431
+ }
432
+ )
433
+ ] }),
434
+ deployError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: deployError }) })
435
+ ] }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", justify: "flex-end", gap: 2, children: [
437
+ isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
438
+ import_ui3.Button,
439
+ {
440
+ text: "Cancel",
441
+ mode: "ghost",
442
+ tone: "critical",
443
+ loading: canceling,
444
+ disabled: canceling,
445
+ onClick: cancel
446
+ }
447
+ ),
448
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
449
+ import_ui3.Button,
450
+ {
451
+ text: "Deploy",
452
+ tone: "primary",
453
+ icon: import_icons2.RocketIcon,
454
+ loading: triggering,
455
+ disabled: isActive,
456
+ onClick: deploy
457
+ }
458
+ )
459
+ ] })
460
+ ] }) }),
461
+ showHistory && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
462
+ DeployHistory,
463
+ {
464
+ target,
465
+ token,
466
+ onClose: () => setShowHistory(false)
467
+ }
468
+ )
469
+ ] });
470
+ }
471
+
472
+ // src/components/TokenSetup.tsx
473
+ var import_react3 = require("react");
474
+ var import_sanity = require("sanity");
475
+ var import_ui4 = require("@sanity/ui");
476
+ var import_icons3 = require("@sanity/icons");
477
+ var import_jsx_runtime4 = require("react/jsx-runtime");
478
+ var TOKEN_DOC_ID = "secrets.vercelDeploy";
479
+ function TokenSetup({ onSaved }) {
480
+ const client = (0, import_sanity.useClient)({ apiVersion: "2025-01-01" });
481
+ const [token, setToken] = (0, import_react3.useState)("");
482
+ const [saving, setSaving] = (0, import_react3.useState)(false);
483
+ const [error, setError] = (0, import_react3.useState)(null);
484
+ const save = (0, import_react3.useCallback)(async () => {
485
+ if (!token.trim()) return;
486
+ setSaving(true);
487
+ setError(null);
488
+ try {
489
+ await client.createOrReplace({
490
+ _id: TOKEN_DOC_ID,
491
+ _type: "vercelDeploy.config",
492
+ accessToken: token.trim()
493
+ });
494
+ onSaved();
495
+ } catch (err) {
496
+ setError(err instanceof Error ? err.message : "Failed to save token");
497
+ } finally {
498
+ setSaving(false);
499
+ }
500
+ }, [client, token, onSaved]);
501
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Card, { height: "fill", tone: "transparent", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Flex, { align: "center", justify: "center", height: "fill", padding: 6, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Card, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Stack, { space: 5, children: [
502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Flex, { align: "center", gap: 3, children: [
503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Text, { size: 3, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_icons3.KeyIcon, {}) }),
504
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Heading, { size: 2, children: "Connect to Vercel" })
505
+ ] }),
506
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Stack, { space: 3, children: [
507
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Text, { size: 1, muted: true, children: [
508
+ "A Vercel API token is required to read deployment status, history, and build logs. Your token is stored securely in the Sanity dataset under a",
509
+ " ",
510
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { children: "secrets.*" }),
511
+ " document ID that is not publicly readable."
512
+ ] }),
513
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Text, { size: 1, muted: true, children: [
514
+ "Create a token at",
515
+ " ",
516
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: "vercel.com \u2192 Settings \u2192 Tokens" }),
517
+ ". Choose ",
518
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: "Full Account" }),
519
+ " scope."
520
+ ] })
521
+ ] }),
522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_ui4.Stack, { space: 3, children: [
523
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Text, { size: 1, weight: "semibold", children: "Vercel API Token" }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
525
+ import_ui4.TextInput,
526
+ {
527
+ value: token,
528
+ onChange: (e) => setToken(e.target.value),
529
+ placeholder: "xxxxxxxxxxxxxxxxxxxxxxxx",
530
+ type: "password"
531
+ }
532
+ )
533
+ ] }),
534
+ error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Card, { tone: "critical", padding: 3, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ui4.Text, { size: 1, children: error }) }),
535
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
536
+ import_ui4.Button,
537
+ {
538
+ text: "Save and connect",
539
+ tone: "primary",
540
+ icon: import_icons3.CheckmarkCircleIcon,
541
+ loading: saving,
542
+ disabled: !token.trim() || saving,
543
+ onClick: save
544
+ }
545
+ )
546
+ ] }) }) }) });
547
+ }
548
+
549
+ // src/components/DeployTool.tsx
550
+ var import_jsx_runtime5 = require("react/jsx-runtime");
551
+ var TOKEN_QUERY = `*[_id == "secrets.vercelDeploy"][0].accessToken`;
552
+ var TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)`;
553
+ function DeployTool() {
554
+ const client = (0, import_sanity2.useClient)({ apiVersion: "2025-01-01" });
555
+ const toast = (0, import_ui5.useToast)();
556
+ const [token, setToken] = (0, import_react4.useState)(null);
557
+ const [targets, setTargets] = (0, import_react4.useState)([]);
558
+ const [loading, setLoading] = (0, import_react4.useState)(true);
559
+ const [showTokenSetup, setShowTokenSetup] = (0, import_react4.useState)(false);
560
+ const [pendingDelete, setPendingDelete] = (0, import_react4.useState)(null);
561
+ const [deleting, setDeleting] = (0, import_react4.useState)(false);
562
+ const load = (0, import_react4.useCallback)(async () => {
563
+ setLoading(true);
564
+ try {
565
+ const [fetchedToken, fetchedTargets] = await Promise.all([
566
+ client.fetch(TOKEN_QUERY),
567
+ client.fetch(TARGETS_QUERY)
568
+ ]);
569
+ setToken(fetchedToken ?? null);
570
+ setTargets(fetchedTargets);
571
+ } catch (err) {
572
+ console.error("deploy-vercel-from-sanity: load error", err);
573
+ } finally {
574
+ setLoading(false);
575
+ }
576
+ }, [client]);
577
+ (0, import_react4.useEffect)(() => {
578
+ load();
579
+ }, [load]);
580
+ (0, import_react4.useEffect)(() => {
581
+ const sub = client.listen(TARGETS_QUERY).subscribe(() => {
582
+ client.fetch(TARGETS_QUERY).then(setTargets).catch((err) => {
583
+ console.error("deploy-vercel-from-sanity: subscription refresh error", err);
584
+ });
585
+ });
586
+ return () => sub.unsubscribe();
587
+ }, [client]);
588
+ const confirmDelete = (0, import_react4.useCallback)(async () => {
589
+ if (!pendingDelete) return;
590
+ setDeleting(true);
591
+ try {
592
+ await client.delete(pendingDelete._id);
593
+ setTargets((prev) => prev.filter((t) => t._id !== pendingDelete._id));
594
+ toast.push({ status: "success", title: `Deleted "${pendingDelete.name}"` });
595
+ } catch (err) {
596
+ toast.push({ status: "error", title: "Delete failed", description: String(err) });
597
+ } finally {
598
+ setDeleting(false);
599
+ setPendingDelete(null);
600
+ }
601
+ }, [client, pendingDelete, toast]);
602
+ if (loading) {
603
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Card, { height: "fill", tone: "transparent", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Flex, { align: "center", justify: "center", height: "fill", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Spinner, { muted: true }) }) });
604
+ }
605
+ if (!token && !showTokenSetup) {
606
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TokenSetup, { onSaved: () => {
607
+ setShowTokenSetup(false);
608
+ load();
609
+ } });
610
+ }
611
+ if (showTokenSetup) {
612
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
613
+ TokenSetup,
614
+ {
615
+ onSaved: () => {
616
+ setShowTokenSetup(false);
617
+ load();
618
+ }
619
+ }
620
+ );
621
+ }
622
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Card, { height: "fill", tone: "transparent", children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Box, { padding: 5, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Stack, { space: 5, children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { align: "center", justify: "space-between", children: [
625
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { align: "center", gap: 3, children: [
626
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons4.RocketIcon, {}),
627
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Heading, { size: 2, children: "Deploy" })
628
+ ] }),
629
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
630
+ import_ui5.Button,
631
+ {
632
+ text: "API Token",
633
+ mode: "ghost",
634
+ icon: import_icons4.KeyIcon,
635
+ fontSize: 1,
636
+ onClick: () => setShowTokenSetup(true)
637
+ }
638
+ )
639
+ ] }),
640
+ targets.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Card, { padding: 5, radius: 2, tone: "transparent", shadow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Stack, { space: 3, style: { textAlign: "center" }, children: [
641
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Text, { size: 2, weight: "semibold", children: "No deploy targets configured" }),
642
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Text, { size: 1, muted: true, children: [
643
+ "Create a ",
644
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { children: "vercel_deploy" }),
645
+ " document in the dataset with a Vercel deploy hook URL, or add one via the Sanity CLI."
646
+ ] })
647
+ ] }) }),
648
+ token && targets.map((target) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
649
+ DeployItem,
650
+ {
651
+ target,
652
+ token,
653
+ onDelete: setPendingDelete
654
+ },
655
+ target._id
656
+ ))
657
+ ] }) }),
658
+ pendingDelete && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
659
+ import_ui5.Dialog,
660
+ {
661
+ header: "Delete deploy target?",
662
+ id: "confirm-delete",
663
+ onClose: () => setPendingDelete(null),
664
+ width: 1,
665
+ footer: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { padding: 3, gap: 2, justify: "flex-end", children: [
666
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
667
+ import_ui5.Button,
668
+ {
669
+ text: "Cancel",
670
+ mode: "ghost",
671
+ onClick: () => setPendingDelete(null)
672
+ }
673
+ ),
674
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
675
+ import_ui5.Button,
676
+ {
677
+ text: "Delete",
678
+ tone: "critical",
679
+ icon: import_icons4.TrashIcon,
680
+ loading: deleting,
681
+ disabled: deleting,
682
+ onClick: confirmDelete
683
+ }
684
+ )
685
+ ] }),
686
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Box, { padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Stack, { space: 3, children: [
687
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { align: "center", gap: 2, children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons4.WarningOutlineIcon, {}),
689
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Text, { size: 2, weight: "semibold", children: pendingDelete.name })
690
+ ] }),
691
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_ui5.Text, { size: 1, muted: true, children: "This removes the deploy target from the dataset. The Vercel deploy hook itself is not affected." })
692
+ ] }) })
693
+ }
694
+ )
695
+ ] });
696
+ }
697
+
698
+ // src/schema/vercelDeploy.ts
699
+ var import_sanity3 = require("sanity");
700
+ var import_icons5 = require("@sanity/icons");
701
+ var vercelDeploySchema = (0, import_sanity3.defineType)({
702
+ name: "vercel_deploy",
703
+ title: "Deploy Target",
704
+ type: "document",
705
+ icon: import_icons5.RocketIcon,
706
+ fields: [
707
+ (0, import_sanity3.defineField)({
708
+ name: "name",
709
+ title: "Name",
710
+ type: "string",
711
+ description: 'Display label shown in the Deploy tool (e.g. "Production", "Staging")',
712
+ validation: (Rule) => Rule.required()
713
+ }),
714
+ (0, import_sanity3.defineField)({
715
+ name: "url",
716
+ title: "Deploy Hook URL",
717
+ type: "url",
718
+ description: "From Vercel \u2192 Project Settings \u2192 Git \u2192 Deploy Hooks",
719
+ validation: (Rule) => Rule.required().uri({ scheme: ["https"] }).custom((url) => {
720
+ if (typeof url !== "string") return true;
721
+ if (!url.includes("api.vercel.com/v1/integrations/deploy/")) {
722
+ return "Must be a Vercel deploy hook URL (api.vercel.com/v1/integrations/deploy/\u2026)";
723
+ }
724
+ return true;
725
+ })
726
+ }),
727
+ (0, import_sanity3.defineField)({
728
+ name: "teamId",
729
+ title: "Vercel Team ID",
730
+ type: "string",
731
+ description: "Required for team-owned projects \u2014 find it in Vercel Team Settings"
732
+ }),
733
+ (0, import_sanity3.defineField)({
734
+ name: "disableDeleteAction",
735
+ title: "Prevent deletion",
736
+ type: "boolean",
737
+ description: "Lock this target so it cannot be deleted from the Studio",
738
+ initialValue: false
739
+ })
740
+ ],
741
+ preview: {
742
+ select: { title: "name", subtitle: "url" }
743
+ }
744
+ });
745
+
746
+ // src/index.ts
747
+ var vercelDeploy = (0, import_sanity4.definePlugin)((options) => {
748
+ const config = options ?? {};
749
+ return {
750
+ name: "deploy-vercel-from-sanity",
751
+ schema: {
752
+ types: [vercelDeploySchema]
753
+ },
754
+ tools: [
755
+ {
756
+ name: config.name ?? "vercel-deploy",
757
+ title: config.title ?? "Deploy",
758
+ icon: config.icon ?? import_icons6.RocketIcon,
759
+ component: DeployTool
760
+ }
761
+ ]
762
+ };
763
+ });
764
+ // Annotate the CommonJS export names for ESM import in node:
765
+ 0 && (module.exports = {
766
+ vercelDeploy,
767
+ vercelDeploySchema
768
+ });