@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,223 @@
1
+ import type { Image, SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { writeFile } from "node:fs/promises";
4
+ import { parseArgs } from "node:util";
5
+
6
+ import { buildTypes } from "./codegen-types";
7
+ import { findGroupInVariation, isGroupField, parseFieldPath, validateNestedFieldPath } from "./lib/field-path";
8
+ import { type Framework, detectFrameworkInfo } from "./lib/framework";
9
+ import { stringify } from "./lib/json";
10
+ import { findSliceModel } from "./lib/slice";
11
+ import { humanReadable } from "./lib/string";
12
+
13
+ const HELP = `
14
+ Add an image field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field image <slice-id> <field-id> [flags]
18
+
19
+ ARGUMENTS
20
+ slice-id Slice identifier (required)
21
+ field-id Field identifier (required)
22
+
23
+ FLAGS
24
+ -v, --variation string Target variation (default: first variation)
25
+ -l, --label string Display label for the field (inferred from field-id if omitted)
26
+ --types string Output file for generated types (default: "prismicio-types.d.ts")
27
+ -h, --help Show help for command
28
+
29
+ EXAMPLES
30
+ prismic slice add-field image my_slice background
31
+ prismic slice add-field image hero banner --label "Hero Banner"
32
+ prismic slice add-field image gallery thumbnail --variation "grid"
33
+ `.trim();
34
+
35
+ function getDocsPath(framework: Framework): string {
36
+ switch (framework) {
37
+ case "next":
38
+ return "nextjs/with-cli";
39
+ case "nuxt":
40
+ return "nuxt/with-cli";
41
+ case "sveltekit":
42
+ return "sveltekit/with-cli";
43
+ }
44
+ }
45
+
46
+ function getWriteComponentsAnchor(framework: Framework): string {
47
+ switch (framework) {
48
+ case "nuxt":
49
+ return "#write-vue-components";
50
+ case "sveltekit":
51
+ return "#write-svelte-components";
52
+ default:
53
+ return "#write-react-components";
54
+ }
55
+ }
56
+
57
+ export async function sliceAddFieldImage(): Promise<void> {
58
+ const {
59
+ values: { help, variation, label, types },
60
+ positionals: [sliceId, fieldId],
61
+ } = parseArgs({
62
+ args: process.argv.slice(5), // skip: node, script, "slice", "add-field", "image"
63
+ options: {
64
+ variation: { type: "string", short: "v" },
65
+ label: { type: "string", short: "l" },
66
+ types: { type: "string" },
67
+ help: { type: "boolean", short: "h" },
68
+ },
69
+ allowPositionals: true,
70
+ });
71
+
72
+ if (help) {
73
+ console.info(HELP);
74
+ return;
75
+ }
76
+
77
+ if (!sliceId) {
78
+ console.error("Missing required argument: slice-id\n");
79
+ console.error("Usage: prismic slice add-field image <slice-id> <field-id>");
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ if (!fieldId) {
85
+ console.error("Missing required argument: field-id\n");
86
+ console.error("Usage: prismic slice add-field image <slice-id> <field-id>");
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+
91
+ // Parse and validate field path
92
+ const fieldPath = parseFieldPath(fieldId);
93
+ const pathValidation = validateNestedFieldPath(fieldPath);
94
+ if (!pathValidation.ok) {
95
+ console.error(pathValidation.error);
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+
100
+ // Find the slice model
101
+ const result = await findSliceModel(sliceId);
102
+ if (!result.ok) {
103
+ console.error(result.error);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+
108
+ const { model, modelPath } = result;
109
+
110
+ // Check for variations
111
+ if (model.variations.length === 0) {
112
+ console.error(`Slice "${sliceId}" has no variations.\n`);
113
+ console.error("Add a variation first before adding fields.");
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ // Find target variation
119
+ const targetVariation = variation
120
+ ? model.variations.find((v) => v.id === variation)
121
+ : model.variations[0];
122
+
123
+ if (!targetVariation) {
124
+ console.error(`Variation "${variation}" not found in slice "${sliceId}"\n`);
125
+ console.error(`Available variations: ${model.variations.map((v) => v.id).join(", ")}`);
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+
130
+ // Initialize primary if it doesn't exist
131
+ if (!targetVariation.primary) {
132
+ targetVariation.primary = {};
133
+ }
134
+
135
+ // Build field definition
136
+ const fieldDefinition: Image = {
137
+ type: "Image",
138
+ config: {
139
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
140
+ },
141
+ };
142
+
143
+ // Add field to variation (with nested field support)
144
+ if (fieldPath.type === "nested") {
145
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
146
+ if (!groupResult.ok) {
147
+ console.error(groupResult.error);
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+ // Check if nested field already exists
152
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
153
+ console.error(
154
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
155
+ );
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
160
+ } else {
161
+ // Check if field already exists in any variation (at top level or in groups)
162
+ for (const v of model.variations) {
163
+ if (v.primary?.[fieldId]) {
164
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ // Also check inside groups
169
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
170
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
171
+ console.error(
172
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
173
+ );
174
+ process.exitCode = 1;
175
+ return;
176
+ }
177
+ }
178
+ }
179
+ targetVariation.primary[fieldId] = fieldDefinition;
180
+ }
181
+
182
+ // Write updated model
183
+ try {
184
+ await writeFile(modelPath, stringify(model as SharedSlice));
185
+ } catch (error) {
186
+ if (error instanceof Error) {
187
+ console.error(`Failed to update slice: ${error.message}`);
188
+ } else {
189
+ console.error("Failed to update slice");
190
+ }
191
+ process.exitCode = 1;
192
+ return;
193
+ }
194
+
195
+ if (fieldPath.type === "nested") {
196
+ console.info(
197
+ `Added field "${fieldPath.nestedFieldId}" (Image) to group "${fieldPath.groupId}" in ${sliceId}`,
198
+ );
199
+ } else {
200
+ console.info(
201
+ `Added field "${fieldId}" (Image) to "${targetVariation.id}" variation in ${sliceId}`,
202
+ );
203
+ }
204
+
205
+ try {
206
+ await buildTypes({ output: types });
207
+ console.info(`Updated types in ${types ?? "prismicio-types.d.ts"}`);
208
+ } catch (error) {
209
+ console.warn(`Could not generate types: ${error instanceof Error ? error.message : error}`);
210
+ }
211
+
212
+ console.info();
213
+ console.info("Next: Add more fields with `prismic slice add-field`");
214
+
215
+ const frameworkInfo = await detectFrameworkInfo();
216
+ if (frameworkInfo?.framework) {
217
+ const docsPath = getDocsPath(frameworkInfo.framework);
218
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
219
+ console.info(
220
+ ` Run \`prismic docs fetch ${docsPath}${anchor}\` to learn how to implement the slice's component`,
221
+ );
222
+ }
223
+ }
@@ -0,0 +1,226 @@
1
+ import type { SharedSlice, Text } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { writeFile } from "node:fs/promises";
4
+ import { parseArgs } from "node:util";
5
+
6
+ import { buildTypes } from "./codegen-types";
7
+ import { findGroupInVariation, isGroupField, parseFieldPath, validateNestedFieldPath } from "./lib/field-path";
8
+ import { type Framework, detectFrameworkInfo } from "./lib/framework";
9
+ import { stringify } from "./lib/json";
10
+ import { findSliceModel } from "./lib/slice";
11
+ import { humanReadable } from "./lib/string";
12
+
13
+ const HELP = `
14
+ Add a key-text (single-line text) field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field key-text <slice-id> <field-id> [flags]
18
+
19
+ ARGUMENTS
20
+ slice-id Slice identifier (required)
21
+ field-id Field identifier (required)
22
+
23
+ FLAGS
24
+ -v, --variation string Target variation (default: first variation)
25
+ -l, --label string Display label for the field (inferred from field-id if omitted)
26
+ -p, --placeholder string Placeholder text
27
+ --types string Output file for generated types (default: "prismicio-types.d.ts")
28
+ -h, --help Show help for command
29
+
30
+ EXAMPLES
31
+ prismic slice add-field key-text my_slice title
32
+ prismic slice add-field key-text hero heading --label "Heading"
33
+ prismic slice add-field key-text cta button_text --placeholder "Enter button text"
34
+ `.trim();
35
+
36
+ function getDocsPath(framework: Framework): string {
37
+ switch (framework) {
38
+ case "next":
39
+ return "nextjs/with-cli";
40
+ case "nuxt":
41
+ return "nuxt/with-cli";
42
+ case "sveltekit":
43
+ return "sveltekit/with-cli";
44
+ }
45
+ }
46
+
47
+ function getWriteComponentsAnchor(framework: Framework): string {
48
+ switch (framework) {
49
+ case "nuxt":
50
+ return "#write-vue-components";
51
+ case "sveltekit":
52
+ return "#write-svelte-components";
53
+ default:
54
+ return "#write-react-components";
55
+ }
56
+ }
57
+
58
+ export async function sliceAddFieldKeyText(): Promise<void> {
59
+ const {
60
+ values: { help, variation, label, placeholder, types },
61
+ positionals: [sliceId, fieldId],
62
+ } = parseArgs({
63
+ args: process.argv.slice(5), // skip: node, script, "slice", "add-field", "key-text"
64
+ options: {
65
+ variation: { type: "string", short: "v" },
66
+ label: { type: "string", short: "l" },
67
+ placeholder: { type: "string", short: "p" },
68
+ types: { type: "string" },
69
+ help: { type: "boolean", short: "h" },
70
+ },
71
+ allowPositionals: true,
72
+ });
73
+
74
+ if (help) {
75
+ console.info(HELP);
76
+ return;
77
+ }
78
+
79
+ if (!sliceId) {
80
+ console.error("Missing required argument: slice-id\n");
81
+ console.error("Usage: prismic slice add-field key-text <slice-id> <field-id>");
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+
86
+ if (!fieldId) {
87
+ console.error("Missing required argument: field-id\n");
88
+ console.error("Usage: prismic slice add-field key-text <slice-id> <field-id>");
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+
93
+ // Parse and validate field path
94
+ const fieldPath = parseFieldPath(fieldId);
95
+ const pathValidation = validateNestedFieldPath(fieldPath);
96
+ if (!pathValidation.ok) {
97
+ console.error(pathValidation.error);
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+
102
+ // Find the slice model
103
+ const result = await findSliceModel(sliceId);
104
+ if (!result.ok) {
105
+ console.error(result.error);
106
+ process.exitCode = 1;
107
+ return;
108
+ }
109
+
110
+ const { model, modelPath } = result;
111
+
112
+ // Check for variations
113
+ if (model.variations.length === 0) {
114
+ console.error(`Slice "${sliceId}" has no variations.\n`);
115
+ console.error("Add a variation first before adding fields.");
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+
120
+ // Find target variation
121
+ const targetVariation = variation
122
+ ? model.variations.find((v) => v.id === variation)
123
+ : model.variations[0];
124
+
125
+ if (!targetVariation) {
126
+ console.error(`Variation "${variation}" not found in slice "${sliceId}"\n`);
127
+ console.error(`Available variations: ${model.variations.map((v) => v.id).join(", ")}`);
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+
132
+ // Initialize primary if it doesn't exist
133
+ if (!targetVariation.primary) {
134
+ targetVariation.primary = {};
135
+ }
136
+
137
+ // Build field definition
138
+ const fieldDefinition: Text = {
139
+ type: "Text",
140
+ config: {
141
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
142
+ ...(placeholder && { placeholder }),
143
+ },
144
+ };
145
+
146
+ // Add field to variation (with nested field support)
147
+ if (fieldPath.type === "nested") {
148
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
149
+ if (!groupResult.ok) {
150
+ console.error(groupResult.error);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+ // Check if nested field already exists
155
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
156
+ console.error(
157
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
158
+ );
159
+ process.exitCode = 1;
160
+ return;
161
+ }
162
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
163
+ } else {
164
+ // Check if field already exists in any variation (at top level or in groups)
165
+ for (const v of model.variations) {
166
+ if (v.primary?.[fieldId]) {
167
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
168
+ process.exitCode = 1;
169
+ return;
170
+ }
171
+ // Also check inside groups
172
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
173
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
174
+ console.error(
175
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
176
+ );
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+ }
181
+ }
182
+ targetVariation.primary[fieldId] = fieldDefinition;
183
+ }
184
+
185
+ // Write updated model
186
+ try {
187
+ await writeFile(modelPath, stringify(model as SharedSlice));
188
+ } catch (error) {
189
+ if (error instanceof Error) {
190
+ console.error(`Failed to update slice: ${error.message}`);
191
+ } else {
192
+ console.error("Failed to update slice");
193
+ }
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+
198
+ if (fieldPath.type === "nested") {
199
+ console.info(
200
+ `Added field "${fieldPath.nestedFieldId}" (Text) to group "${fieldPath.groupId}" in ${sliceId}`,
201
+ );
202
+ } else {
203
+ console.info(
204
+ `Added field "${fieldId}" (Text) to "${targetVariation.id}" variation in ${sliceId}`,
205
+ );
206
+ }
207
+
208
+ try {
209
+ await buildTypes({ output: types });
210
+ console.info(`Updated types in ${types ?? "prismicio-types.d.ts"}`);
211
+ } catch (error) {
212
+ console.warn(`Could not generate types: ${error instanceof Error ? error.message : error}`);
213
+ }
214
+
215
+ console.info();
216
+ console.info("Next: Add more fields with `prismic slice add-field`");
217
+
218
+ const frameworkInfo = await detectFrameworkInfo();
219
+ if (frameworkInfo?.framework) {
220
+ const docsPath = getDocsPath(frameworkInfo.framework);
221
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
222
+ console.info(
223
+ ` Run \`prismic docs fetch ${docsPath}${anchor}\` to learn how to implement the slice's component`,
224
+ );
225
+ }
226
+ }
@@ -0,0 +1,245 @@
1
+ import type { Link, SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { writeFile } from "node:fs/promises";
4
+ import { parseArgs } from "node:util";
5
+
6
+ import { buildTypes } from "./codegen-types";
7
+ import { findGroupInVariation, isGroupField, parseFieldPath, validateNestedFieldPath } from "./lib/field-path";
8
+ import { type Framework, detectFrameworkInfo } from "./lib/framework";
9
+ import { stringify } from "./lib/json";
10
+ import { findSliceModel } from "./lib/slice";
11
+ import { humanReadable } from "./lib/string";
12
+
13
+ const HELP = `
14
+ Add a link field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field link <slice-id> <field-id> [flags]
18
+
19
+ ARGUMENTS
20
+ slice-id Slice identifier (required)
21
+ field-id Field identifier (required)
22
+
23
+ FLAGS
24
+ -v, --variation string Target variation (default: first variation)
25
+ -l, --label string Display label for the field (inferred from field-id if omitted)
26
+ -p, --placeholder string Placeholder text
27
+ --allow-text Allow text with link
28
+ --allow-target-blank Allow opening link in new tab
29
+ --repeatable Allow multiple links
30
+ --types string Output file for generated types (default: "prismicio-types.d.ts")
31
+ -h, --help Show help for command
32
+
33
+ EXAMPLES
34
+ prismic slice add-field link my_slice button
35
+ prismic slice add-field link cta primary_link --allow-text
36
+ prismic slice add-field link navigation links --repeatable
37
+ prismic slice add-field link hero cta --allow-text --allow-target-blank
38
+ `.trim();
39
+
40
+ function getDocsPath(framework: Framework): string {
41
+ switch (framework) {
42
+ case "next":
43
+ return "nextjs/with-cli";
44
+ case "nuxt":
45
+ return "nuxt/with-cli";
46
+ case "sveltekit":
47
+ return "sveltekit/with-cli";
48
+ }
49
+ }
50
+
51
+ function getWriteComponentsAnchor(framework: Framework): string {
52
+ switch (framework) {
53
+ case "nuxt":
54
+ return "#write-vue-components";
55
+ case "sveltekit":
56
+ return "#write-svelte-components";
57
+ default:
58
+ return "#write-react-components";
59
+ }
60
+ }
61
+
62
+ export async function sliceAddFieldLink(): Promise<void> {
63
+ const {
64
+ values: {
65
+ help,
66
+ variation,
67
+ label,
68
+ placeholder,
69
+ "allow-text": allowText,
70
+ "allow-target-blank": allowTargetBlank,
71
+ repeatable,
72
+ types,
73
+ },
74
+ positionals: [sliceId, fieldId],
75
+ } = parseArgs({
76
+ args: process.argv.slice(5), // skip: node, script, "slice", "add-field", "link"
77
+ options: {
78
+ variation: { type: "string", short: "v" },
79
+ label: { type: "string", short: "l" },
80
+ placeholder: { type: "string", short: "p" },
81
+ "allow-text": { type: "boolean" },
82
+ "allow-target-blank": { type: "boolean" },
83
+ repeatable: { type: "boolean" },
84
+ types: { type: "string" },
85
+ help: { type: "boolean", short: "h" },
86
+ },
87
+ allowPositionals: true,
88
+ });
89
+
90
+ if (help) {
91
+ console.info(HELP);
92
+ return;
93
+ }
94
+
95
+ if (!sliceId) {
96
+ console.error("Missing required argument: slice-id\n");
97
+ console.error("Usage: prismic slice add-field link <slice-id> <field-id>");
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+
102
+ if (!fieldId) {
103
+ console.error("Missing required argument: field-id\n");
104
+ console.error("Usage: prismic slice add-field link <slice-id> <field-id>");
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+
109
+ // Parse and validate field path
110
+ const fieldPath = parseFieldPath(fieldId);
111
+ const pathValidation = validateNestedFieldPath(fieldPath);
112
+ if (!pathValidation.ok) {
113
+ console.error(pathValidation.error);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ // Find the slice model
119
+ const result = await findSliceModel(sliceId);
120
+ if (!result.ok) {
121
+ console.error(result.error);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
126
+ const { model, modelPath } = result;
127
+
128
+ // Check for variations
129
+ if (model.variations.length === 0) {
130
+ console.error(`Slice "${sliceId}" has no variations.\n`);
131
+ console.error("Add a variation first before adding fields.");
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+
136
+ // Find target variation
137
+ const targetVariation = variation
138
+ ? model.variations.find((v) => v.id === variation)
139
+ : model.variations[0];
140
+
141
+ if (!targetVariation) {
142
+ console.error(`Variation "${variation}" not found in slice "${sliceId}"\n`);
143
+ console.error(`Available variations: ${model.variations.map((v) => v.id).join(", ")}`);
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+
148
+ // Initialize primary if it doesn't exist
149
+ if (!targetVariation.primary) {
150
+ targetVariation.primary = {};
151
+ }
152
+
153
+ // Build field definition
154
+ const fieldDefinition: Link = {
155
+ type: "Link",
156
+ config: {
157
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
158
+ ...(placeholder && { placeholder }),
159
+ ...(allowText && { allowText: true }),
160
+ ...(allowTargetBlank && { allowTargetBlank: true }),
161
+ ...(repeatable && { repeat: true }),
162
+ },
163
+ };
164
+
165
+ // Add field to variation (with nested field support)
166
+ if (fieldPath.type === "nested") {
167
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
168
+ if (!groupResult.ok) {
169
+ console.error(groupResult.error);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ // Check if nested field already exists
174
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
175
+ console.error(
176
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
177
+ );
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
182
+ } else {
183
+ // Check if field already exists in any variation (at top level or in groups)
184
+ for (const v of model.variations) {
185
+ if (v.primary?.[fieldId]) {
186
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ // Also check inside groups
191
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
192
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
193
+ console.error(
194
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
195
+ );
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ }
200
+ }
201
+ targetVariation.primary[fieldId] = fieldDefinition;
202
+ }
203
+
204
+ // Write updated model
205
+ try {
206
+ await writeFile(modelPath, stringify(model as SharedSlice));
207
+ } catch (error) {
208
+ if (error instanceof Error) {
209
+ console.error(`Failed to update slice: ${error.message}`);
210
+ } else {
211
+ console.error("Failed to update slice");
212
+ }
213
+ process.exitCode = 1;
214
+ return;
215
+ }
216
+
217
+ if (fieldPath.type === "nested") {
218
+ console.info(
219
+ `Added field "${fieldPath.nestedFieldId}" (Link) to group "${fieldPath.groupId}" in ${sliceId}`,
220
+ );
221
+ } else {
222
+ console.info(
223
+ `Added field "${fieldId}" (Link) to "${targetVariation.id}" variation in ${sliceId}`,
224
+ );
225
+ }
226
+
227
+ try {
228
+ await buildTypes({ output: types });
229
+ console.info(`Updated types in ${types ?? "prismicio-types.d.ts"}`);
230
+ } catch (error) {
231
+ console.warn(`Could not generate types: ${error instanceof Error ? error.message : error}`);
232
+ }
233
+
234
+ console.info();
235
+ console.info("Next: Add more fields with `prismic slice add-field`");
236
+
237
+ const frameworkInfo = await detectFrameworkInfo();
238
+ if (frameworkInfo?.framework) {
239
+ const docsPath = getDocsPath(frameworkInfo.framework);
240
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
241
+ console.info(
242
+ ` Run \`prismic docs fetch ${docsPath}${anchor}\` to learn how to implement the slice's component`,
243
+ );
244
+ }
245
+ }