prismic 0.0.0-pr.28.59bf330

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.
Files changed (158) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +69 -0
  3. package/dist/builders-hKD4IrLX-DsO7BUQw.mjs +97 -0
  4. package/dist/dist-B11B2hHn.mjs +1 -0
  5. package/dist/dist-DT8CtumB.mjs +1 -0
  6. package/dist/framework-CfjEoVk0.mjs +17 -0
  7. package/dist/index.mjs +2537 -0
  8. package/dist/nextjs-9z7YrSnS.mjs +312 -0
  9. package/dist/nuxt-KoJ61G2q.mjs +59 -0
  10. package/dist/sveltekit-DjXKCG78.mjs +226 -0
  11. package/package.json +58 -0
  12. package/src/codegen-types.ts +82 -0
  13. package/src/codegen.ts +45 -0
  14. package/src/custom-type-add-field-boolean.ts +185 -0
  15. package/src/custom-type-add-field-color.ts +168 -0
  16. package/src/custom-type-add-field-date.ts +171 -0
  17. package/src/custom-type-add-field-embed.ts +168 -0
  18. package/src/custom-type-add-field-geo-point.ts +165 -0
  19. package/src/custom-type-add-field-group.ts +142 -0
  20. package/src/custom-type-add-field-image.ts +168 -0
  21. package/src/custom-type-add-field-key-text.ts +168 -0
  22. package/src/custom-type-add-field-link.ts +191 -0
  23. package/src/custom-type-add-field-number.ts +200 -0
  24. package/src/custom-type-add-field-rich-text.ts +192 -0
  25. package/src/custom-type-add-field-select.ts +174 -0
  26. package/src/custom-type-add-field-timestamp.ts +171 -0
  27. package/src/custom-type-add-field-uid.ts +151 -0
  28. package/src/custom-type-add-field.ts +116 -0
  29. package/src/custom-type-connect-slice.ts +178 -0
  30. package/src/custom-type-create.ts +98 -0
  31. package/src/custom-type-disconnect-slice.ts +134 -0
  32. package/src/custom-type-list.ts +110 -0
  33. package/src/custom-type-remove-field.ts +135 -0
  34. package/src/custom-type-remove.ts +103 -0
  35. package/src/custom-type-set-name.ts +102 -0
  36. package/src/custom-type-view.ts +118 -0
  37. package/src/custom-type.ts +85 -0
  38. package/src/docs-fetch.ts +146 -0
  39. package/src/docs-list.ts +131 -0
  40. package/src/docs.ts +54 -0
  41. package/src/env.d.ts +12 -0
  42. package/src/framework/index.ts +399 -0
  43. package/src/framework/nextjs.templates.ts +426 -0
  44. package/src/framework/nextjs.ts +216 -0
  45. package/src/framework/nuxt.templates.ts +74 -0
  46. package/src/framework/nuxt.ts +250 -0
  47. package/src/framework/sveltekit.templates.ts +278 -0
  48. package/src/framework/sveltekit.ts +241 -0
  49. package/src/index.ts +155 -0
  50. package/src/init.ts +173 -0
  51. package/src/lib/auth.ts +200 -0
  52. package/src/lib/browser.ts +11 -0
  53. package/src/lib/config.ts +111 -0
  54. package/src/lib/custom-types-api.ts +385 -0
  55. package/src/lib/field-path.ts +81 -0
  56. package/src/lib/file.ts +49 -0
  57. package/src/lib/json.ts +3 -0
  58. package/src/lib/packageJson.ts +35 -0
  59. package/src/lib/profile.ts +39 -0
  60. package/src/lib/request.ts +116 -0
  61. package/src/lib/segment.ts +145 -0
  62. package/src/lib/sentry.ts +63 -0
  63. package/src/lib/string.ts +10 -0
  64. package/src/lib/url.ts +31 -0
  65. package/src/locale-add.ts +116 -0
  66. package/src/locale-list.ts +107 -0
  67. package/src/locale-remove.ts +88 -0
  68. package/src/locale-set-default.ts +131 -0
  69. package/src/locale.ts +60 -0
  70. package/src/login.ts +45 -0
  71. package/src/logout.ts +36 -0
  72. package/src/page-type-add-field-boolean.ts +179 -0
  73. package/src/page-type-add-field-color.ts +165 -0
  74. package/src/page-type-add-field-date.ts +168 -0
  75. package/src/page-type-add-field-embed.ts +165 -0
  76. package/src/page-type-add-field-geo-point.ts +162 -0
  77. package/src/page-type-add-field-group.ts +139 -0
  78. package/src/page-type-add-field-image.ts +165 -0
  79. package/src/page-type-add-field-key-text.ts +165 -0
  80. package/src/page-type-add-field-link.ts +188 -0
  81. package/src/page-type-add-field-number.ts +197 -0
  82. package/src/page-type-add-field-rich-text.ts +189 -0
  83. package/src/page-type-add-field-select.ts +171 -0
  84. package/src/page-type-add-field-timestamp.ts +168 -0
  85. package/src/page-type-add-field-uid.ts +148 -0
  86. package/src/page-type-add-field.ts +116 -0
  87. package/src/page-type-connect-slice.ts +178 -0
  88. package/src/page-type-create.ts +128 -0
  89. package/src/page-type-disconnect-slice.ts +134 -0
  90. package/src/page-type-list.ts +109 -0
  91. package/src/page-type-remove-field.ts +135 -0
  92. package/src/page-type-remove.ts +103 -0
  93. package/src/page-type-set-name.ts +102 -0
  94. package/src/page-type-set-repeatable.ts +111 -0
  95. package/src/page-type-view.ts +118 -0
  96. package/src/page-type.ts +90 -0
  97. package/src/preview-add.ts +126 -0
  98. package/src/preview-get-simulator.ts +104 -0
  99. package/src/preview-list.ts +106 -0
  100. package/src/preview-remove-simulator.ts +80 -0
  101. package/src/preview-remove.ts +109 -0
  102. package/src/preview-set-name.ts +137 -0
  103. package/src/preview-set-simulator.ts +116 -0
  104. package/src/preview.ts +75 -0
  105. package/src/pull.ts +236 -0
  106. package/src/push.ts +409 -0
  107. package/src/repo-create.ts +175 -0
  108. package/src/repo-get-access.ts +86 -0
  109. package/src/repo-list.ts +100 -0
  110. package/src/repo-set-access.ts +100 -0
  111. package/src/repo-set-name.ts +102 -0
  112. package/src/repo-view.ts +113 -0
  113. package/src/repo.ts +70 -0
  114. package/src/slice-add-field-boolean.ts +219 -0
  115. package/src/slice-add-field-color.ts +205 -0
  116. package/src/slice-add-field-date.ts +205 -0
  117. package/src/slice-add-field-embed.ts +205 -0
  118. package/src/slice-add-field-geo-point.ts +202 -0
  119. package/src/slice-add-field-group.ts +170 -0
  120. package/src/slice-add-field-image.ts +202 -0
  121. package/src/slice-add-field-key-text.ts +205 -0
  122. package/src/slice-add-field-link.ts +224 -0
  123. package/src/slice-add-field-number.ts +205 -0
  124. package/src/slice-add-field-rich-text.ts +229 -0
  125. package/src/slice-add-field-select.ts +211 -0
  126. package/src/slice-add-field-timestamp.ts +205 -0
  127. package/src/slice-add-field.ts +111 -0
  128. package/src/slice-add-variation.ts +142 -0
  129. package/src/slice-create.ts +164 -0
  130. package/src/slice-list-variations.ts +71 -0
  131. package/src/slice-list.ts +60 -0
  132. package/src/slice-remove-field.ts +125 -0
  133. package/src/slice-remove-variation.ts +113 -0
  134. package/src/slice-remove.ts +92 -0
  135. package/src/slice-rename.ts +104 -0
  136. package/src/slice-set-screenshot.ts +239 -0
  137. package/src/slice-view.ts +83 -0
  138. package/src/slice.ts +95 -0
  139. package/src/status.ts +834 -0
  140. package/src/sync.ts +259 -0
  141. package/src/token-create.ts +203 -0
  142. package/src/token-delete.ts +182 -0
  143. package/src/token-list.ts +223 -0
  144. package/src/token-set-name.ts +193 -0
  145. package/src/token.ts +60 -0
  146. package/src/webhook-add-header.ts +118 -0
  147. package/src/webhook-create.ts +152 -0
  148. package/src/webhook-disable.ts +109 -0
  149. package/src/webhook-enable.ts +132 -0
  150. package/src/webhook-list.ts +93 -0
  151. package/src/webhook-remove-header.ts +117 -0
  152. package/src/webhook-remove.ts +106 -0
  153. package/src/webhook-set-triggers.ts +148 -0
  154. package/src/webhook-status.ts +90 -0
  155. package/src/webhook-test.ts +106 -0
  156. package/src/webhook-view.ts +147 -0
  157. package/src/webhook.ts +95 -0
  158. package/src/whoami.ts +62 -0
package/src/status.ts ADDED
@@ -0,0 +1,834 @@
1
+ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { parseArgs } from "node:util";
5
+ import * as v from "valibot";
6
+
7
+ import { isAuthenticated } from "./lib/auth";
8
+ import { safeGetRepositoryFromConfig } from "./lib/config";
9
+ import { fetchRemoteCustomTypes, fetchRemoteSlices } from "./lib/custom-types-api";
10
+ import { exists } from "./lib/file";
11
+ import {
12
+ type FrameworkAdapter,
13
+ getClientSetupAnchor,
14
+ getDocsPath,
15
+ getPreviewSetupAnchor,
16
+ getWriteComponentsAnchor,
17
+ requireFramework,
18
+ } from "./framework";
19
+ import { request } from "./lib/request";
20
+ import { getRepoUrl } from "./lib/url";
21
+ import { getWebhooks } from "./webhook-view";
22
+
23
+ const HELP = `
24
+ Show the status of the current Prismic project.
25
+
26
+ Each section with incomplete items includes "Next steps:" with actionable
27
+ instructions.
28
+
29
+ By default, this command reads the repository from prismic.config.json at the
30
+ project root.
31
+
32
+ USAGE
33
+ prismic status [flags]
34
+
35
+ FLAGS
36
+ -r, --repo string Repository domain
37
+ -h, --help Show help for command
38
+
39
+ LEARN MORE
40
+ Use \`prismic <command> --help\` for more information about a command.
41
+ `.trim();
42
+
43
+ // Symbols for checkboxes
44
+ const CHECK = "\u2713";
45
+ const CIRCLE = "\u25CB";
46
+
47
+ type StatusItem = {
48
+ done: boolean;
49
+ label: string;
50
+ hint?: string;
51
+ };
52
+
53
+ type NextStep = {
54
+ action: string;
55
+ };
56
+
57
+ type StatusSection = {
58
+ title: string;
59
+ items: StatusItem[];
60
+ nextSteps?: NextStep[];
61
+ };
62
+
63
+ function getDocsRef(docsPath: string, anchor?: string): string {
64
+ if (!docsPath) return "";
65
+ const fullPath = anchor ? `${docsPath}${anchor}` : docsPath;
66
+ return `\`prismic docs fetch ${fullPath}\``;
67
+ }
68
+
69
+ // Next-step builder functions
70
+
71
+ function buildSetupNextSteps(items: StatusItem[], framework: FrameworkAdapter): NextStep[] {
72
+ const nextSteps: NextStep[] = [];
73
+ const docsPath = getDocsPath(framework.id);
74
+
75
+ // Missing dependencies
76
+ const missingDeps = items.filter((i) => !i.done && i.hint === "not installed");
77
+ if (missingDeps.length > 0) {
78
+ const depsList = missingDeps.map((d) => d.label).join(" ");
79
+ nextSteps.push({
80
+ action: `Install dependencies: Run \`npm install ${depsList}\``,
81
+ });
82
+ }
83
+
84
+ // Missing client file
85
+ const missingClientFile = items.find((i) => !i.done && i.hint?.includes("client"));
86
+ if (missingClientFile) {
87
+ const docsRef = getDocsRef(docsPath, getClientSetupAnchor(framework.id));
88
+ nextSteps.push({
89
+ action: `Create Prismic client file: Run ${docsRef} and create the file as shown`,
90
+ });
91
+ }
92
+
93
+ return nextSteps;
94
+ }
95
+
96
+ function buildTypesNextSteps(statuses: TypeWithStatus[]): NextStep[] {
97
+ const nextSteps: NextStep[] = [];
98
+
99
+ const hasToPush = statuses.some((t) => t.status === "to_push");
100
+ const hasToPull = statuses.some((t) => t.status === "to_pull");
101
+
102
+ if (hasToPush) {
103
+ nextSteps.push({
104
+ action: "Push local models to Prismic: Run `prismic push`",
105
+ });
106
+ }
107
+
108
+ if (hasToPull) {
109
+ nextSteps.push({
110
+ action: "Pull remote models from Prismic: Run `prismic pull`",
111
+ });
112
+ }
113
+
114
+ return nextSteps;
115
+ }
116
+
117
+ function buildSlicesNextSteps(
118
+ statuses: TypeWithStatus[],
119
+ missingComponents: string[],
120
+ slicesReadyToConnect: string[],
121
+ framework: FrameworkAdapter,
122
+ ): NextStep[] {
123
+ const nextSteps: NextStep[] = [];
124
+ const docsPath = getDocsPath(framework.id);
125
+
126
+ if (missingComponents.length > 0) {
127
+ const docsRef = getDocsRef(docsPath, getWriteComponentsAnchor(framework.id));
128
+ nextSteps.push({
129
+ action: `Implement slice components: Run ${docsRef} and create each component file`,
130
+ });
131
+ }
132
+
133
+ const hasToPull = statuses.some((t) => t.status === "to_pull");
134
+ const hasToPush = statuses.some((t) => t.status === "to_push");
135
+
136
+ if (hasToPull) {
137
+ nextSteps.push({
138
+ action: "Pull remote models from Prismic: Run `prismic pull`",
139
+ });
140
+ }
141
+
142
+ // Slices should be connected to page types before pushing
143
+ if (slicesReadyToConnect.length > 0) {
144
+ const sorted = [...slicesReadyToConnect].sort();
145
+ const sliceName = sorted[0];
146
+ nextSteps.push({
147
+ action: `Connect slice to page type: Run \`prismic page-type connect-slice <type-id> ${sliceName}\``,
148
+ });
149
+ }
150
+
151
+ if (hasToPush) {
152
+ nextSteps.push({
153
+ action: "Push local models to Prismic: Run `prismic push`",
154
+ });
155
+ }
156
+
157
+ return nextSteps;
158
+ }
159
+
160
+ function buildPreviewNextSteps(items: StatusItem[], framework: FrameworkAdapter): NextStep[] {
161
+ const nextSteps: NextStep[] = [];
162
+ const docsPath = getDocsPath(framework.id);
163
+
164
+ // Check for missing /slice-simulator route
165
+ const sliceSimRoute = items.find((i) => i.label === "/slice-simulator route" && !i.done);
166
+ if (sliceSimRoute) {
167
+ const docsRef = getDocsRef(docsPath, "#set-up-live-previewing");
168
+ nextSteps.push({
169
+ action: `Create /slice-simulator route: Run ${docsRef} and create the route file as shown`,
170
+ });
171
+ }
172
+
173
+ // Check for missing simulator URL config
174
+ const simulatorUrl = items.find((i) => i.label === "Slice simulator URL" && !i.done);
175
+ if (simulatorUrl) {
176
+ nextSteps.push({
177
+ action: "Configure slice simulator URL: Run `prismic preview set-simulator`",
178
+ });
179
+ }
180
+
181
+ // Check for missing preview endpoints (combine /api/preview and /api/exit-preview)
182
+ const apiPreview = items.find((i) => i.label === "/api/preview endpoint" && !i.done);
183
+ const exitPreview = items.find((i) => i.label === "/api/exit-preview endpoint" && !i.done);
184
+ if (apiPreview || exitPreview) {
185
+ const docsRef = getDocsRef(docsPath, getPreviewSetupAnchor(framework.id));
186
+ nextSteps.push({
187
+ action: `Create preview endpoints: Run ${docsRef} and create the endpoint files as shown`,
188
+ });
189
+ }
190
+
191
+ // Check for missing preview environment
192
+ const previewEnv = items.find((i) => i.label === "Preview environment" && !i.done);
193
+ if (previewEnv) {
194
+ nextSteps.push({
195
+ action: "Add preview environment: Run `prismic preview add`",
196
+ });
197
+ }
198
+
199
+ return nextSteps;
200
+ }
201
+
202
+ function buildDeploymentNextSteps(items: StatusItem[], framework: FrameworkAdapter): NextStep[] {
203
+ const nextSteps: NextStep[] = [];
204
+ const docsPath = getDocsPath(framework.id);
205
+
206
+ // Check for missing /api/revalidate endpoint
207
+ const revalidateEndpoint = items.find((i) => i.label === "/api/revalidate endpoint" && !i.done);
208
+ if (revalidateEndpoint) {
209
+ const docsRef = getDocsRef(docsPath, "#handle-content-changes");
210
+ nextSteps.push({
211
+ action: `Create /api/revalidate endpoint: Run ${docsRef} and create the endpoint as shown`,
212
+ });
213
+ }
214
+
215
+ // Check for missing revalidation webhook
216
+ const webhook = items.find((i) => i.label === "Revalidation webhook" && !i.done);
217
+ if (webhook) {
218
+ nextSteps.push({
219
+ action: "Create revalidation webhook: Run `prismic webhook create`",
220
+ });
221
+ }
222
+
223
+ return nextSteps;
224
+ }
225
+
226
+ export async function status(): Promise<void> {
227
+ const {
228
+ values: { help, repo = await safeGetRepositoryFromConfig() },
229
+ } = parseArgs({
230
+ args: process.argv.slice(3), // skip: node, script, "status"
231
+ options: {
232
+ repo: { type: "string", short: "r" },
233
+ help: { type: "boolean", short: "h" },
234
+ },
235
+ allowPositionals: false,
236
+ });
237
+
238
+ if (help) {
239
+ console.info(HELP);
240
+ return;
241
+ }
242
+
243
+ if (!repo) {
244
+ console.error("Missing prismic.config.json or --repo option");
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+
249
+ const authenticated = await isAuthenticated();
250
+ if (!authenticated) {
251
+ console.error("Not logged in. Run `prismic login` first.");
252
+ process.exitCode = 1;
253
+ return;
254
+ }
255
+
256
+ // Get framework adapter for reading local models
257
+ const framework = await requireFramework();
258
+ if (!framework) return;
259
+
260
+ const projectRoot = await framework.getProjectRoot();
261
+
262
+ // Gather all status data in parallel
263
+ const [
264
+ repoInfoResult,
265
+ previewsResult,
266
+ webhooksResult,
267
+ localCustomTypeResults,
268
+ remoteTypesResult,
269
+ localSliceResults,
270
+ remoteSlicesResult,
271
+ installedDeps,
272
+ ] = await Promise.all([
273
+ fetchRepositoryInfo(repo),
274
+ fetchPreviews(repo),
275
+ getWebhooks(repo),
276
+ framework.getCustomTypes(),
277
+ fetchRemoteCustomTypes(repo),
278
+ framework.getSlices(),
279
+ fetchRemoteSlices(repo),
280
+ getInstalledDependencies(projectRoot),
281
+ ]);
282
+
283
+ const localTypesResult = {
284
+ ok: true as const,
285
+ value: localCustomTypeResults.map((ct) => ct.model as unknown as CustomType),
286
+ };
287
+ const localSlicesResult = {
288
+ ok: true as const,
289
+ value: localSliceResults.map((s) => s.model as unknown as SharedSlice),
290
+ };
291
+
292
+ // Print repository header
293
+ const repoUrl = await getRepoUrl(repo);
294
+ console.info(`Repository: ${repo}`);
295
+ console.info(`URL: ${repoUrl.href}`);
296
+ console.info("");
297
+
298
+ const sections: StatusSection[] = [];
299
+
300
+ // Setup section
301
+ const setupSection = await buildSetupSection(framework, installedDeps);
302
+ setupSection.nextSteps = buildSetupNextSteps(setupSection.items, framework);
303
+ sections.push(setupSection);
304
+
305
+ // Types sections (Page Types and Custom Types)
306
+ if (localTypesResult.ok && remoteTypesResult.ok) {
307
+ const { pageTypes, customTypes, pageTypeStatuses, customTypeStatuses } = buildTypeSections(
308
+ localTypesResult.value,
309
+ remoteTypesResult.value,
310
+ );
311
+ pageTypes.nextSteps = buildTypesNextSteps(pageTypeStatuses);
312
+ customTypes.nextSteps = buildTypesNextSteps(customTypeStatuses);
313
+ sections.push(pageTypes);
314
+ sections.push(customTypes);
315
+ }
316
+
317
+ // Slices section
318
+ if (localSlicesResult.ok && remoteSlicesResult.ok) {
319
+ const {
320
+ section: slicesSection,
321
+ statuses,
322
+ missingComponents,
323
+ slicesReadyToConnect,
324
+ } = await buildSlicesSection(
325
+ localSlicesResult.value,
326
+ remoteSlicesResult.value,
327
+ framework,
328
+ localTypesResult.ok ? localTypesResult.value : [],
329
+ );
330
+ slicesSection.nextSteps = buildSlicesNextSteps(
331
+ statuses,
332
+ missingComponents,
333
+ slicesReadyToConnect,
334
+ framework,
335
+ );
336
+ sections.push(slicesSection);
337
+ }
338
+
339
+ // Preview section
340
+ const previewSection = await buildPreviewSection(
341
+ framework,
342
+ previewsResult.ok ? previewsResult.value : undefined,
343
+ repoInfoResult.ok ? repoInfoResult.value.simulator_url : undefined,
344
+ );
345
+ previewSection.nextSteps = buildPreviewNextSteps(previewSection.items, framework);
346
+ sections.push(previewSection);
347
+
348
+ // Deployment section (Next.js only)
349
+ if (framework.id === "next") {
350
+ const deploymentSection = await buildDeploymentSection(
351
+ framework,
352
+ webhooksResult.ok ? webhooksResult.value : [],
353
+ );
354
+ deploymentSection.nextSteps = buildDeploymentNextSteps(deploymentSection.items, framework);
355
+ sections.push(deploymentSection);
356
+ }
357
+
358
+ // Print all sections
359
+ for (const section of sections) {
360
+ printSection(section);
361
+ }
362
+ }
363
+
364
+ function printSection(section: StatusSection): void {
365
+ const remaining = section.items.filter((item) => !item.done).length;
366
+ const header = remaining > 0 ? `${section.title} (${remaining} remaining)` : section.title;
367
+ console.info(header);
368
+
369
+ // Group completed items together
370
+ const completed = section.items.filter((item) => item.done);
371
+ const incomplete = section.items.filter((item) => !item.done);
372
+
373
+ // Print completed items on one line if there are multiple
374
+ if (completed.length > 0) {
375
+ if (completed.length === 1) {
376
+ const item = completed[0];
377
+ const hint = item.hint ? ` \u2014 ${item.hint}` : "";
378
+ console.info(` ${CHECK} ${item.label}${hint}`);
379
+ } else {
380
+ const labels = completed.map((item) => item.label).join(", ");
381
+ const allSameHint = completed.every((item) => item.hint === completed[0].hint);
382
+ const hint = allSameHint && completed[0].hint ? ` \u2014 ${completed[0].hint}` : "";
383
+ console.info(` ${CHECK} ${labels}${hint}`);
384
+ }
385
+ }
386
+
387
+ // Print incomplete items individually
388
+ for (const item of incomplete) {
389
+ const hint = item.hint ? ` \u2014 ${item.hint}` : "";
390
+ console.info(` ${CIRCLE} ${item.label}${hint}`);
391
+ }
392
+
393
+ // Print next steps if there are any
394
+ if (section.nextSteps && section.nextSteps.length > 0) {
395
+ console.info("");
396
+ console.info(" Next steps:");
397
+ for (const step of section.nextSteps) {
398
+ console.info(` - ${step.action}`);
399
+ }
400
+ }
401
+
402
+ console.info("");
403
+ }
404
+
405
+ // Repository Info (from /core/repository)
406
+ const RepositoryInfoSchema = v.object({
407
+ simulator_url: v.optional(v.string()),
408
+ });
409
+ type RepositoryInfo = v.InferOutput<typeof RepositoryInfoSchema>;
410
+
411
+ async function fetchRepositoryInfo(
412
+ repo: string,
413
+ ): Promise<{ ok: true; value: RepositoryInfo } | { ok: false }> {
414
+ const url = new URL("/core/repository", await getRepoUrl(repo));
415
+ const result = await request(url, { schema: RepositoryInfoSchema });
416
+ if (result.ok) {
417
+ return { ok: true, value: result.value };
418
+ }
419
+ return { ok: false };
420
+ }
421
+
422
+ // Previews
423
+ const PreviewSchema = v.object({
424
+ id: v.string(),
425
+ label: v.string(),
426
+ url: v.string(),
427
+ });
428
+ const PreviewsResponseSchema = v.object({
429
+ results: v.array(PreviewSchema),
430
+ });
431
+ type Preview = v.InferOutput<typeof PreviewSchema>;
432
+
433
+ async function fetchPreviews(
434
+ repo: string,
435
+ ): Promise<{ ok: true; value: Preview[] } | { ok: false }> {
436
+ const url = new URL("/core/repository/preview_configs", await getRepoUrl(repo));
437
+ const result = await request(url, { schema: PreviewsResponseSchema });
438
+ if (result.ok) {
439
+ return { ok: true, value: result.value.results };
440
+ }
441
+ return { ok: false };
442
+ }
443
+
444
+ // Dependencies
445
+ const PackageJsonSchema = v.object({
446
+ dependencies: v.optional(v.record(v.string(), v.string())),
447
+ devDependencies: v.optional(v.record(v.string(), v.string())),
448
+ });
449
+
450
+ async function getInstalledDependencies(projectRoot: URL): Promise<Set<string>> {
451
+ const packageJsonPath = new URL("package.json", projectRoot);
452
+ try {
453
+ const contents = await readFile(packageJsonPath, "utf8");
454
+ const { dependencies = {}, devDependencies = {} } = v.parse(
455
+ PackageJsonSchema,
456
+ JSON.parse(contents),
457
+ );
458
+ return new Set([...Object.keys(dependencies), ...Object.keys(devDependencies)]);
459
+ } catch {
460
+ return new Set();
461
+ }
462
+ }
463
+
464
+ // Setup Section
465
+ async function buildSetupSection(
466
+ framework: FrameworkAdapter,
467
+ installedDeps: Set<string>,
468
+ ): Promise<StatusSection> {
469
+ const items: StatusItem[] = [];
470
+
471
+ // Check required dependencies
472
+ const requiredDeps = Object.keys(await framework.getDependencies());
473
+ for (const dep of requiredDeps) {
474
+ items.push({
475
+ done: installedDeps.has(dep),
476
+ label: dep,
477
+ hint: installedDeps.has(dep) ? "installed" : "not installed",
478
+ });
479
+ }
480
+
481
+ // Check client file
482
+ const projectRoot = await framework.getProjectRoot();
483
+ const clientFilePath = await framework.getClientFilePath();
484
+ if (clientFilePath) {
485
+ const clientFileExists = await exists(new URL(clientFilePath, projectRoot));
486
+ items.push({
487
+ done: clientFileExists,
488
+ label: clientFilePath,
489
+ hint: clientFileExists ? undefined : "create Prismic client file",
490
+ });
491
+ } else if (framework.id === "nuxt") {
492
+ // Check nuxt.config.ts for prismic config
493
+ const nuxtConfigExists = await checkNuxtPrismicConfig(projectRoot);
494
+ items.push({
495
+ done: nuxtConfigExists,
496
+ label: "nuxt.config.ts",
497
+ hint: nuxtConfigExists ? "prismic configured" : "add @nuxtjs/prismic to modules",
498
+ });
499
+ }
500
+
501
+ return { title: "Setup", items };
502
+ }
503
+
504
+ async function checkNuxtPrismicConfig(projectRoot: URL): Promise<boolean> {
505
+ const configPath = new URL("nuxt.config.ts", projectRoot);
506
+ try {
507
+ const contents = await readFile(configPath, "utf8");
508
+ return contents.includes("@nuxtjs/prismic") || contents.includes("prismic:");
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+
514
+ // Types Sections
515
+ type TypeStatus = "in_sync" | "to_push" | "to_pull";
516
+
517
+ type TypeWithStatus = {
518
+ id: string;
519
+ label: string;
520
+ status: TypeStatus;
521
+ };
522
+
523
+ function computeTypeStatus<T extends { id: string }>(local: T[], remote: T[]): TypeWithStatus[] {
524
+ const localById = new Map(local.map((item) => [item.id, item]));
525
+ const remoteById = new Map(remote.map((item) => [item.id, item]));
526
+ const result: TypeWithStatus[] = [];
527
+
528
+ // Check local items
529
+ for (const localItem of local) {
530
+ const label = (localItem as { label?: string }).label || localItem.id;
531
+ const remoteItem = remoteById.get(localItem.id);
532
+ if (!remoteItem) {
533
+ result.push({ id: localItem.id, label, status: "to_push" });
534
+ } else if (JSON.stringify(localItem) !== JSON.stringify(remoteItem)) {
535
+ result.push({ id: localItem.id, label, status: "to_push" });
536
+ } else {
537
+ result.push({ id: localItem.id, label, status: "in_sync" });
538
+ }
539
+ }
540
+
541
+ // Check remote items not in local
542
+ for (const remoteItem of remote) {
543
+ if (!localById.has(remoteItem.id)) {
544
+ const label = (remoteItem as { label?: string }).label || remoteItem.id;
545
+ result.push({ id: remoteItem.id, label, status: "to_pull" });
546
+ }
547
+ }
548
+
549
+ return result;
550
+ }
551
+
552
+ function buildTypeSections(
553
+ localTypes: CustomType[],
554
+ remoteTypes: CustomType[],
555
+ ): {
556
+ pageTypes: StatusSection;
557
+ customTypes: StatusSection;
558
+ pageTypeStatuses: TypeWithStatus[];
559
+ customTypeStatuses: TypeWithStatus[];
560
+ } {
561
+ const typeStatuses = computeTypeStatus(localTypes, remoteTypes);
562
+
563
+ // Separate by format
564
+ const pageTypeStatuses = typeStatuses.filter((t) => {
565
+ const localType = localTypes.find((lt) => lt.id === t.id);
566
+ const remoteType = remoteTypes.find((rt) => rt.id === t.id);
567
+ const type = localType || remoteType;
568
+ return type && (type as { format?: string }).format === "page";
569
+ });
570
+
571
+ const customTypeStatuses = typeStatuses.filter((t) => {
572
+ const localType = localTypes.find((lt) => lt.id === t.id);
573
+ const remoteType = remoteTypes.find((rt) => rt.id === t.id);
574
+ const type = localType || remoteType;
575
+ return !type || (type as { format?: string }).format !== "page";
576
+ });
577
+
578
+ const pageTypeItems: StatusItem[] = pageTypeStatuses.map((t) => ({
579
+ done: t.status === "in_sync",
580
+ label: t.label,
581
+ hint: statusToHint(t.status),
582
+ }));
583
+
584
+ const customTypeItems: StatusItem[] = customTypeStatuses.map((t) => ({
585
+ done: t.status === "in_sync",
586
+ label: t.label,
587
+ hint: statusToHint(t.status),
588
+ }));
589
+
590
+ return {
591
+ pageTypes: { title: "Page Types", items: pageTypeItems },
592
+ customTypes: { title: "Custom Types", items: customTypeItems },
593
+ pageTypeStatuses,
594
+ customTypeStatuses,
595
+ };
596
+ }
597
+
598
+ function statusToHint(status: TypeStatus): string | undefined {
599
+ switch (status) {
600
+ case "in_sync":
601
+ return "in sync";
602
+ case "to_push":
603
+ return "to push";
604
+ case "to_pull":
605
+ return "to pull";
606
+ }
607
+ }
608
+
609
+ // Slices Section
610
+ function sliceHasFields(slice: SharedSlice): boolean {
611
+ for (const variation of slice.variations) {
612
+ const primaryFields = Object.keys(variation.primary ?? {});
613
+ const itemFields = Object.keys(variation.items ?? {});
614
+ if (primaryFields.length > 0 || itemFields.length > 0) {
615
+ return true;
616
+ }
617
+ }
618
+ return false;
619
+ }
620
+
621
+ function isSliceConnectedToAnyType(sliceId: string, localTypes: CustomType[]): boolean {
622
+ for (const type of localTypes) {
623
+ for (const tabFields of Object.values(type.json)) {
624
+ for (const field of Object.values(tabFields as Record<string, unknown>)) {
625
+ const typedField = field as {
626
+ type?: string;
627
+ config?: { choices?: Record<string, unknown> };
628
+ };
629
+ if (typedField.type === "Slices" && typedField.config?.choices?.[sliceId]) {
630
+ return true;
631
+ }
632
+ }
633
+ }
634
+ }
635
+ return false;
636
+ }
637
+
638
+ async function buildSlicesSection(
639
+ localSlices: SharedSlice[],
640
+ remoteSlices: SharedSlice[],
641
+ framework: FrameworkAdapter,
642
+ localTypes: CustomType[],
643
+ ): Promise<{
644
+ section: StatusSection;
645
+ statuses: TypeWithStatus[];
646
+ missingComponents: string[];
647
+ slicesReadyToConnect: string[];
648
+ }> {
649
+ const sliceStatuses = computeTypeStatus(localSlices, remoteSlices);
650
+ const items: StatusItem[] = [];
651
+ const missingComponents: string[] = [];
652
+ const slicesReadyToConnect: string[] = [];
653
+
654
+ const projectRoot = await framework.getProjectRoot();
655
+ const slicesDir = await framework.getSlicesDirectoryPath();
656
+ const extensions = framework.getSliceComponentExtensions();
657
+
658
+ for (const slice of sliceStatuses) {
659
+ const localSlice = localSlices.find((s) => s.id === slice.id);
660
+
661
+ // Track slices that have fields but aren't connected to any type
662
+ // These should be connected before pushing
663
+ if (localSlice) {
664
+ const hasFields = sliceHasFields(localSlice);
665
+ const isConnected = isSliceConnectedToAnyType(slice.id, localTypes);
666
+
667
+ if (hasFields && !isConnected) {
668
+ slicesReadyToConnect.push(slice.label);
669
+ }
670
+ }
671
+
672
+ // Check if component is implemented
673
+ const componentExists = await checkSliceComponent(projectRoot, slicesDir, slice.id, extensions);
674
+
675
+ if (slice.status === "in_sync" && componentExists) {
676
+ items.push({
677
+ done: true,
678
+ label: slice.label,
679
+ hint: "component implemented",
680
+ });
681
+ } else if (slice.status === "in_sync" && !componentExists) {
682
+ items.push({
683
+ done: false,
684
+ label: slice.label,
685
+ hint: "missing component",
686
+ });
687
+ missingComponents.push(slice.label);
688
+ } else {
689
+ items.push({
690
+ done: false,
691
+ label: slice.label,
692
+ hint: statusToHint(slice.status),
693
+ });
694
+ }
695
+ }
696
+
697
+ return {
698
+ section: { title: "Slices", items },
699
+ statuses: sliceStatuses,
700
+ missingComponents,
701
+ slicesReadyToConnect,
702
+ };
703
+ }
704
+
705
+ async function checkSliceComponent(
706
+ projectRoot: URL,
707
+ slicesDir: string,
708
+ sliceId: string,
709
+ extensions: string[],
710
+ ): Promise<boolean> {
711
+ // Convert slice ID to PascalCase for folder name
712
+ const sliceName = pascalCase(sliceId);
713
+
714
+ for (const ext of extensions) {
715
+ const componentPath = new URL(`${slicesDir}${sliceName}/index${ext}`, projectRoot);
716
+ if (await exists(componentPath)) {
717
+ return true;
718
+ }
719
+ }
720
+ return false;
721
+ }
722
+
723
+ function pascalCase(input: string): string {
724
+ return input.toLowerCase().replace(/(^|[-_\s]+)(.)?/g, (_, __, c) => c?.toUpperCase() ?? "");
725
+ }
726
+
727
+ // Preview Section
728
+ async function buildPreviewSection(
729
+ framework: FrameworkAdapter,
730
+ previews: Preview[] | undefined,
731
+ simulatorUrl: string | undefined,
732
+ ): Promise<StatusSection> {
733
+ const items: StatusItem[] = [];
734
+ const projectRoot = await framework.getProjectRoot();
735
+
736
+ // Check simulator URL configured
737
+ items.push({
738
+ done: Boolean(simulatorUrl),
739
+ label: "Slice simulator URL",
740
+ hint: simulatorUrl ? "configured" : "run `prismic preview set-simulator`",
741
+ });
742
+
743
+ // Check slice-simulator route
744
+ const sliceSimRoute = await framework.getRoutePath("/slice-simulator");
745
+ if (sliceSimRoute) {
746
+ const routeExists = await checkRouteExists(projectRoot, sliceSimRoute);
747
+ items.push({
748
+ done: routeExists,
749
+ label: "/slice-simulator route",
750
+ hint: routeExists ? undefined : "create route for Page Builder",
751
+ });
752
+ }
753
+
754
+ // Check preview environment
755
+ const hasPreviewEnv = previews && previews.length > 0;
756
+ items.push({
757
+ done: Boolean(hasPreviewEnv),
758
+ label: "Preview environment",
759
+ hint: hasPreviewEnv ? undefined : "run `prismic preview add`",
760
+ });
761
+
762
+ // Check /api/preview endpoint (skip for Nuxt - built-in)
763
+ if (framework.id !== "nuxt") {
764
+ const previewRoute = await framework.getRoutePath("/api/preview");
765
+ if (previewRoute) {
766
+ const routeExists = await checkRouteExists(projectRoot, previewRoute);
767
+ items.push({
768
+ done: routeExists,
769
+ label: "/api/preview endpoint",
770
+ hint: routeExists ? undefined : "create preview endpoint",
771
+ });
772
+ }
773
+ }
774
+
775
+ // Check /api/exit-preview endpoint (Next.js only)
776
+ if (framework.id === "next") {
777
+ const exitPreviewRoute = await framework.getRoutePath("/api/exit-preview");
778
+ if (exitPreviewRoute) {
779
+ const routeExists = await checkRouteExists(projectRoot, exitPreviewRoute);
780
+ items.push({
781
+ done: routeExists,
782
+ label: "/api/exit-preview endpoint",
783
+ hint: routeExists ? undefined : "create exit-preview endpoint",
784
+ });
785
+ }
786
+ }
787
+
788
+ return { title: "Preview", items };
789
+ }
790
+
791
+ async function checkRouteExists(
792
+ projectRoot: URL,
793
+ route: { path: string; extensions: string[] },
794
+ ): Promise<boolean> {
795
+ for (const ext of route.extensions) {
796
+ const fullPath = new URL(`${route.path}${ext}`, projectRoot);
797
+ if (await exists(fullPath)) {
798
+ return true;
799
+ }
800
+ }
801
+ return false;
802
+ }
803
+
804
+ // Deployment Section (Next.js only)
805
+ async function buildDeploymentSection(
806
+ framework: FrameworkAdapter,
807
+ webhooks: Array<{ config: { url: string; active: boolean } }>,
808
+ ): Promise<StatusSection> {
809
+ const items: StatusItem[] = [];
810
+ const projectRoot = await framework.getProjectRoot();
811
+
812
+ // Check /api/revalidate endpoint
813
+ const revalidateRoute = await framework.getRoutePath("/api/revalidate");
814
+ if (revalidateRoute) {
815
+ const routeExists = await checkRouteExists(projectRoot, revalidateRoute);
816
+ items.push({
817
+ done: routeExists,
818
+ label: "/api/revalidate endpoint",
819
+ hint: routeExists ? undefined : "create for ISR",
820
+ });
821
+ }
822
+
823
+ // Check revalidation webhook
824
+ const hasRevalidationWebhook = webhooks.some(
825
+ (w) => w.config.active && w.config.url.toLowerCase().includes("revalidate"),
826
+ );
827
+ items.push({
828
+ done: hasRevalidationWebhook,
829
+ label: "Revalidation webhook",
830
+ hint: hasRevalidationWebhook ? "configured" : "run `prismic webhook create`",
831
+ });
832
+
833
+ return { title: "Deployment", items };
834
+ }