@angeloashmore/prismic-cli-poc 0.0.0-canary.1143872

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