@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.
- package/LICENSE +202 -0
- package/README.md +98 -0
- package/dist/index.mjs +2548 -0
- package/package.json +53 -0
- package/src/codegen-types.ts +82 -0
- package/src/codegen.ts +45 -0
- package/src/custom-type-add-field-boolean.ts +192 -0
- package/src/custom-type-add-field-color.ts +177 -0
- package/src/custom-type-add-field-date.ts +180 -0
- package/src/custom-type-add-field-embed.ts +177 -0
- package/src/custom-type-add-field-geo-point.ts +174 -0
- package/src/custom-type-add-field-image.ts +177 -0
- package/src/custom-type-add-field-key-text.ts +177 -0
- package/src/custom-type-add-field-link.ts +201 -0
- package/src/custom-type-add-field-number.ts +209 -0
- package/src/custom-type-add-field-rich-text.ts +202 -0
- package/src/custom-type-add-field-select.ts +192 -0
- package/src/custom-type-add-field-timestamp.ts +180 -0
- package/src/custom-type-add-field-uid.ts +177 -0
- package/src/custom-type-add-field.ts +111 -0
- package/src/custom-type-connect-slice.ts +220 -0
- package/src/custom-type-create.ts +118 -0
- package/src/custom-type-disconnect-slice.ts +177 -0
- package/src/custom-type-list.ts +110 -0
- package/src/custom-type-remove-field.ts +177 -0
- package/src/custom-type-remove.ts +144 -0
- package/src/custom-type-set-name.ts +144 -0
- package/src/custom-type-view.ts +118 -0
- package/src/custom-type.ts +85 -0
- package/src/index.ts +127 -0
- package/src/init.ts +64 -0
- package/src/lib/auth.ts +83 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/custom-types-api.ts +438 -0
- package/src/lib/file.ts +49 -0
- package/src/lib/framework.ts +143 -0
- package/src/lib/json.ts +3 -0
- package/src/lib/request.ts +116 -0
- package/src/lib/slice.ts +115 -0
- package/src/lib/string.ts +6 -0
- package/src/lib/url.ts +25 -0
- package/src/locale-add.ts +116 -0
- package/src/locale-list.ts +107 -0
- package/src/locale-remove.ts +88 -0
- package/src/locale-set-default.ts +131 -0
- package/src/locale.ts +60 -0
- package/src/login.ts +152 -0
- package/src/logout.ts +36 -0
- package/src/page-type-add-field-boolean.ts +192 -0
- package/src/page-type-add-field-color.ts +177 -0
- package/src/page-type-add-field-date.ts +180 -0
- package/src/page-type-add-field-embed.ts +177 -0
- package/src/page-type-add-field-geo-point.ts +174 -0
- package/src/page-type-add-field-image.ts +177 -0
- package/src/page-type-add-field-key-text.ts +177 -0
- package/src/page-type-add-field-link.ts +201 -0
- package/src/page-type-add-field-number.ts +209 -0
- package/src/page-type-add-field-rich-text.ts +202 -0
- package/src/page-type-add-field-select.ts +192 -0
- package/src/page-type-add-field-timestamp.ts +180 -0
- package/src/page-type-add-field-uid.ts +177 -0
- package/src/page-type-add-field.ts +111 -0
- package/src/page-type-connect-slice.ts +220 -0
- package/src/page-type-create.ts +142 -0
- package/src/page-type-disconnect-slice.ts +177 -0
- package/src/page-type-list.ts +109 -0
- package/src/page-type-remove-field.ts +177 -0
- package/src/page-type-remove.ts +144 -0
- package/src/page-type-set-name.ts +144 -0
- package/src/page-type-set-repeatable.ts +153 -0
- package/src/page-type-view.ts +118 -0
- package/src/page-type.ts +90 -0
- package/src/preview-add.ts +126 -0
- package/src/preview-get-simulator.ts +104 -0
- package/src/preview-list.ts +106 -0
- package/src/preview-remove-simulator.ts +80 -0
- package/src/preview-remove.ts +109 -0
- package/src/preview-set-name.ts +137 -0
- package/src/preview-set-simulator.ts +116 -0
- package/src/preview.ts +75 -0
- package/src/pull.ts +247 -0
- package/src/push.ts +405 -0
- package/src/repo-create.ts +136 -0
- package/src/repo-get-access.ts +86 -0
- package/src/repo-list.ts +100 -0
- package/src/repo-set-access.ts +100 -0
- package/src/repo-set-name.ts +102 -0
- package/src/repo-view.ts +113 -0
- package/src/repo.ts +70 -0
- package/src/slice-add-field-boolean.ts +173 -0
- package/src/slice-add-field-color.ts +158 -0
- package/src/slice-add-field-date.ts +158 -0
- package/src/slice-add-field-embed.ts +158 -0
- package/src/slice-add-field-geo-point.ts +155 -0
- package/src/slice-add-field-image.ts +155 -0
- package/src/slice-add-field-key-text.ts +158 -0
- package/src/slice-add-field-link.ts +178 -0
- package/src/slice-add-field-number.ts +158 -0
- package/src/slice-add-field-rich-text.ts +183 -0
- package/src/slice-add-field-select.ts +173 -0
- package/src/slice-add-field-timestamp.ts +158 -0
- package/src/slice-add-field.ts +106 -0
- package/src/slice-add-variation.ts +145 -0
- package/src/slice-create.ts +148 -0
- package/src/slice-list-variations.ts +67 -0
- package/src/slice-list.ts +88 -0
- package/src/slice-remove-field.ts +128 -0
- package/src/slice-remove-variation.ts +118 -0
- package/src/slice-remove.ts +97 -0
- package/src/slice-rename.ts +128 -0
- package/src/slice-view.ts +77 -0
- package/src/slice.ts +90 -0
- package/src/status.ts +733 -0
- package/src/token-create.ts +203 -0
- package/src/token-delete.ts +182 -0
- package/src/token-list.ts +223 -0
- package/src/token-set-name.ts +193 -0
- package/src/token.ts +60 -0
- package/src/webhook-add-header.ts +118 -0
- package/src/webhook-create.ts +152 -0
- package/src/webhook-disable.ts +109 -0
- package/src/webhook-enable.ts +132 -0
- package/src/webhook-list.ts +93 -0
- package/src/webhook-remove-header.ts +117 -0
- package/src/webhook-remove.ts +106 -0
- package/src/webhook-set-triggers.ts +148 -0
- package/src/webhook-status.ts +90 -0
- package/src/webhook-test.ts +106 -0
- package/src/webhook-view.ts +147 -0
- package/src/webhook.ts +95 -0
- 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
|
+
}
|