@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,226 @@
1
+ import type { Number, 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 number field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field number <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 number my_slice price
32
+ prismic slice add-field number product quantity --label "Quantity"
33
+ prismic slice add-field number stats count --variation "detailed"
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 sliceAddFieldNumber(): 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", "number"
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 number <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 number <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: Number = {
139
+ type: "Number",
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}" (Number) to group "${fieldPath.groupId}" in ${sliceId}`,
201
+ );
202
+ } else {
203
+ console.info(
204
+ `Added field "${fieldId}" (Number) 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,250 @@
1
+ import type { RichText, 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 rich text field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field rich-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
+ --single string Allowed block types for single-line (comma-separated)
28
+ --multi string Allowed block types for multi-line (comma-separated)
29
+ --allow-target-blank Allow opening links in new tab
30
+ --types string Output file for generated types (default: "prismicio-types.d.ts")
31
+ -h, --help Show help for command
32
+
33
+ BLOCK TYPES
34
+ heading1, heading2, heading3, heading4, heading5, heading6,
35
+ paragraph, strong, em, preformatted, hyperlink, image, embed,
36
+ list-item, o-list-item, rtl
37
+
38
+ EXAMPLES
39
+ prismic slice add-field rich-text my_slice body
40
+ prismic slice add-field rich-text article content --multi "paragraph,heading2,heading3,strong,em,hyperlink"
41
+ prismic slice add-field rich-text hero tagline --single "heading1"
42
+ prismic slice add-field rich-text blog post --multi "paragraph,strong,em,hyperlink" --allow-target-blank
43
+ `.trim();
44
+
45
+ function getDocsPath(framework: Framework): string {
46
+ switch (framework) {
47
+ case "next":
48
+ return "nextjs/with-cli";
49
+ case "nuxt":
50
+ return "nuxt/with-cli";
51
+ case "sveltekit":
52
+ return "sveltekit/with-cli";
53
+ }
54
+ }
55
+
56
+ function getWriteComponentsAnchor(framework: Framework): string {
57
+ switch (framework) {
58
+ case "nuxt":
59
+ return "#write-vue-components";
60
+ case "sveltekit":
61
+ return "#write-svelte-components";
62
+ default:
63
+ return "#write-react-components";
64
+ }
65
+ }
66
+
67
+ export async function sliceAddFieldRichText(): Promise<void> {
68
+ const {
69
+ values: {
70
+ help,
71
+ variation,
72
+ label,
73
+ placeholder,
74
+ single,
75
+ multi,
76
+ "allow-target-blank": allowTargetBlank,
77
+ types,
78
+ },
79
+ positionals: [sliceId, fieldId],
80
+ } = parseArgs({
81
+ args: process.argv.slice(5), // skip: node, script, "slice", "add-field", "rich-text"
82
+ options: {
83
+ variation: { type: "string", short: "v" },
84
+ label: { type: "string", short: "l" },
85
+ placeholder: { type: "string", short: "p" },
86
+ single: { type: "string" },
87
+ multi: { type: "string" },
88
+ "allow-target-blank": { type: "boolean" },
89
+ types: { type: "string" },
90
+ help: { type: "boolean", short: "h" },
91
+ },
92
+ allowPositionals: true,
93
+ });
94
+
95
+ if (help) {
96
+ console.info(HELP);
97
+ return;
98
+ }
99
+
100
+ if (!sliceId) {
101
+ console.error("Missing required argument: slice-id\n");
102
+ console.error("Usage: prismic slice add-field rich-text <slice-id> <field-id>");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+
107
+ if (!fieldId) {
108
+ console.error("Missing required argument: field-id\n");
109
+ console.error("Usage: prismic slice add-field rich-text <slice-id> <field-id>");
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+
114
+ // Parse and validate field path
115
+ const fieldPath = parseFieldPath(fieldId);
116
+ const pathValidation = validateNestedFieldPath(fieldPath);
117
+ if (!pathValidation.ok) {
118
+ console.error(pathValidation.error);
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+
123
+ // Find the slice model
124
+ const result = await findSliceModel(sliceId);
125
+ if (!result.ok) {
126
+ console.error(result.error);
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+
131
+ const { model, modelPath } = result;
132
+
133
+ // Check for variations
134
+ if (model.variations.length === 0) {
135
+ console.error(`Slice "${sliceId}" has no variations.\n`);
136
+ console.error("Add a variation first before adding fields.");
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+
141
+ // Find target variation
142
+ const targetVariation = variation
143
+ ? model.variations.find((v) => v.id === variation)
144
+ : model.variations[0];
145
+
146
+ if (!targetVariation) {
147
+ console.error(`Variation "${variation}" not found in slice "${sliceId}"\n`);
148
+ console.error(`Available variations: ${model.variations.map((v) => v.id).join(", ")}`);
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+
153
+ // Initialize primary if it doesn't exist
154
+ if (!targetVariation.primary) {
155
+ targetVariation.primary = {};
156
+ }
157
+
158
+ // Build field definition
159
+ const fieldDefinition: RichText = {
160
+ type: "StructuredText",
161
+ config: {
162
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
163
+ ...(placeholder && { placeholder }),
164
+ ...(single && { single }),
165
+ ...(multi && { multi }),
166
+ ...(allowTargetBlank && { allowTargetBlank: true }),
167
+ },
168
+ };
169
+
170
+ // Add field to variation (with nested field support)
171
+ if (fieldPath.type === "nested") {
172
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
173
+ if (!groupResult.ok) {
174
+ console.error(groupResult.error);
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+ // Check if nested field already exists
179
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
180
+ console.error(
181
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
182
+ );
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
187
+ } else {
188
+ // Check if field already exists in any variation (at top level or in groups)
189
+ for (const v of model.variations) {
190
+ if (v.primary?.[fieldId]) {
191
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ // Also check inside groups
196
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
197
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
198
+ console.error(
199
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
200
+ );
201
+ process.exitCode = 1;
202
+ return;
203
+ }
204
+ }
205
+ }
206
+ targetVariation.primary[fieldId] = fieldDefinition;
207
+ }
208
+
209
+ // Write updated model
210
+ try {
211
+ await writeFile(modelPath, stringify(model as SharedSlice));
212
+ } catch (error) {
213
+ if (error instanceof Error) {
214
+ console.error(`Failed to update slice: ${error.message}`);
215
+ } else {
216
+ console.error("Failed to update slice");
217
+ }
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+
222
+ if (fieldPath.type === "nested") {
223
+ console.info(
224
+ `Added field "${fieldPath.nestedFieldId}" (StructuredText) to group "${fieldPath.groupId}" in ${sliceId}`,
225
+ );
226
+ } else {
227
+ console.info(
228
+ `Added field "${fieldId}" (StructuredText) to "${targetVariation.id}" variation in ${sliceId}`,
229
+ );
230
+ }
231
+
232
+ try {
233
+ await buildTypes({ output: types });
234
+ console.info(`Updated types in ${types ?? "prismicio-types.d.ts"}`);
235
+ } catch (error) {
236
+ console.warn(`Could not generate types: ${error instanceof Error ? error.message : error}`);
237
+ }
238
+
239
+ console.info();
240
+ console.info("Next: Add more fields with `prismic slice add-field`");
241
+
242
+ const frameworkInfo = await detectFrameworkInfo();
243
+ if (frameworkInfo?.framework) {
244
+ const docsPath = getDocsPath(frameworkInfo.framework);
245
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
246
+ console.info(
247
+ ` Run \`prismic docs fetch ${docsPath}${anchor}\` to learn how to implement the slice's component`,
248
+ );
249
+ }
250
+ }
@@ -0,0 +1,232 @@
1
+ import type { Select, 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 select (dropdown) field to an existing slice.
15
+
16
+ USAGE
17
+ prismic slice add-field select <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
+ --option string Add an option (can be used multiple times)
28
+ --default string Default selected value
29
+ --types string Output file for generated types (default: "prismicio-types.d.ts")
30
+ -h, --help Show help for command
31
+
32
+ EXAMPLES
33
+ prismic slice add-field select my_slice layout --option "full" --option "sidebar"
34
+ prismic slice add-field select hero style --option "light" --option "dark" --default "light"
35
+ prismic slice add-field select product size --option "small" --option "medium" --option "large" --label "Size"
36
+ `.trim();
37
+
38
+ function getDocsPath(framework: Framework): string {
39
+ switch (framework) {
40
+ case "next":
41
+ return "nextjs/with-cli";
42
+ case "nuxt":
43
+ return "nuxt/with-cli";
44
+ case "sveltekit":
45
+ return "sveltekit/with-cli";
46
+ }
47
+ }
48
+
49
+ function getWriteComponentsAnchor(framework: Framework): string {
50
+ switch (framework) {
51
+ case "nuxt":
52
+ return "#write-vue-components";
53
+ case "sveltekit":
54
+ return "#write-svelte-components";
55
+ default:
56
+ return "#write-react-components";
57
+ }
58
+ }
59
+
60
+ export async function sliceAddFieldSelect(): Promise<void> {
61
+ const {
62
+ values: { help, variation, label, placeholder, option, default: defaultValue, types },
63
+ positionals: [sliceId, fieldId],
64
+ } = parseArgs({
65
+ args: process.argv.slice(5), // skip: node, script, "slice", "add-field", "select"
66
+ options: {
67
+ variation: { type: "string", short: "v" },
68
+ label: { type: "string", short: "l" },
69
+ placeholder: { type: "string", short: "p" },
70
+ option: { type: "string", multiple: true },
71
+ default: { type: "string" },
72
+ types: { type: "string" },
73
+ help: { type: "boolean", short: "h" },
74
+ },
75
+ allowPositionals: true,
76
+ });
77
+
78
+ if (help) {
79
+ console.info(HELP);
80
+ return;
81
+ }
82
+
83
+ if (!sliceId) {
84
+ console.error("Missing required argument: slice-id\n");
85
+ console.error("Usage: prismic slice add-field select <slice-id> <field-id>");
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ if (!fieldId) {
91
+ console.error("Missing required argument: field-id\n");
92
+ console.error("Usage: prismic slice add-field select <slice-id> <field-id>");
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+
97
+ // Parse and validate field path
98
+ const fieldPath = parseFieldPath(fieldId);
99
+ const pathValidation = validateNestedFieldPath(fieldPath);
100
+ if (!pathValidation.ok) {
101
+ console.error(pathValidation.error);
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+
106
+ // Find the slice model
107
+ const result = await findSliceModel(sliceId);
108
+ if (!result.ok) {
109
+ console.error(result.error);
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+
114
+ const { model, modelPath } = result;
115
+
116
+ // Check for variations
117
+ if (model.variations.length === 0) {
118
+ console.error(`Slice "${sliceId}" has no variations.\n`);
119
+ console.error("Add a variation first before adding fields.");
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+
124
+ // Find target variation
125
+ const targetVariation = variation
126
+ ? model.variations.find((v) => v.id === variation)
127
+ : model.variations[0];
128
+
129
+ if (!targetVariation) {
130
+ console.error(`Variation "${variation}" not found in slice "${sliceId}"\n`);
131
+ console.error(`Available variations: ${model.variations.map((v) => v.id).join(", ")}`);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+
136
+ // Initialize primary if it doesn't exist
137
+ if (!targetVariation.primary) {
138
+ targetVariation.primary = {};
139
+ }
140
+
141
+ // Build field definition
142
+ const fieldDefinition: Select = {
143
+ type: "Select",
144
+ config: {
145
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
146
+ ...(placeholder && { placeholder }),
147
+ ...(option && option.length > 0 && { options: option }),
148
+ ...(defaultValue && { default_value: defaultValue }),
149
+ },
150
+ };
151
+
152
+ // Add field to variation (with nested field support)
153
+ if (fieldPath.type === "nested") {
154
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
155
+ if (!groupResult.ok) {
156
+ console.error(groupResult.error);
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ // Check if nested field already exists
161
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
162
+ console.error(
163
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
164
+ );
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
169
+ } else {
170
+ // Check if field already exists in any variation (at top level or in groups)
171
+ for (const v of model.variations) {
172
+ if (v.primary?.[fieldId]) {
173
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
174
+ process.exitCode = 1;
175
+ return;
176
+ }
177
+ // Also check inside groups
178
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
179
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
180
+ console.error(
181
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
182
+ );
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ }
187
+ }
188
+ targetVariation.primary[fieldId] = fieldDefinition;
189
+ }
190
+
191
+ // Write updated model
192
+ try {
193
+ await writeFile(modelPath, stringify(model as SharedSlice));
194
+ } catch (error) {
195
+ if (error instanceof Error) {
196
+ console.error(`Failed to update slice: ${error.message}`);
197
+ } else {
198
+ console.error("Failed to update slice");
199
+ }
200
+ process.exitCode = 1;
201
+ return;
202
+ }
203
+
204
+ if (fieldPath.type === "nested") {
205
+ console.info(
206
+ `Added field "${fieldPath.nestedFieldId}" (Select) to group "${fieldPath.groupId}" in ${sliceId}`,
207
+ );
208
+ } else {
209
+ console.info(
210
+ `Added field "${fieldId}" (Select) to "${targetVariation.id}" variation in ${sliceId}`,
211
+ );
212
+ }
213
+
214
+ try {
215
+ await buildTypes({ output: types });
216
+ console.info(`Updated types in ${types ?? "prismicio-types.d.ts"}`);
217
+ } catch (error) {
218
+ console.warn(`Could not generate types: ${error instanceof Error ? error.message : error}`);
219
+ }
220
+
221
+ console.info();
222
+ console.info("Next: Add more fields with `prismic slice add-field`");
223
+
224
+ const frameworkInfo = await detectFrameworkInfo();
225
+ if (frameworkInfo?.framework) {
226
+ const docsPath = getDocsPath(frameworkInfo.framework);
227
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
228
+ console.info(
229
+ ` Run \`prismic docs fetch ${docsPath}${anchor}\` to learn how to implement the slice's component`,
230
+ );
231
+ }
232
+ }