@angeloashmore/prismic-cli-poc 0.0.0-pr.8.b80fefa → 0.0.0-pr.9.5366ece

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 (58) hide show
  1. package/dist/index.mjs +302 -168
  2. package/package.json +1 -1
  3. package/src/custom-type-add-field-boolean.ts +49 -12
  4. package/src/custom-type-add-field-color.ts +46 -12
  5. package/src/custom-type-add-field-date.ts +46 -12
  6. package/src/custom-type-add-field-embed.ts +46 -12
  7. package/src/custom-type-add-field-geo-point.ts +46 -12
  8. package/src/custom-type-add-field-group.ts +179 -0
  9. package/src/custom-type-add-field-image.ts +46 -12
  10. package/src/custom-type-add-field-key-text.ts +46 -12
  11. package/src/custom-type-add-field-link.ts +46 -12
  12. package/src/custom-type-add-field-number.ts +46 -12
  13. package/src/custom-type-add-field-rich-text.ts +46 -12
  14. package/src/custom-type-add-field-select.ts +47 -21
  15. package/src/custom-type-add-field-timestamp.ts +46 -12
  16. package/src/custom-type-add-field-uid.ts +17 -0
  17. package/src/custom-type-add-field.ts +5 -0
  18. package/src/index.ts +5 -0
  19. package/src/lib/field-path.ts +81 -0
  20. package/src/page-type-add-field-boolean.ts +66 -13
  21. package/src/page-type-add-field-color.ts +66 -13
  22. package/src/page-type-add-field-date.ts +66 -13
  23. package/src/page-type-add-field-embed.ts +66 -13
  24. package/src/page-type-add-field-geo-point.ts +66 -13
  25. package/src/page-type-add-field-group.ts +198 -0
  26. package/src/page-type-add-field-image.ts +66 -13
  27. package/src/page-type-add-field-key-text.ts +66 -13
  28. package/src/page-type-add-field-link.ts +66 -13
  29. package/src/page-type-add-field-number.ts +66 -13
  30. package/src/page-type-add-field-rich-text.ts +66 -13
  31. package/src/page-type-add-field-select.ts +67 -22
  32. package/src/page-type-add-field-timestamp.ts +66 -13
  33. package/src/page-type-add-field-uid.ts +37 -1
  34. package/src/page-type-add-field.ts +5 -0
  35. package/src/page-type-create.ts +25 -0
  36. package/src/repo-create.ts +59 -0
  37. package/src/skill-install.ts +177 -0
  38. package/src/skill-uninstall.ts +85 -0
  39. package/src/skill.ts +50 -0
  40. package/src/slice-add-field-boolean.ts +90 -16
  41. package/src/slice-add-field-color.ts +90 -16
  42. package/src/slice-add-field-date.ts +90 -16
  43. package/src/slice-add-field-embed.ts +90 -16
  44. package/src/slice-add-field-geo-point.ts +90 -16
  45. package/src/slice-add-field-group.ts +191 -0
  46. package/src/slice-add-field-image.ts +90 -16
  47. package/src/slice-add-field-key-text.ts +90 -16
  48. package/src/slice-add-field-link.ts +90 -16
  49. package/src/slice-add-field-number.ts +90 -16
  50. package/src/slice-add-field-rich-text.ts +90 -16
  51. package/src/slice-add-field-select.ts +91 -25
  52. package/src/slice-add-field-timestamp.ts +90 -16
  53. package/src/slice-add-field.ts +5 -0
  54. package/src/slice-create.ts +66 -5
  55. package/src/slice-set-screenshot.ts +235 -0
  56. package/src/slice-view.ts +3 -0
  57. package/src/slice.ts +5 -0
  58. package/src/status.ts +164 -124
@@ -2,6 +2,7 @@ import { parseArgs } from "node:util";
2
2
 
3
3
  import { isAuthenticated, readHost } from "./lib/auth";
4
4
  import { createConfig, readConfig, updateConfig } from "./lib/config";
5
+ import { type Framework, detectFrameworkInfo, getClientFilePath } from "./lib/framework";
5
6
  import { stringify } from "./lib/json";
6
7
  import { ForbiddenRequestError, request } from "./lib/request";
7
8
  import { getRepoUrl } from "./lib/url";
@@ -31,6 +32,26 @@ LEARN MORE
31
32
 
32
33
  const DOMAIN_REGEX = /^[a-zA-Z0-9][-a-zA-Z0-9]{2,}[a-zA-Z0-9]$/;
33
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 getClientSetupAnchor(framework: Framework): string {
47
+ switch (framework) {
48
+ case "nuxt":
49
+ return "#configure-the-modules-prismic-client";
50
+ default:
51
+ return "#set-up-a-prismic-client";
52
+ }
53
+ }
54
+
34
55
  export async function repoCreate(): Promise<void> {
35
56
  const {
36
57
  values: { help, name, "no-config": noConfig, replace },
@@ -81,6 +102,23 @@ export async function repoCreate(): Promise<void> {
81
102
  return;
82
103
  }
83
104
 
105
+ // Check if domain is available
106
+ const available = await checkDomainAvailable(domain);
107
+ if (!available.ok) {
108
+ if (available.error instanceof ForbiddenRequestError) {
109
+ handleUnauthenticated();
110
+ } else {
111
+ console.error(`Failed to check domain availability: ${stringify(available.error)}`);
112
+ process.exitCode = 1;
113
+ }
114
+ return;
115
+ }
116
+ if (!available.value) {
117
+ console.error(`Repository name "${domain}" is already taken.`);
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+
84
122
  const response = await createRepository(domain, name);
85
123
  if (!response.ok) {
86
124
  if (response.error instanceof ForbiddenRequestError) {
@@ -113,6 +151,27 @@ export async function repoCreate(): Promise<void> {
113
151
 
114
152
  console.info(`Repository created: ${domain}`);
115
153
  console.info(`URL: ${await getRepoUrl(domain)}`);
154
+
155
+ // Print framework-specific next steps
156
+ const frameworkInfo = await detectFrameworkInfo();
157
+ if (frameworkInfo?.framework) {
158
+ const docsPath = getDocsPath(frameworkInfo.framework);
159
+ const anchor = getClientSetupAnchor(frameworkInfo.framework);
160
+ const clientFile = getClientFilePath(frameworkInfo);
161
+ const fileDesc = clientFile ? `creating ${clientFile}` : "configuring Prismic";
162
+ console.info();
163
+ console.info(`Next: Run \`prismic docs ${docsPath}${anchor}\` for instructions on ${fileDesc}`);
164
+ }
165
+ }
166
+
167
+ async function checkDomainAvailable(domain: string) {
168
+ const url = new URL(`/app/dashboard/repositories/${domain}/exists`, await readHost());
169
+ const response = await request<string>(url);
170
+ if (!response.ok) {
171
+ return response;
172
+ }
173
+ // Endpoint returns "false" when repository exists, "true" when available
174
+ return { ok: true as const, value: response.value === "true" };
116
175
  }
117
176
 
118
177
  async function createRepository(domain: string, name = domain) {
@@ -0,0 +1,177 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { parseArgs } from "node:util";
5
+
6
+ import { exists } from "./lib/file";
7
+ import { appendTrailingSlash } from "./lib/url";
8
+
9
+ const HELP = `
10
+ Install the Prismic skill into supported global AI tool skill directories.
11
+
12
+ USAGE
13
+ prismic skill install [flags]
14
+
15
+ FLAGS
16
+ -h, --help Show help for command
17
+
18
+ LEARN MORE
19
+ This command currently installs to global/user tool directories only.
20
+ `.trim();
21
+
22
+ const SKILL_ID = "prismic";
23
+ const SKILL_FILENAME = "SKILL.md";
24
+
25
+ export const PRISMIC_SKILL_TEMPLATE = `
26
+ ---
27
+ name: prismic
28
+ description: Use when a task involves Prismic setup, repository configuration, content modeling, content type or slice changes, localization, previews, API tokens, webhooks, syncing local models, or reading Prismic documentation.
29
+ allowed-tools: Bash(prismic *)
30
+ ---
31
+
32
+ # Prismic Workflows
33
+
34
+ For Prismic-related tasks, use the \`prismic\` CLI first.
35
+
36
+ 1. Discover capabilities with \`prismic --help\`.
37
+ 2. Inspect details with \`prismic <command> --help\`.
38
+ 3. Use \`prismic docs <path>\` when documentation is needed.
39
+ 4. Prefer CLI workflows over direct API/manual changes.
40
+ 5. If the CLI does not support a required operation, state that explicitly, then use the next-best fallback.
41
+ `.trim();
42
+
43
+ export type SkillInstallTarget = {
44
+ tool: string;
45
+ baseDir: URL;
46
+ skillsDir: URL;
47
+ skillDir: URL;
48
+ skillFile: URL;
49
+ };
50
+
51
+ export async function findGlobalSkillInstallTargets(config?: {
52
+ homeDir?: string;
53
+ }): Promise<SkillInstallTarget[]> {
54
+ const homeURL = appendTrailingSlash(pathToFileURL(config?.homeDir ?? homedir()));
55
+ const codexBaseURL = new URL(".codex/", homeURL);
56
+
57
+ const candidates: SkillInstallTarget[] = [
58
+ createTarget("Claude", new URL(".claude/", homeURL), new URL(".claude/skills/", homeURL)),
59
+ createTarget("Cursor", new URL(".cursor/", homeURL), new URL(".cursor/skills/", homeURL)),
60
+ createTarget("Gemini", new URL(".gemini/", homeURL), new URL(".gemini/skills/", homeURL)),
61
+ createTarget(
62
+ "OpenCode",
63
+ new URL(".config/opencode/", homeURL),
64
+ new URL(".config/opencode/skill/", homeURL),
65
+ ),
66
+ createTarget("Codex", codexBaseURL, new URL("skills/", codexBaseURL)),
67
+ ];
68
+
69
+ const targets: SkillInstallTarget[] = [];
70
+ for (const target of candidates) {
71
+ if (await exists(target.baseDir)) {
72
+ targets.push(target);
73
+ }
74
+ }
75
+
76
+ return targets;
77
+ }
78
+
79
+ export async function skillInstall(): Promise<void> {
80
+ const {
81
+ values: { help },
82
+ } = parseArgs({
83
+ args: process.argv.slice(4), // skip: node, script, "skill", "install"
84
+ options: {
85
+ help: { type: "boolean", short: "h" },
86
+ },
87
+ allowPositionals: true,
88
+ strict: false,
89
+ });
90
+
91
+ if (help) {
92
+ console.info(HELP);
93
+ return;
94
+ }
95
+
96
+ const targets = await findGlobalSkillInstallTargets();
97
+
98
+ if (targets.length === 0) {
99
+ console.error(
100
+ "No supported global AI tool directories were detected (Claude, Cursor, Gemini, OpenCode, Codex).",
101
+ );
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+
106
+ const conflicts: SkillInstallTarget[] = [];
107
+ conflicts.push(...(await findExistingSkillConflicts(targets)));
108
+
109
+ if (conflicts.length > 0) {
110
+ console.error("Skill already exists in one or more targets:");
111
+ for (const conflict of conflicts) {
112
+ console.error(`- ${fileURLToPath(conflict.skillFile)}`);
113
+ }
114
+ console.error("Nothing was installed. Remove existing files and retry.");
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+
119
+ const installResult = await installSkillTemplateToTargets(PRISMIC_SKILL_TEMPLATE, targets);
120
+ if (!installResult.ok) {
121
+ console.error(`Failed to install skill for ${installResult.target.tool}: ${installResult.error}`);
122
+ console.error(`Path: ${fileURLToPath(installResult.target.skillFile)}`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ const installedPaths = installResult.installedPaths;
127
+
128
+ for (const installedPath of installedPaths) {
129
+ console.info(`Installed: ${installedPath}`);
130
+ }
131
+ console.info(`Installed ${installedPaths.length} Prismic skill file(s).`);
132
+ }
133
+
134
+ export async function findExistingSkillConflicts(
135
+ targets: SkillInstallTarget[],
136
+ ): Promise<SkillInstallTarget[]> {
137
+ const conflicts: SkillInstallTarget[] = [];
138
+ for (const target of targets) {
139
+ if (await exists(target.skillFile)) {
140
+ conflicts.push(target);
141
+ }
142
+ }
143
+ return conflicts;
144
+ }
145
+
146
+ export async function installSkillTemplateToTargets(
147
+ template: string,
148
+ targets: SkillInstallTarget[],
149
+ ): Promise<
150
+ | { ok: true; installedPaths: string[] }
151
+ | { ok: false; target: SkillInstallTarget; error: string }
152
+ > {
153
+ const installedPaths: string[] = [];
154
+ for (const target of targets) {
155
+ try {
156
+ await mkdir(target.skillDir, { recursive: true });
157
+ await writeFile(target.skillFile, template);
158
+ installedPaths.push(fileURLToPath(target.skillFile));
159
+ } catch (error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ return { ok: false, target, error: message };
162
+ }
163
+ }
164
+
165
+ return { ok: true, installedPaths };
166
+ }
167
+
168
+ function createTarget(tool: string, baseDir: URL, skillsDir: URL): SkillInstallTarget {
169
+ const skillDir = new URL(`${SKILL_ID}/`, skillsDir);
170
+ return {
171
+ tool,
172
+ baseDir,
173
+ skillsDir,
174
+ skillDir,
175
+ skillFile: new URL(SKILL_FILENAME, skillDir),
176
+ };
177
+ }
@@ -0,0 +1,85 @@
1
+ import { rm, rmdir } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { parseArgs } from "node:util";
4
+
5
+ import { exists } from "./lib/file";
6
+ import { findGlobalSkillInstallTargets } from "./skill-install";
7
+
8
+ const HELP = `
9
+ Uninstall the Prismic skill from supported global AI tool skill directories.
10
+
11
+ USAGE
12
+ prismic skill uninstall [flags]
13
+
14
+ FLAGS
15
+ -h, --help Show help for command
16
+
17
+ LEARN MORE
18
+ This command currently uninstalls from global/user tool directories only.
19
+ `.trim();
20
+
21
+ export async function skillUninstall(): Promise<void> {
22
+ const {
23
+ values: { help },
24
+ } = parseArgs({
25
+ args: process.argv.slice(4), // skip: node, script, "skill", "uninstall"
26
+ options: {
27
+ help: { type: "boolean", short: "h" },
28
+ },
29
+ allowPositionals: true,
30
+ strict: false,
31
+ });
32
+
33
+ if (help) {
34
+ console.info(HELP);
35
+ return;
36
+ }
37
+
38
+ const targets = await findGlobalSkillInstallTargets();
39
+
40
+ const removedSkillFiles: string[] = [];
41
+
42
+ for (const target of targets) {
43
+ if (!(await exists(target.skillFile))) {
44
+ continue;
45
+ }
46
+
47
+ try {
48
+ await rm(target.skillFile);
49
+ removedSkillFiles.push(fileURLToPath(target.skillFile));
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ console.error(`Failed to remove skill for ${target.tool}: ${message}`);
53
+ console.error(`Path: ${fileURLToPath(target.skillFile)}`);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+
58
+ try {
59
+ await rmdir(target.skillDir);
60
+ } catch (error) {
61
+ if (error && typeof error === "object" && "code" in error) {
62
+ const code = String(error.code);
63
+ if (code === "ENOTEMPTY" || code === "ENOENT") {
64
+ continue;
65
+ }
66
+ }
67
+
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ console.error(`Failed to clean up skill directory for ${target.tool}: ${message}`);
70
+ console.error(`Path: ${fileURLToPath(target.skillDir)}`);
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ }
75
+
76
+ if (removedSkillFiles.length === 0) {
77
+ console.info("No Prismic skill installation found. Nothing to uninstall.");
78
+ return;
79
+ }
80
+
81
+ for (const removedSkillFile of removedSkillFiles) {
82
+ console.info(`Removed: ${removedSkillFile}`);
83
+ }
84
+ console.info(`Uninstalled ${removedSkillFiles.length} Prismic skill file(s).`);
85
+ }
package/src/skill.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { skillInstall } from "./skill-install";
4
+ import { skillUninstall } from "./skill-uninstall";
5
+
6
+ const HELP = `
7
+ Manage Prismic skills in supported AI tool directories.
8
+
9
+ USAGE
10
+ prismic skill <command> [flags]
11
+
12
+ COMMANDS
13
+ install Install the Prismic skill into detected global skill directories
14
+ uninstall Uninstall the Prismic skill from detected global skill directories
15
+
16
+ FLAGS
17
+ -h, --help Show help for command
18
+
19
+ LEARN MORE
20
+ Use \`prismic skill <command> --help\` for more information about a command.
21
+ `.trim();
22
+
23
+ export async function skill(): Promise<void> {
24
+ const {
25
+ positionals: [subcommand],
26
+ } = parseArgs({
27
+ args: process.argv.slice(3), // skip: node, script, "skill"
28
+ options: {
29
+ help: { type: "boolean", short: "h" },
30
+ },
31
+ allowPositionals: true,
32
+ strict: false,
33
+ });
34
+
35
+ switch (subcommand) {
36
+ case "install":
37
+ await skillInstall();
38
+ break;
39
+ case "uninstall":
40
+ await skillUninstall();
41
+ break;
42
+ default: {
43
+ if (subcommand) {
44
+ console.error(`Unknown skill subcommand: ${subcommand}\n`);
45
+ process.exitCode = 1;
46
+ }
47
+ console.info(HELP);
48
+ }
49
+ }
50
+ }
@@ -4,6 +4,8 @@ import { writeFile } from "node:fs/promises";
4
4
  import { parseArgs } from "node:util";
5
5
 
6
6
  import { buildTypes } from "./codegen-types";
7
+ import { findGroupInVariation, isGroupField, parseFieldPath, validateNestedFieldPath } from "./lib/field-path";
8
+ import { type Framework, detectFrameworkInfo } from "./lib/framework";
7
9
  import { stringify } from "./lib/json";
8
10
  import { findSliceModel } from "./lib/slice";
9
11
  import { humanReadable } from "./lib/string";
@@ -33,6 +35,28 @@ EXAMPLES
33
35
  prismic slice add-field boolean product available --true-label "In Stock" --false-label "Out of Stock"
34
36
  `.trim();
35
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
+
36
60
  export async function sliceAddFieldBoolean(): Promise<void> {
37
61
  const {
38
62
  values: {
@@ -78,6 +102,15 @@ export async function sliceAddFieldBoolean(): Promise<void> {
78
102
  return;
79
103
  }
80
104
 
105
+ // Parse and validate field path
106
+ const fieldPath = parseFieldPath(fieldId);
107
+ const pathValidation = validateNestedFieldPath(fieldPath);
108
+ if (!pathValidation.ok) {
109
+ console.error(pathValidation.error);
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+
81
114
  // Find the slice model
82
115
  const result = await findSliceModel(sliceId);
83
116
  if (!result.ok) {
@@ -113,28 +146,55 @@ export async function sliceAddFieldBoolean(): Promise<void> {
113
146
  targetVariation.primary = {};
114
147
  }
115
148
 
116
- // Check if field already exists in any variation
117
- for (const v of model.variations) {
118
- if (v.primary?.[fieldId]) {
119
- console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
120
- process.exitCode = 1;
121
- return;
122
- }
123
- }
124
-
125
149
  // Build field definition
126
150
  const fieldDefinition: BooleanField = {
127
151
  type: "Boolean",
128
152
  config: {
129
- label: label ?? humanReadable(fieldId),
153
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
130
154
  ...(defaultValue && { default_value: true }),
131
155
  ...(trueLabel && { placeholder_true: trueLabel }),
132
156
  ...(falseLabel && { placeholder_false: falseLabel }),
133
157
  },
134
158
  };
135
159
 
136
- // Add field to variation
137
- targetVariation.primary[fieldId] = fieldDefinition;
160
+ // Add field to variation (with nested field support)
161
+ if (fieldPath.type === "nested") {
162
+ const groupResult = findGroupInVariation(targetVariation.primary, fieldPath.groupId, targetVariation.id);
163
+ if (!groupResult.ok) {
164
+ console.error(groupResult.error);
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ // Check if nested field already exists
169
+ if (groupResult.group.config.fields[fieldPath.nestedFieldId]) {
170
+ console.error(
171
+ `Field "${fieldPath.nestedFieldId}" already exists in group "${fieldPath.groupId}"`,
172
+ );
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ groupResult.group.config.fields[fieldPath.nestedFieldId] = fieldDefinition;
177
+ } else {
178
+ // Check if field already exists in any variation (at top level or in groups)
179
+ for (const v of model.variations) {
180
+ if (v.primary?.[fieldId]) {
181
+ console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
182
+ process.exitCode = 1;
183
+ return;
184
+ }
185
+ // Also check inside groups
186
+ for (const [groupFieldId, groupField] of Object.entries(v.primary ?? {})) {
187
+ if (isGroupField(groupField) && groupField.config.fields[fieldId]) {
188
+ console.error(
189
+ `Field "${fieldId}" already exists in group "${groupFieldId}" in variation "${v.id}"`,
190
+ );
191
+ process.exitCode = 1;
192
+ return;
193
+ }
194
+ }
195
+ }
196
+ targetVariation.primary[fieldId] = fieldDefinition;
197
+ }
138
198
 
139
199
  // Write updated model
140
200
  try {
@@ -149,9 +209,15 @@ export async function sliceAddFieldBoolean(): Promise<void> {
149
209
  return;
150
210
  }
151
211
 
152
- console.info(
153
- `Added field "${fieldId}" (Boolean) to "${targetVariation.id}" variation in ${sliceId}`,
154
- );
212
+ if (fieldPath.type === "nested") {
213
+ console.info(
214
+ `Added field "${fieldPath.nestedFieldId}" (Boolean) to group "${fieldPath.groupId}" in ${sliceId}`,
215
+ );
216
+ } else {
217
+ console.info(
218
+ `Added field "${fieldId}" (Boolean) to "${targetVariation.id}" variation in ${sliceId}`,
219
+ );
220
+ }
155
221
 
156
222
  try {
157
223
  await buildTypes({ output: types });
@@ -162,5 +228,13 @@ export async function sliceAddFieldBoolean(): Promise<void> {
162
228
 
163
229
  console.info();
164
230
  console.info("Next: Add more fields with `prismic slice add-field`");
165
- console.info(" Run `prismic status` when done to find next steps");
231
+
232
+ const frameworkInfo = await detectFrameworkInfo();
233
+ if (frameworkInfo?.framework) {
234
+ const docsPath = getDocsPath(frameworkInfo.framework);
235
+ const anchor = getWriteComponentsAnchor(frameworkInfo.framework);
236
+ console.info(
237
+ ` Run \`prismic docs ${docsPath}${anchor}\` to learn how to implement the slice's component`,
238
+ );
239
+ }
166
240
  }
@@ -4,6 +4,8 @@ import { writeFile } from "node:fs/promises";
4
4
  import { parseArgs } from "node:util";
5
5
 
6
6
  import { buildTypes } from "./codegen-types";
7
+ import { findGroupInVariation, isGroupField, parseFieldPath, validateNestedFieldPath } from "./lib/field-path";
8
+ import { type Framework, detectFrameworkInfo } from "./lib/framework";
7
9
  import { stringify } from "./lib/json";
8
10
  import { findSliceModel } from "./lib/slice";
9
11
  import { humanReadable } from "./lib/string";
@@ -31,6 +33,28 @@ EXAMPLES
31
33
  prismic slice add-field color banner theme_color --variation "dark"
32
34
  `.trim();
33
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
+
34
58
  export async function sliceAddFieldColor(): Promise<void> {
35
59
  const {
36
60
  values: { help, variation, label, placeholder, types },
@@ -66,6 +90,15 @@ export async function sliceAddFieldColor(): Promise<void> {
66
90
  return;
67
91
  }
68
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
+
69
102
  // Find the slice model
70
103
  const result = await findSliceModel(sliceId);
71
104
  if (!result.ok) {
@@ -101,26 +134,53 @@ export async function sliceAddFieldColor(): Promise<void> {
101
134
  targetVariation.primary = {};
102
135
  }
103
136
 
104
- // Check if field already exists in any variation
105
- for (const v of model.variations) {
106
- if (v.primary?.[fieldId]) {
107
- console.error(`Field "${fieldId}" already exists in variation "${v.id}"`);
108
- process.exitCode = 1;
109
- return;
110
- }
111
- }
112
-
113
137
  // Build field definition
114
138
  const fieldDefinition: Color = {
115
139
  type: "Color",
116
140
  config: {
117
- label: label ?? humanReadable(fieldId),
141
+ label: label ?? humanReadable(fieldPath.type === "nested" ? fieldPath.nestedFieldId : fieldId),
118
142
  ...(placeholder && { placeholder }),
119
143
  },
120
144
  };
121
145
 
122
- // Add field to variation
123
- targetVariation.primary[fieldId] = fieldDefinition;
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
+ }
124
184
 
125
185
  // Write updated model
126
186
  try {
@@ -135,9 +195,15 @@ export async function sliceAddFieldColor(): Promise<void> {
135
195
  return;
136
196
  }
137
197
 
138
- console.info(
139
- `Added field "${fieldId}" (Color) to "${targetVariation.id}" variation in ${sliceId}`,
140
- );
198
+ if (fieldPath.type === "nested") {
199
+ console.info(
200
+ `Added field "${fieldPath.nestedFieldId}" (Color) to group "${fieldPath.groupId}" in ${sliceId}`,
201
+ );
202
+ } else {
203
+ console.info(
204
+ `Added field "${fieldId}" (Color) to "${targetVariation.id}" variation in ${sliceId}`,
205
+ );
206
+ }
141
207
 
142
208
  try {
143
209
  await buildTypes({ output: types });
@@ -148,5 +214,13 @@ export async function sliceAddFieldColor(): Promise<void> {
148
214
 
149
215
  console.info();
150
216
  console.info("Next: Add more fields with `prismic slice add-field`");
151
- console.info(" Run `prismic status` when done to find next steps");
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 ${docsPath}${anchor}\` to learn how to implement the slice's component`,
224
+ );
225
+ }
152
226
  }