@angeloashmore/prismic-cli-poc 0.0.0-canary.1d36cd8

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