@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
@@ -0,0 +1,111 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
4
+ import * as v from "valibot";
5
+
6
+ import { findUpward } from "./file";
7
+ import { stringify } from "./json";
8
+
9
+ const CONFIG_FILENAME = "prismic.config.json";
10
+
11
+ const ConfigSchema = v.object({
12
+ repositoryName: v.string(),
13
+ apiEndpoint: v.optional(v.pipe(v.string(), v.url())),
14
+ localSliceMachineSimulatorURL: v.optional(v.pipe(v.string(), v.url())),
15
+ libraries: v.optional(v.array(v.string())),
16
+ adapter: v.optional(v.string()),
17
+ labs: v.optional(
18
+ v.object({
19
+ legacySliceUpgrader: v.optional(v.boolean()),
20
+ }),
21
+ ),
22
+ });
23
+ type Config = v.InferOutput<typeof ConfigSchema>;
24
+
25
+ export type ConfigResult = SuccessfulConfigResult | FailedConfigResult;
26
+ export type SuccessfulConfigResult = { ok: true; config: Config };
27
+ export type FailedConfigResult = {
28
+ ok: false;
29
+ error: InvalidPrismicConfig | MissingPrismicConfig;
30
+ };
31
+ export type UnknownProjectRootConfigResult = {
32
+ ok: false;
33
+ error: UnknownProjectRoot;
34
+ };
35
+
36
+ export async function createConfig(
37
+ config: Config,
38
+ cwd = pathToFileURL(process.cwd()),
39
+ ): Promise<ConfigResult | UnknownProjectRootConfigResult> {
40
+ const result = await findSuggestedConfigPath(cwd);
41
+ if (!result.ok) return result;
42
+ await writeFile(result.path, stringify(config));
43
+ return { ok: true, config };
44
+ }
45
+
46
+ export async function safeGetRepositoryFromConfig(
47
+ cwd = pathToFileURL(process.cwd()),
48
+ ): Promise<string | undefined> {
49
+ const result = await readConfig(cwd);
50
+ if (result.ok) return result.config.repositoryName;
51
+ }
52
+
53
+ export async function readConfig(cwd = pathToFileURL(process.cwd())): Promise<ConfigResult> {
54
+ const findResult = await findConfig(cwd);
55
+ if (!findResult.ok) return findResult;
56
+
57
+ try {
58
+ const contents = await readFile(findResult.path, "utf8");
59
+ const result = v.safeParse(ConfigSchema, JSON.parse(contents));
60
+ if (!result.success) {
61
+ return { ok: false, error: new InvalidPrismicConfig(result.issues) };
62
+ }
63
+ return { ok: true, config: result.output };
64
+ } catch {
65
+ return { ok: false, error: new InvalidPrismicConfig() };
66
+ }
67
+ }
68
+
69
+ export class InvalidPrismicConfig extends Error {
70
+ issues: v.InferIssue<typeof ConfigSchema>[];
71
+
72
+ constructor(issues: v.InferIssue<typeof ConfigSchema>[] = []) {
73
+ super("prismic.config.json is invalid.");
74
+ this.issues = issues;
75
+ }
76
+ }
77
+
78
+ export async function updateConfig(
79
+ config: Partial<Config>,
80
+ cwd = pathToFileURL(process.cwd()),
81
+ ): Promise<ConfigResult> {
82
+ const findResult = await findConfig(cwd);
83
+ if (!findResult.ok) return findResult;
84
+
85
+ const readResult = await readConfig(cwd);
86
+ if (!readResult.ok) return readResult;
87
+
88
+ const updatedConfig = { ...readResult.config, ...config };
89
+ await writeFile(findResult.path, stringify(updatedConfig));
90
+ return { ok: true, config: updatedConfig };
91
+ }
92
+
93
+ async function findConfig(cwd = pathToFileURL(process.cwd())) {
94
+ const path = await findUpward(CONFIG_FILENAME, { start: cwd, stop: "package.json" });
95
+ if (!path) return { ok: false, error: new MissingPrismicConfig() } as const;
96
+ return { ok: true, path } as const;
97
+ }
98
+
99
+ async function findSuggestedConfigPath(cwd = pathToFileURL(process.cwd())) {
100
+ const path = await findUpward("package.json", { start: cwd });
101
+ if (!path) return { ok: false, error: new UnknownProjectRoot() } as const;
102
+ return { ok: true, path: new URL(CONFIG_FILENAME, path) } as const;
103
+ }
104
+
105
+ export class MissingPrismicConfig extends Error {
106
+ message = "Could not find a prismic.config.json file.";
107
+ }
108
+
109
+ export class UnknownProjectRoot extends Error {
110
+ message = "Could not find a package.json file.";
111
+ }
@@ -0,0 +1,438 @@
1
+ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import * as v from "valibot";
5
+
6
+ import { readHost, readToken } from "./auth";
7
+ import { findUpward } from "./file";
8
+ import { getSlicesDirectory, SharedSliceSchema } from "./slice";
9
+
10
+ export const CustomTypeSchema = v.object({
11
+ id: v.string(),
12
+ label: v.optional(v.string()),
13
+ repeatable: v.boolean(),
14
+ status: v.boolean(),
15
+ format: v.optional(v.string()),
16
+ json: v.record(v.string(), v.unknown()),
17
+ });
18
+
19
+ export type FetchResult<T> = { ok: true; value: T } | { ok: false; error: string };
20
+
21
+ export async function getCustomTypesApiUrl(): Promise<URL> {
22
+ const host = await readHost();
23
+ host.hostname = `customtypes.${host.hostname}`;
24
+ return host;
25
+ }
26
+
27
+ export async function fetchRemoteCustomTypes(repo: string): Promise<FetchResult<CustomType[]>> {
28
+ const token = await readToken();
29
+ if (!token) {
30
+ return { ok: false, error: "Not authenticated" };
31
+ }
32
+
33
+ const baseUrl = await getCustomTypesApiUrl();
34
+ const url = new URL("customtypes", baseUrl);
35
+
36
+ try {
37
+ const response = await fetch(url, {
38
+ headers: {
39
+ Authorization: `Bearer ${token}`,
40
+ repository: repo,
41
+ },
42
+ });
43
+
44
+ if (!response.ok) {
45
+ if (response.status === 401) {
46
+ return {
47
+ ok: false,
48
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
49
+ };
50
+ }
51
+ if (response.status === 403) {
52
+ return {
53
+ ok: false,
54
+ error: `Access denied. You may not have access to repository "${repo}".`,
55
+ };
56
+ }
57
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
58
+ }
59
+
60
+ const data = await response.json();
61
+ const result = v.safeParse(v.array(CustomTypeSchema), data);
62
+ if (!result.success) {
63
+ return { ok: false, error: "Invalid response from Custom Types API" };
64
+ }
65
+
66
+ return { ok: true, value: result.output as CustomType[] };
67
+ } catch (error) {
68
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
69
+ }
70
+ }
71
+
72
+ export async function fetchRemoteSlices(repo: string): Promise<FetchResult<SharedSlice[]>> {
73
+ const token = await readToken();
74
+ if (!token) {
75
+ return { ok: false, error: "Not authenticated" };
76
+ }
77
+
78
+ const baseUrl = await getCustomTypesApiUrl();
79
+ const url = new URL("slices", baseUrl);
80
+
81
+ try {
82
+ const response = await fetch(url, {
83
+ headers: {
84
+ Authorization: `Bearer ${token}`,
85
+ repository: repo,
86
+ },
87
+ });
88
+
89
+ if (!response.ok) {
90
+ if (response.status === 401) {
91
+ return {
92
+ ok: false,
93
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
94
+ };
95
+ }
96
+ if (response.status === 403) {
97
+ return {
98
+ ok: false,
99
+ error: `Access denied. You may not have access to repository "${repo}".`,
100
+ };
101
+ }
102
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
103
+ }
104
+
105
+ const data = await response.json();
106
+ const result = v.safeParse(v.array(SharedSliceSchema), data);
107
+ if (!result.success) {
108
+ return { ok: false, error: "Invalid response from Custom Types API" };
109
+ }
110
+
111
+ return { ok: true, value: result.output as SharedSlice[] };
112
+ } catch (error) {
113
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
114
+ }
115
+ }
116
+
117
+ export async function readLocalCustomTypes(): Promise<FetchResult<CustomType[]>> {
118
+ const projectRoot = await findUpward("package.json");
119
+ if (!projectRoot) {
120
+ return { ok: false, error: "Could not find project root (no package.json found)" };
121
+ }
122
+
123
+ const projectDir = new URL(".", projectRoot);
124
+ const customTypesDir = new URL("customtypes/", projectDir);
125
+
126
+ let entries: string[];
127
+ try {
128
+ entries = (await readdir(customTypesDir, { withFileTypes: false })) as unknown as string[];
129
+ } catch {
130
+ // No customtypes directory means no local custom types
131
+ return { ok: true, value: [] };
132
+ }
133
+
134
+ const customTypes: CustomType[] = [];
135
+ for (const entry of entries) {
136
+ const modelPath = new URL(`${entry}/index.json`, customTypesDir);
137
+ try {
138
+ const contents = await readFile(modelPath, "utf8");
139
+ const parsed = JSON.parse(contents);
140
+ const result = v.safeParse(CustomTypeSchema, parsed);
141
+ if (result.success) {
142
+ customTypes.push(result.output as CustomType);
143
+ }
144
+ } catch {
145
+ // Skip directories without valid index.json
146
+ }
147
+ }
148
+
149
+ return { ok: true, value: customTypes };
150
+ }
151
+
152
+ export async function readLocalSlices(): Promise<FetchResult<SharedSlice[]>> {
153
+ let slicesDir: URL;
154
+ try {
155
+ slicesDir = await getSlicesDirectory();
156
+ } catch {
157
+ return { ok: false, error: "Could not find project root (no package.json found)" };
158
+ }
159
+
160
+ let entries: string[];
161
+ try {
162
+ entries = (await readdir(slicesDir, { withFileTypes: false })) as unknown as string[];
163
+ } catch {
164
+ // No slices directory means no local slices
165
+ return { ok: true, value: [] };
166
+ }
167
+
168
+ const slices: SharedSlice[] = [];
169
+ for (const entry of entries) {
170
+ const modelPath = new URL(`${entry}/model.json`, slicesDir);
171
+ try {
172
+ const contents = await readFile(modelPath, "utf8");
173
+ const parsed = JSON.parse(contents);
174
+ const result = v.safeParse(SharedSliceSchema, parsed);
175
+ if (result.success) {
176
+ slices.push(result.output as SharedSlice);
177
+ }
178
+ } catch {
179
+ // Skip directories without valid model.json
180
+ }
181
+ }
182
+
183
+ return { ok: true, value: slices };
184
+ }
185
+
186
+ export async function insertCustomType(
187
+ repo: string,
188
+ model: CustomType,
189
+ ): Promise<FetchResult<void>> {
190
+ const token = await readToken();
191
+ if (!token) {
192
+ return { ok: false, error: "Not authenticated" };
193
+ }
194
+
195
+ const baseUrl = await getCustomTypesApiUrl();
196
+ const url = new URL("customtypes/insert", baseUrl);
197
+
198
+ try {
199
+ const response = await fetch(url, {
200
+ method: "POST",
201
+ headers: {
202
+ Authorization: `Bearer ${token}`,
203
+ repository: repo,
204
+ "Content-Type": "application/json",
205
+ },
206
+ body: JSON.stringify(model),
207
+ });
208
+
209
+ if (!response.ok) {
210
+ if (response.status === 401) {
211
+ return {
212
+ ok: false,
213
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
214
+ };
215
+ }
216
+ if (response.status === 403) {
217
+ return {
218
+ ok: false,
219
+ error: `Access denied. You may not have access to repository "${repo}".`,
220
+ };
221
+ }
222
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
223
+ }
224
+
225
+ return { ok: true, value: undefined };
226
+ } catch (error) {
227
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
228
+ }
229
+ }
230
+
231
+ export async function updateCustomType(
232
+ repo: string,
233
+ model: CustomType,
234
+ ): Promise<FetchResult<void>> {
235
+ const token = await readToken();
236
+ if (!token) {
237
+ return { ok: false, error: "Not authenticated" };
238
+ }
239
+
240
+ const baseUrl = await getCustomTypesApiUrl();
241
+ const url = new URL("customtypes/update", baseUrl);
242
+
243
+ try {
244
+ const response = await fetch(url, {
245
+ method: "POST",
246
+ headers: {
247
+ Authorization: `Bearer ${token}`,
248
+ repository: repo,
249
+ "Content-Type": "application/json",
250
+ },
251
+ body: JSON.stringify(model),
252
+ });
253
+
254
+ if (!response.ok) {
255
+ if (response.status === 401) {
256
+ return {
257
+ ok: false,
258
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
259
+ };
260
+ }
261
+ if (response.status === 403) {
262
+ return {
263
+ ok: false,
264
+ error: `Access denied. You may not have access to repository "${repo}".`,
265
+ };
266
+ }
267
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
268
+ }
269
+
270
+ return { ok: true, value: undefined };
271
+ } catch (error) {
272
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
273
+ }
274
+ }
275
+
276
+ export async function deleteCustomType(repo: string, id: string): Promise<FetchResult<void>> {
277
+ const token = await readToken();
278
+ if (!token) {
279
+ return { ok: false, error: "Not authenticated" };
280
+ }
281
+
282
+ const baseUrl = await getCustomTypesApiUrl();
283
+ const url = new URL(`customtypes/${encodeURIComponent(id)}`, baseUrl);
284
+
285
+ try {
286
+ const response = await fetch(url, {
287
+ method: "DELETE",
288
+ headers: {
289
+ Authorization: `Bearer ${token}`,
290
+ repository: repo,
291
+ },
292
+ });
293
+
294
+ if (!response.ok) {
295
+ if (response.status === 401) {
296
+ return {
297
+ ok: false,
298
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
299
+ };
300
+ }
301
+ if (response.status === 403) {
302
+ return {
303
+ ok: false,
304
+ error: `Access denied. You may not have access to repository "${repo}".`,
305
+ };
306
+ }
307
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
308
+ }
309
+
310
+ return { ok: true, value: undefined };
311
+ } catch (error) {
312
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
313
+ }
314
+ }
315
+
316
+ export async function insertSlice(repo: string, model: SharedSlice): Promise<FetchResult<void>> {
317
+ const token = await readToken();
318
+ if (!token) {
319
+ return { ok: false, error: "Not authenticated" };
320
+ }
321
+
322
+ const baseUrl = await getCustomTypesApiUrl();
323
+ const url = new URL("slices/insert", baseUrl);
324
+
325
+ try {
326
+ const response = await fetch(url, {
327
+ method: "POST",
328
+ headers: {
329
+ Authorization: `Bearer ${token}`,
330
+ repository: repo,
331
+ "Content-Type": "application/json",
332
+ },
333
+ body: JSON.stringify(model),
334
+ });
335
+
336
+ if (!response.ok) {
337
+ if (response.status === 401) {
338
+ return {
339
+ ok: false,
340
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
341
+ };
342
+ }
343
+ if (response.status === 403) {
344
+ return {
345
+ ok: false,
346
+ error: `Access denied. You may not have access to repository "${repo}".`,
347
+ };
348
+ }
349
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
350
+ }
351
+
352
+ return { ok: true, value: undefined };
353
+ } catch (error) {
354
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
355
+ }
356
+ }
357
+
358
+ export async function updateSlice(repo: string, model: SharedSlice): Promise<FetchResult<void>> {
359
+ const token = await readToken();
360
+ if (!token) {
361
+ return { ok: false, error: "Not authenticated" };
362
+ }
363
+
364
+ const baseUrl = await getCustomTypesApiUrl();
365
+ const url = new URL("slices/update", baseUrl);
366
+
367
+ try {
368
+ const response = await fetch(url, {
369
+ method: "POST",
370
+ headers: {
371
+ Authorization: `Bearer ${token}`,
372
+ repository: repo,
373
+ "Content-Type": "application/json",
374
+ },
375
+ body: JSON.stringify(model),
376
+ });
377
+
378
+ if (!response.ok) {
379
+ if (response.status === 401) {
380
+ return {
381
+ ok: false,
382
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
383
+ };
384
+ }
385
+ if (response.status === 403) {
386
+ return {
387
+ ok: false,
388
+ error: `Access denied. You may not have access to repository "${repo}".`,
389
+ };
390
+ }
391
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
392
+ }
393
+
394
+ return { ok: true, value: undefined };
395
+ } catch (error) {
396
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
397
+ }
398
+ }
399
+
400
+ export async function deleteSlice(repo: string, id: string): Promise<FetchResult<void>> {
401
+ const token = await readToken();
402
+ if (!token) {
403
+ return { ok: false, error: "Not authenticated" };
404
+ }
405
+
406
+ const baseUrl = await getCustomTypesApiUrl();
407
+ const url = new URL(`slices/${encodeURIComponent(id)}`, baseUrl);
408
+
409
+ try {
410
+ const response = await fetch(url, {
411
+ method: "DELETE",
412
+ headers: {
413
+ Authorization: `Bearer ${token}`,
414
+ repository: repo,
415
+ },
416
+ });
417
+
418
+ if (!response.ok) {
419
+ if (response.status === 401) {
420
+ return {
421
+ ok: false,
422
+ error: "Unauthorized. Your session may have expired. Run `prismic login` again.",
423
+ };
424
+ }
425
+ if (response.status === 403) {
426
+ return {
427
+ ok: false,
428
+ error: `Access denied. You may not have access to repository "${repo}".`,
429
+ };
430
+ }
431
+ return { ok: false, error: `API error: ${response.status} ${response.statusText}` };
432
+ }
433
+
434
+ return { ok: true, value: undefined };
435
+ } catch (error) {
436
+ return { ok: false, error: `Network error: ${error instanceof Error ? error.message : error}` };
437
+ }
438
+ }
@@ -0,0 +1,81 @@
1
+ export type FieldPath =
2
+ | { type: "top-level"; fieldId: string }
3
+ | { type: "nested"; groupId: string; nestedFieldId: string };
4
+
5
+ export function parseFieldPath(fieldId: string): FieldPath {
6
+ const parts = fieldId.split(".");
7
+
8
+ if (parts.length === 1) {
9
+ return { type: "top-level", fieldId };
10
+ }
11
+
12
+ if (parts.length === 2) {
13
+ return { type: "nested", groupId: parts[0], nestedFieldId: parts[1] };
14
+ }
15
+
16
+ // More than 2 parts means nested groups which aren't supported
17
+ return { type: "nested", groupId: parts[0], nestedFieldId: parts.slice(1).join(".") };
18
+ }
19
+
20
+ export type GroupFieldResult =
21
+ | { ok: true; group: { config: { fields: Record<string, unknown> } } }
22
+ | { ok: false; error: string };
23
+
24
+ export function isGroupField(field: unknown): field is { type: "Group"; config: { fields: Record<string, unknown> } } {
25
+ return (
26
+ typeof field === "object" &&
27
+ field !== null &&
28
+ "type" in field &&
29
+ field.type === "Group" &&
30
+ "config" in field &&
31
+ typeof field.config === "object" &&
32
+ field.config !== null &&
33
+ "fields" in field.config
34
+ );
35
+ }
36
+
37
+ export function findGroupInTab(
38
+ tabFields: Record<string, unknown>,
39
+ groupId: string,
40
+ tabName: string,
41
+ ): GroupFieldResult {
42
+ const field = tabFields[groupId];
43
+
44
+ if (!field) {
45
+ return { ok: false, error: `Group "${groupId}" not found in tab "${tabName}"` };
46
+ }
47
+
48
+ if (!isGroupField(field)) {
49
+ return { ok: false, error: `Field "${groupId}" is not a group` };
50
+ }
51
+
52
+ return { ok: true, group: field };
53
+ }
54
+
55
+ export function findGroupInVariation(
56
+ primary: Record<string, unknown>,
57
+ groupId: string,
58
+ variationId: string,
59
+ ): GroupFieldResult {
60
+ const field = primary[groupId];
61
+
62
+ if (!field) {
63
+ return { ok: false, error: `Group "${groupId}" not found in variation "${variationId}"` };
64
+ }
65
+
66
+ if (!isGroupField(field)) {
67
+ return { ok: false, error: `Field "${groupId}" is not a group` };
68
+ }
69
+
70
+ return { ok: true, group: field };
71
+ }
72
+
73
+ export function validateNestedFieldPath(fieldPath: FieldPath): { ok: true } | { ok: false; error: string } {
74
+ if (fieldPath.type === "nested" && fieldPath.nestedFieldId.includes(".")) {
75
+ return {
76
+ ok: false,
77
+ error: `Nested groups not supported. Use: group.field`,
78
+ };
79
+ }
80
+ return { ok: true };
81
+ }
@@ -0,0 +1,49 @@
1
+ import { access } from "node:fs/promises";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { appendTrailingSlash } from "./url";
5
+
6
+ export async function findUpward(
7
+ name: string,
8
+ config: { start?: URL; stop?: URL | string } = {},
9
+ ): Promise<URL | undefined> {
10
+ const { start = pathToFileURL(process.cwd()), stop } = config;
11
+
12
+ let dir = appendTrailingSlash(start);
13
+
14
+ while (true) {
15
+ const path = new URL(name, dir);
16
+ try {
17
+ await access(path);
18
+ return path;
19
+ } catch {}
20
+
21
+ if (typeof stop === "string") {
22
+ const stopPath = new URL(stop, dir);
23
+ try {
24
+ await access(stopPath);
25
+ return;
26
+ } catch {}
27
+ } else if (stop instanceof URL) {
28
+ if (stop.href === dir.href) {
29
+ return;
30
+ }
31
+ }
32
+
33
+ const parent = new URL("..", dir);
34
+ if (parent.href === dir.href) {
35
+ return undefined;
36
+ }
37
+
38
+ dir = parent;
39
+ }
40
+ }
41
+
42
+ export async function exists(path: URL): Promise<boolean> {
43
+ try {
44
+ await access(path);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }