@angeloashmore/prismic-cli-poc 0.0.0-pr.4.d609497 → 0.0.0-pr.5.f8fb51a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angeloashmore/prismic-cli-poc",
3
- "version": "0.0.0-pr.4.d609497",
3
+ "version": "0.0.0-pr.5.f8fb51a",
4
4
  "description": "A proof-of-concept developer CLI for Prismic.",
5
5
  "keywords": [
6
6
  "prismic",
package/src/docs.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ const HELP = `
4
+ Fetch and display documentation from Prismic's docs site.
5
+
6
+ USAGE
7
+ prismic docs <path> [flags]
8
+
9
+ ARGUMENTS
10
+ path Documentation path with optional anchor (e.g., "nextjs" or "nextjs#set-up-a-prismic-client")
11
+
12
+ FLAGS
13
+ -h, --help Show help for command
14
+
15
+ EXAMPLES
16
+ prismic docs nextjs
17
+ prismic docs nextjs#set-up-a-prismic-client
18
+
19
+ LEARN MORE
20
+ Visit https://prismic.io/docs for the full documentation.
21
+ `.trim();
22
+
23
+ function parsePathAndAnchor(input: string): { path: string; anchor?: string } {
24
+ const hashIndex = input.indexOf("#");
25
+ if (hashIndex === -1) {
26
+ return { path: input };
27
+ }
28
+ return {
29
+ path: input.slice(0, hashIndex),
30
+ anchor: input.slice(hashIndex + 1),
31
+ };
32
+ }
33
+
34
+ async function fetchMarkdown(
35
+ url: string,
36
+ ): Promise<{ ok: true; content: string } | { ok: false; error: string }> {
37
+ try {
38
+ const response = await fetch(url);
39
+ if (response.status === 404) {
40
+ return { ok: false, error: `Documentation not found: ${url}` };
41
+ }
42
+ if (!response.ok) {
43
+ return {
44
+ ok: false,
45
+ error: `Failed to fetch documentation: ${response.status}`,
46
+ };
47
+ }
48
+ const content = await response.text();
49
+ return { ok: true, content };
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return { ok: false, error: `Network error: ${message}` };
53
+ }
54
+ }
55
+
56
+ function anchorToHeadingPattern(anchor: string): RegExp {
57
+ // Convert kebab-case anchor to a pattern that matches the heading text
58
+ // Each hyphen/space becomes a flexible match for hyphens or spaces
59
+ const pattern = anchor
60
+ .split("-")
61
+ .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
62
+ .join("[\\s-]+");
63
+ return new RegExp(`^(#{1,6})\\s+${pattern}\\s*$`, "im");
64
+ }
65
+
66
+ function extractSection(
67
+ markdown: string,
68
+ anchor: string,
69
+ ): { ok: true; content: string } | { ok: false; error: string } {
70
+ const lines = markdown.split("\n");
71
+ const headingPattern = anchorToHeadingPattern(anchor);
72
+
73
+ let startIndex = -1;
74
+ let headingLevel = 0;
75
+
76
+ // Find the matching heading
77
+ for (let i = 0; i < lines.length; i++) {
78
+ const match = lines[i].match(headingPattern);
79
+ if (match) {
80
+ startIndex = i;
81
+ headingLevel = match[1].length;
82
+ break;
83
+ }
84
+ }
85
+
86
+ if (startIndex === -1) {
87
+ return { ok: false, error: `Anchor not found: #${anchor}` };
88
+ }
89
+
90
+ // Find the end of this section (next heading of equal or lower level number)
91
+ let endIndex = lines.length;
92
+ for (let i = startIndex + 1; i < lines.length; i++) {
93
+ const headingMatch = lines[i].match(/^(#{1,6})\s/);
94
+ if (headingMatch && headingMatch[1].length <= headingLevel) {
95
+ endIndex = i;
96
+ break;
97
+ }
98
+ }
99
+
100
+ const content = lines.slice(startIndex, endIndex).join("\n").trim();
101
+ return { ok: true, content };
102
+ }
103
+
104
+ export async function docs(): Promise<void> {
105
+ const {
106
+ positionals: [pathArg],
107
+ values: { help },
108
+ } = parseArgs({
109
+ args: process.argv.slice(3),
110
+ options: {
111
+ help: { type: "boolean", short: "h" },
112
+ },
113
+ allowPositionals: true,
114
+ });
115
+
116
+ if (help) {
117
+ console.info(HELP);
118
+ return;
119
+ }
120
+
121
+ if (!pathArg) {
122
+ console.info(HELP);
123
+ return;
124
+ }
125
+
126
+ const { path, anchor } = parsePathAndAnchor(pathArg);
127
+ const url = `https://prismic.io/docs/${path}.md`;
128
+
129
+ const fetchResult = await fetchMarkdown(url);
130
+ if (!fetchResult.ok) {
131
+ console.error(fetchResult.error);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+
136
+ let output = fetchResult.content;
137
+
138
+ if (anchor) {
139
+ const extractResult = extractSection(output, anchor);
140
+ if (!extractResult.ok) {
141
+ console.error(extractResult.error);
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ output = extractResult.content;
146
+ }
147
+
148
+ console.info(output);
149
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import packageJson from "../package.json" with { type: "json" };
6
6
  import { codegen } from "./codegen";
7
7
  import { customType } from "./custom-type";
8
+ import { docs } from "./docs";
8
9
  import { init } from "./init";
9
10
  import { locale } from "./locale";
10
11
  import { login } from "./login";
@@ -40,6 +41,7 @@ COMMANDS
40
41
  pull Pull types and slices from Prismic
41
42
  push Push types and slices to Prismic
42
43
  codegen Generate code from Prismic models
44
+ docs Fetch documentation from Prismic
43
45
  preview Manage preview configurations
44
46
  token Manage API tokens in a repository
45
47
  webhook Manage webhooks in a repository
@@ -107,6 +109,9 @@ if (version) {
107
109
  case "codegen":
108
110
  await codegen();
109
111
  break;
112
+ case "docs":
113
+ await docs();
114
+ break;
110
115
  case "preview":
111
116
  await preview();
112
117
  break;
package/src/status.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from "./lib/custom-types-api";
15
15
  import { exists } from "./lib/file";
16
16
  import {
17
+ type Framework,
17
18
  type FrameworkInfo,
18
19
  detectFrameworkInfo,
19
20
  getClientFilePath,
@@ -29,6 +30,9 @@ import { getWebhooks } from "./webhook-view";
29
30
  const HELP = `
30
31
  Show the status of the current Prismic project.
31
32
 
33
+ Includes a "Next:" step showing the most important action to take based on
34
+ project state.
35
+
32
36
  By default, this command reads the repository from prismic.config.json at the
33
37
  project root.
34
38
 
@@ -58,6 +62,172 @@ type StatusSection = {
58
62
  items: StatusItem[];
59
63
  };
60
64
 
65
+ type NextStep = {
66
+ message: string;
67
+ };
68
+
69
+ function getDocsPath(framework: Framework | undefined): string {
70
+ switch (framework) {
71
+ case "next":
72
+ return "nextjs/with-cli";
73
+ case "nuxt":
74
+ return "nuxt/with-cli";
75
+ case "sveltekit":
76
+ return "sveltekit/with-cli";
77
+ default:
78
+ return "";
79
+ }
80
+ }
81
+
82
+ function getDocsRef(docsPath: string, anchor?: string): string {
83
+ if (!docsPath) return "";
84
+ const fullPath = anchor ? `${docsPath}${anchor}` : docsPath;
85
+ return ` (run \`prismic docs ${fullPath}\`)`;
86
+ }
87
+
88
+ function getClientSetupAnchor(framework: Framework | undefined): string {
89
+ switch (framework) {
90
+ case "nuxt":
91
+ return "#configure-the-modules-prismic-client";
92
+ default:
93
+ return "#set-up-a-prismic-client";
94
+ }
95
+ }
96
+
97
+ function getPreviewSetupAnchor(framework: Framework | undefined): string {
98
+ switch (framework) {
99
+ case "next":
100
+ return "#set-up-previews-in-next-js";
101
+ case "sveltekit":
102
+ return "#set-up-previews-in-sveltekit";
103
+ default:
104
+ return "";
105
+ }
106
+ }
107
+
108
+ function getWriteComponentsAnchor(framework: Framework | undefined): string {
109
+ switch (framework) {
110
+ case "nuxt":
111
+ return "#write-vue-components";
112
+ case "sveltekit":
113
+ return "#write-svelte-components";
114
+ default:
115
+ return "#write-react-components";
116
+ }
117
+ }
118
+
119
+ function computeNextStep(
120
+ sections: StatusSection[],
121
+ frameworkInfo: FrameworkInfo,
122
+ typeStatuses: TypeWithStatus[],
123
+ sliceStatuses: TypeWithStatus[],
124
+ slicesWithMissingComponents: string[],
125
+ ): NextStep | undefined {
126
+ const docsPath = getDocsPath(frameworkInfo.framework);
127
+
128
+ // 1. Setup - missing dependencies
129
+ const setupSection = sections.find((s) => s.title === "Setup");
130
+ const missingDeps = setupSection?.items.filter((i) => !i.done && i.hint === "not installed");
131
+ if (missingDeps && missingDeps.length > 0) {
132
+ const depsList = missingDeps.map((d) => d.label).join(" ");
133
+ return { message: `Install Prismic packages with 'npm install ${depsList}'` };
134
+ }
135
+
136
+ // 2. Setup - missing client file
137
+ const missingClientFile = setupSection?.items.find((i) => !i.done && i.hint?.includes("client"));
138
+ if (missingClientFile) {
139
+ return { message: `Create a ${missingClientFile.label} file${getDocsRef(docsPath, getClientSetupAnchor(frameworkInfo.framework))}` };
140
+ }
141
+
142
+ // 3-7. Preview section (in order: local files, then remote config)
143
+ const previewSection = sections.find((s) => s.title === "Preview");
144
+ if (previewSection) {
145
+ // Local files first
146
+ const sliceSimRoute = previewSection.items.find(
147
+ (i) => i.label === "/slice-simulator route" && !i.done,
148
+ );
149
+ if (sliceSimRoute) {
150
+ return { message: `Create the /slice-simulator route${getDocsRef(docsPath, "#set-up-live-previewing")}` };
151
+ }
152
+
153
+ const apiPreview = previewSection.items.find(
154
+ (i) => i.label === "/api/preview endpoint" && !i.done,
155
+ );
156
+ if (apiPreview) {
157
+ return { message: `Create the /api/preview route${getDocsRef(docsPath, getPreviewSetupAnchor(frameworkInfo.framework))}` };
158
+ }
159
+
160
+ const exitPreview = previewSection.items.find(
161
+ (i) => i.label === "/api/exit-preview endpoint" && !i.done,
162
+ );
163
+ if (exitPreview) {
164
+ return { message: `Create the /api/exit-preview route${getDocsRef(docsPath, getPreviewSetupAnchor(frameworkInfo.framework))}` };
165
+ }
166
+
167
+ // Remote config
168
+ const simulatorUrl = previewSection.items.find(
169
+ (i) => i.label === "Slice simulator URL" && !i.done,
170
+ );
171
+ if (simulatorUrl) {
172
+ return { message: `Configure the slice simulator URL with 'prismic preview set-simulator'` };
173
+ }
174
+
175
+ const previewEnv = previewSection.items.find(
176
+ (i) => i.label === "Preview environment" && !i.done,
177
+ );
178
+ if (previewEnv) {
179
+ return { message: `Add a preview environment with 'prismic preview add'` };
180
+ }
181
+ }
182
+
183
+ // 8. Models to pull
184
+ const hasToPull =
185
+ typeStatuses.some((t) => t.status === "to_pull") ||
186
+ sliceStatuses.some((s) => s.status === "to_pull");
187
+ if (hasToPull) {
188
+ return { message: `Pull remote models with 'prismic pull'` };
189
+ }
190
+
191
+ // 9. Models to push
192
+ const hasToPush =
193
+ typeStatuses.some((t) => t.status === "to_push") ||
194
+ sliceStatuses.some((s) => s.status === "to_push");
195
+ if (hasToPush) {
196
+ return { message: `Push local models with 'prismic push'` };
197
+ }
198
+
199
+ // 10. Slice components to implement (first alphabetically)
200
+ if (slicesWithMissingComponents.length > 0) {
201
+ const sorted = [...slicesWithMissingComponents].sort();
202
+ const sliceName = sorted[0];
203
+ const slicesDir = getSlicesDirectory(frameworkInfo);
204
+ const ext = getSliceComponentExtensions(frameworkInfo.framework)[0];
205
+ const path = `${slicesDir}${sliceName}/index${ext}`;
206
+ return { message: `Implement the ${sliceName} slice component at ${path}${getDocsRef(docsPath, getWriteComponentsAnchor(frameworkInfo.framework))}` };
207
+ }
208
+
209
+ // 11-12. Deployment (Next.js only)
210
+ const deploymentSection = sections.find((s) => s.title === "Deployment");
211
+ if (deploymentSection) {
212
+ const revalidateEndpoint = deploymentSection.items.find(
213
+ (i) => i.label === "/api/revalidate endpoint" && !i.done,
214
+ );
215
+ if (revalidateEndpoint) {
216
+ return { message: `Create the /api/revalidate route for ISR${getDocsRef(docsPath, "#handle-content-changes")}` };
217
+ }
218
+
219
+ const webhook = deploymentSection.items.find(
220
+ (i) => i.label === "Revalidation webhook" && !i.done,
221
+ );
222
+ if (webhook) {
223
+ return { message: `Create a revalidation webhook with 'prismic webhook create'` };
224
+ }
225
+ }
226
+
227
+ // All complete
228
+ return undefined;
229
+ }
230
+
61
231
  export async function status(): Promise<void> {
62
232
  const {
63
233
  values: { help, repo = await safeGetRepositoryFromConfig() },
@@ -118,39 +288,42 @@ export async function status(): Promise<void> {
118
288
 
119
289
  // Print repository header
120
290
  const repoUrl = await getRepoUrl(repo);
121
- console.info(`Repository: ${repo}`);
122
- console.info(`URL: ${repoUrl.href}`);
123
-
124
- if (repoInfoResult.ok) {
125
- const accessLevel = formatApiAccess(repoInfoResult.value.api_access);
126
- console.info(`Content API Access: ${accessLevel}`);
127
- }
291
+ console.info(`Repository: ${repo}`);
292
+ console.info(`URL: ${repoUrl.href}`);
128
293
  console.info("");
129
294
 
130
295
  const sections: StatusSection[] = [];
131
296
 
297
+ // Track statuses for next step computation
298
+ let typeStatuses: TypeWithStatus[] = [];
299
+ let sliceStatuses: TypeWithStatus[] = [];
300
+ let slicesWithMissingComponents: string[] = [];
301
+
132
302
  // Setup section
133
303
  const setupSection = await buildSetupSection(frameworkInfo, installedDeps);
134
304
  sections.push(setupSection);
135
305
 
136
306
  // Types sections (Page Types and Custom Types)
137
307
  if (localTypesResult.ok && remoteTypesResult.ok) {
138
- const { pageTypes, customTypes } = buildTypeSections(
308
+ const { pageTypes, customTypes, allTypeStatuses } = buildTypeSections(
139
309
  localTypesResult.value,
140
310
  remoteTypesResult.value,
141
311
  );
142
312
  sections.push(pageTypes);
143
313
  sections.push(customTypes);
314
+ typeStatuses = allTypeStatuses;
144
315
  }
145
316
 
146
317
  // Slices section
147
318
  if (localSlicesResult.ok && remoteSlicesResult.ok) {
148
- const slicesSection = await buildSlicesSection(
149
- localSlicesResult.value,
150
- remoteSlicesResult.value,
151
- frameworkInfo,
152
- );
319
+ const {
320
+ section: slicesSection,
321
+ statuses,
322
+ missingComponents,
323
+ } = await buildSlicesSection(localSlicesResult.value, remoteSlicesResult.value, frameworkInfo);
153
324
  sections.push(slicesSection);
325
+ sliceStatuses = statuses;
326
+ slicesWithMissingComponents = missingComponents;
154
327
  }
155
328
 
156
329
  // Preview section
@@ -174,6 +347,18 @@ export async function status(): Promise<void> {
174
347
  for (const section of sections) {
175
348
  printSection(section);
176
349
  }
350
+
351
+ // Print next step
352
+ const nextStep = computeNextStep(
353
+ sections,
354
+ frameworkInfo,
355
+ typeStatuses,
356
+ sliceStatuses,
357
+ slicesWithMissingComponents,
358
+ );
359
+ if (nextStep) {
360
+ console.info(`Next: ${nextStep.message}`);
361
+ }
177
362
  }
178
363
 
179
364
  function printSection(section: StatusSection): void {
@@ -208,9 +393,8 @@ function printSection(section: StatusSection): void {
208
393
  console.info("");
209
394
  }
210
395
 
211
- // Repository Info
396
+ // Repository Info (from /core/repository)
212
397
  const RepositoryInfoSchema = v.object({
213
- api_access: v.optional(v.string()),
214
398
  simulator_url: v.optional(v.string()),
215
399
  });
216
400
  type RepositoryInfo = v.InferOutput<typeof RepositoryInfoSchema>;
@@ -226,19 +410,6 @@ async function fetchRepositoryInfo(
226
410
  return { ok: false };
227
411
  }
228
412
 
229
- function formatApiAccess(access: string | undefined): string {
230
- switch (access) {
231
- case "private":
232
- return "Private";
233
- case "open":
234
- return "Open";
235
- case "master_only":
236
- return "Master only";
237
- default:
238
- return access || "Unknown";
239
- }
240
- }
241
-
242
413
  // Previews
243
414
  const PreviewSchema = v.object({
244
415
  id: v.string(),
@@ -371,7 +542,7 @@ function computeTypeStatus<T extends { id: string }>(local: T[], remote: T[]): T
371
542
  function buildTypeSections(
372
543
  localTypes: CustomType[],
373
544
  remoteTypes: CustomType[],
374
- ): { pageTypes: StatusSection; customTypes: StatusSection } {
545
+ ): { pageTypes: StatusSection; customTypes: StatusSection; allTypeStatuses: TypeWithStatus[] } {
375
546
  const typeStatuses = computeTypeStatus(localTypes, remoteTypes);
376
547
 
377
548
  // Separate by format
@@ -404,6 +575,7 @@ function buildTypeSections(
404
575
  return {
405
576
  pageTypes: { title: "Page Types", items: pageTypeItems },
406
577
  customTypes: { title: "Custom Types", items: customTypeItems },
578
+ allTypeStatuses: typeStatuses,
407
579
  };
408
580
  }
409
581
 
@@ -423,9 +595,14 @@ async function buildSlicesSection(
423
595
  localSlices: SharedSlice[],
424
596
  remoteSlices: SharedSlice[],
425
597
  info: FrameworkInfo,
426
- ): Promise<StatusSection> {
598
+ ): Promise<{
599
+ section: StatusSection;
600
+ statuses: TypeWithStatus[];
601
+ missingComponents: string[];
602
+ }> {
427
603
  const sliceStatuses = computeTypeStatus(localSlices, remoteSlices);
428
604
  const items: StatusItem[] = [];
605
+ const missingComponents: string[] = [];
429
606
 
430
607
  const slicesDir = getSlicesDirectory(info);
431
608
  const extensions = getSliceComponentExtensions(info.framework);
@@ -446,6 +623,7 @@ async function buildSlicesSection(
446
623
  label: slice.label,
447
624
  hint: "missing component",
448
625
  });
626
+ missingComponents.push(slice.label);
449
627
  } else {
450
628
  items.push({
451
629
  done: false,
@@ -455,7 +633,11 @@ async function buildSlicesSection(
455
633
  }
456
634
  }
457
635
 
458
- return { title: "Slices", items };
636
+ return {
637
+ section: { title: "Slices", items },
638
+ statuses: sliceStatuses,
639
+ missingComponents,
640
+ };
459
641
  }
460
642
 
461
643
  async function checkSliceComponent(