From bdbaa1e87903aae545aa4bc0bb6441942c633b5e Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 16:10:56 -1000 Subject: [PATCH 01/29] feat: add remote modeling commands for custom types, page types, and slices Co-Authored-By: Claude Opus 4.6 (1M context) --- src/clients/custom-types.ts | 82 ++++++++++++++++++++++++++ src/commands/custom-type-create.ts | 53 +++++++++++++++++ src/commands/custom-type-list.ts | 39 ++++++++++++ src/commands/custom-type-remove.ts | 48 +++++++++++++++ src/commands/custom-type-view.ts | 48 +++++++++++++++ src/commands/custom-type.ts | 29 +++++++++ src/commands/page-type-create.ts | 77 ++++++++++++++++++++++++ src/commands/page-type-list.ts | 39 ++++++++++++ src/commands/page-type-remove.ts | 48 +++++++++++++++ src/commands/page-type-view.ts | 48 +++++++++++++++ src/commands/page-type.ts | 29 +++++++++ src/commands/slice-add-variation.ts | 68 +++++++++++++++++++++ src/commands/slice-connect.ts | 81 +++++++++++++++++++++++++ src/commands/slice-create.ts | 58 ++++++++++++++++++ src/commands/slice-disconnect.ts | 78 ++++++++++++++++++++++++ src/commands/slice-list.ts | 38 ++++++++++++ src/commands/slice-remove-variation.ts | 56 ++++++++++++++++++ src/commands/slice-remove.ts | 42 +++++++++++++ src/commands/slice-view.ts | 41 +++++++++++++ src/commands/slice.ts | 49 +++++++++++++++ src/index.ts | 15 +++++ 21 files changed, 1066 insertions(+) create mode 100644 src/commands/custom-type-create.ts create mode 100644 src/commands/custom-type-list.ts create mode 100644 src/commands/custom-type-remove.ts create mode 100644 src/commands/custom-type-view.ts create mode 100644 src/commands/custom-type.ts create mode 100644 src/commands/page-type-create.ts create mode 100644 src/commands/page-type-list.ts create mode 100644 src/commands/page-type-remove.ts create mode 100644 src/commands/page-type-view.ts create mode 100644 src/commands/page-type.ts create mode 100644 src/commands/slice-add-variation.ts create mode 100644 src/commands/slice-connect.ts create mode 100644 src/commands/slice-create.ts create mode 100644 src/commands/slice-disconnect.ts create mode 100644 src/commands/slice-list.ts create mode 100644 src/commands/slice-remove-variation.ts create mode 100644 src/commands/slice-remove.ts create mode 100644 src/commands/slice-view.ts create mode 100644 src/commands/slice.ts diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index 54db858..98ddeae 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -16,6 +16,47 @@ export async function getCustomTypes(config: { return response; } +export async function insertCustomType( + model: CustomType, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL("customtypes", customTypesServiceUrl); + await request(url, { + method: "POST", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + body: model, + }); +} + +export async function updateCustomType( + model: CustomType, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`customtypes/${encodeURIComponent(model.id)}`, customTypesServiceUrl); + await request(url, { + method: "PUT", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + body: model, + }); +} + +export async function removeCustomType( + id: string, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`customtypes/${encodeURIComponent(id)}`, customTypesServiceUrl); + await request(url, { + method: "DELETE", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + }); +} + export async function getSlices(config: { repo: string; token: string | undefined; @@ -30,6 +71,47 @@ export async function getSlices(config: { return response; } +export async function insertSlice( + model: SharedSlice, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL("slices", customTypesServiceUrl); + await request(url, { + method: "POST", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + body: model, + }); +} + +export async function updateSlice( + model: SharedSlice, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`slices/${encodeURIComponent(model.id)}`, customTypesServiceUrl); + await request(url, { + method: "PUT", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + body: model, + }); +} + +export async function removeSlice( + id: string, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`slices/${encodeURIComponent(id)}`, customTypesServiceUrl); + await request(url, { + method: "DELETE", + headers: { repository: repo, Authorization: `Bearer ${token}` }, + }); +} + function getCustomTypesServiceUrl(host: string): URL { return new URL(`https://customtypes.${host}/`); } diff --git a/src/commands/custom-type-create.ts b/src/commands/custom-type-create.ts new file mode 100644 index 0000000..dd1e6d0 --- /dev/null +++ b/src/commands/custom-type-create.ts @@ -0,0 +1,53 @@ +import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; + +import { snakeCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { insertCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic custom-type create", + description: "Create a new custom type.", + positionals: { + name: { description: "Name of the custom type", required: true }, + }, + options: { + single: { type: "boolean", short: "s", description: "Allow only one of this type" }, + id: { type: "string", description: "Custom ID for the custom type" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { single = false, id = snakeCase(name), repo = await getRepositoryName() } = values; + + const model: CustomType = { + id, + label: name, + repeatable: !single, + status: true, + format: "custom", + json: { + Main: {}, + }, + }; + + const token = await getToken(); + const host = await getHost(); + + try { + await insertCustomType(model, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to create custom type: ${message}`); + } + throw error; + } + + console.info(`Created custom type "${name}" (id: "${id}")`); +}); diff --git a/src/commands/custom-type-list.ts b/src/commands/custom-type-list.ts new file mode 100644 index 0000000..8dc41cc --- /dev/null +++ b/src/commands/custom-type-list.ts @@ -0,0 +1,39 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes } from "../clients/custom-types"; +import { createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic custom-type list", + description: "List all custom types.", + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ values }) => { + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + + const customTypes = await getCustomTypes({ repo, token, host }); + const types = customTypes.filter((customType) => customType.format !== "page"); + + if (json) { + console.info(stringify(types)); + return; + } + + if (types.length === 0) { + console.info("No custom types found."); + return; + } + + for (const type of types) { + const label = type.label || "(no name)"; + console.info(`${label} (id: ${type.id})`); + } +}); diff --git a/src/commands/custom-type-remove.ts b/src/commands/custom-type-remove.ts new file mode 100644 index 0000000..e5154ab --- /dev/null +++ b/src/commands/custom-type-remove.ts @@ -0,0 +1,48 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes, removeCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic custom-type remove", + description: "Remove a custom type.", + positionals: { + name: { description: "Name of the custom type", required: true }, + }, + options: { + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const customType = customTypes.find((ct) => ct.label === name); + + if (!customType) { + throw new CommandError(`Custom type not found: ${name}`); + } + + if (customType.format === "page") { + throw new CommandError( + `"${name}" is not a custom type. Use \`prismic page-type remove\` instead.`, + ); + } + + try { + await removeCustomType(customType.id, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to remove custom type: ${message}`); + } + throw error; + } + + console.info(`Custom type removed: "${name}" (id: ${customType.id})`); +}); diff --git a/src/commands/custom-type-view.ts b/src/commands/custom-type-view.ts new file mode 100644 index 0000000..40ef025 --- /dev/null +++ b/src/commands/custom-type-view.ts @@ -0,0 +1,48 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic custom-type view", + description: "View details of a custom type.", + positionals: { + name: { description: "Name of the custom type", required: true }, + }, + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const customType = customTypes.find((ct) => ct.label === name); + + if (!customType) { + throw new CommandError(`Custom type not found: ${name}`); + } + + if (customType.format === "page") { + throw new CommandError( + `"${name}" is not a custom type. Use \`prismic page-type view\` instead.`, + ); + } + + if (json) { + console.info(stringify(customType)); + return; + } + + console.info(`ID: ${customType.id}`); + console.info(`Name: ${customType.label || "(no name)"}`); + console.info(`Repeatable: ${customType.repeatable}`); + const tabs = Object.keys(customType.json).join(", ") || "(none)"; + console.info(`Tabs: ${tabs}`); +}); diff --git a/src/commands/custom-type.ts b/src/commands/custom-type.ts new file mode 100644 index 0000000..64119ce --- /dev/null +++ b/src/commands/custom-type.ts @@ -0,0 +1,29 @@ +import { createCommandRouter } from "../lib/command"; + +import customTypeCreate from "./custom-type-create"; +import customTypeList from "./custom-type-list"; +import customTypeRemove from "./custom-type-remove"; +import customTypeView from "./custom-type-view"; + +export default createCommandRouter({ + name: "prismic custom-type", + description: "Manage custom types.", + commands: { + create: { + handler: customTypeCreate, + description: "Create a new custom type", + }, + remove: { + handler: customTypeRemove, + description: "Remove a custom type", + }, + list: { + handler: customTypeList, + description: "List custom types", + }, + view: { + handler: customTypeView, + description: "View a custom type", + }, + }, +}); diff --git a/src/commands/page-type-create.ts b/src/commands/page-type-create.ts new file mode 100644 index 0000000..80bbf24 --- /dev/null +++ b/src/commands/page-type-create.ts @@ -0,0 +1,77 @@ +import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; + +import { snakeCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { insertCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic page-type create", + description: "Create a new page type.", + positionals: { + name: { description: "Name of the page type", required: true }, + }, + options: { + single: { type: "boolean", short: "s", description: "Allow only page one of this type" }, + id: { type: "string", description: "Custom ID for the page type" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { single = false, id = snakeCase(name), repo = await getRepositoryName() } = values; + + const model: CustomType = { + id, + label: name, + repeatable: !single, + status: true, + format: "page", + json: { + Main: {}, + "SEO & Metadata": { + meta_title: { + type: "Text", + config: { + label: "Meta Title", + placeholder: "A title of the page used for social media and search engines", + }, + }, + meta_description: { + type: "Text", + config: { + label: "Meta Description", + placeholder: "A brief summary of the page", + }, + }, + meta_image: { + type: "Image", + config: { + label: "Meta Image", + constraint: { width: 2400, height: 1260 }, + thumbnails: [], + }, + }, + }, + }, + }; + + const token = await getToken(); + const host = await getHost(); + + try { + await insertCustomType(model, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to create page type: ${message}`); + } + throw error; + } + + console.info(`Created page type "${name}" (id: "${id}")`); +}); diff --git a/src/commands/page-type-list.ts b/src/commands/page-type-list.ts new file mode 100644 index 0000000..d0ac691 --- /dev/null +++ b/src/commands/page-type-list.ts @@ -0,0 +1,39 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes } from "../clients/custom-types"; +import { createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic page-type list", + description: "List all page types.", + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ values }) => { + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + + const customTypes = await getCustomTypes({ repo, token, host }); + const pageTypes = customTypes.filter((customType) => customType.format === "page"); + + if (json) { + console.info(stringify(pageTypes)); + return; + } + + if (pageTypes.length === 0) { + console.info("No page types found."); + return; + } + + for (const pageType of pageTypes) { + const label = pageType.label || "(no name)"; + console.info(`${label} (id: ${pageType.id})`); + } +}); diff --git a/src/commands/page-type-remove.ts b/src/commands/page-type-remove.ts new file mode 100644 index 0000000..8c6ab5c --- /dev/null +++ b/src/commands/page-type-remove.ts @@ -0,0 +1,48 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes, removeCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic page-type remove", + description: "Remove a page type.", + positionals: { + name: { description: "Name of the page type", required: true }, + }, + options: { + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const pageType = customTypes.find((ct) => ct.label === name); + + if (!pageType) { + throw new CommandError(`Page type not found: ${name}`); + } + + if (pageType.format !== "page") { + throw new CommandError( + `"${name}" is not a page type. Use \`prismic custom-type remove\` instead.`, + ); + } + + try { + await removeCustomType(pageType.id, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to create page type: ${message}`); + } + throw error; + } + + console.info(`Page type removed: "${name}" (id: ${pageType.id})`); +}); diff --git a/src/commands/page-type-view.ts b/src/commands/page-type-view.ts new file mode 100644 index 0000000..1f0dd52 --- /dev/null +++ b/src/commands/page-type-view.ts @@ -0,0 +1,48 @@ +import { getHost, getToken } from "../auth"; +import { getCustomTypes } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic page-type view", + description: "View details of a page type.", + positionals: { + name: { description: "Name of the page type", required: true }, + }, + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const pageType = customTypes.find((ct) => ct.label === name); + + if (!pageType) { + throw new CommandError(`Page type not found: ${name}`); + } + + if (pageType.format !== "page") { + throw new CommandError( + `"${name}" is not a page type. Use \`prismic custom-type view\` instead.`, + ); + } + + if (json) { + console.info(stringify(pageType)); + return; + } + + console.info(`ID: ${pageType.id}`); + console.info(`Name: ${pageType.label || "(no name)"}`); + console.info(`Repeatable: ${pageType.repeatable}`); + const tabs = Object.keys(pageType.json).join(", ") || "(none)"; + console.info(`Tabs: ${tabs}`); +}); diff --git a/src/commands/page-type.ts b/src/commands/page-type.ts new file mode 100644 index 0000000..ef8f57b --- /dev/null +++ b/src/commands/page-type.ts @@ -0,0 +1,29 @@ +import { createCommandRouter } from "../lib/command"; + +import pageTypeCreate from "./page-type-create"; +import pageTypeList from "./page-type-list"; +import pageTypeRemove from "./page-type-remove"; +import pageTypeView from "./page-type-view"; + +export default createCommandRouter({ + name: "prismic page-type", + description: "Manage page types.", + commands: { + create: { + handler: pageTypeCreate, + description: "Create a new page type", + }, + remove: { + handler: pageTypeRemove, + description: "Remove a page type", + }, + list: { + handler: pageTypeList, + description: "List page types", + }, + view: { + handler: pageTypeView, + description: "View a page type", + }, + }, +}); diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts new file mode 100644 index 0000000..ad1b86e --- /dev/null +++ b/src/commands/slice-add-variation.ts @@ -0,0 +1,68 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { camelCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { getSlices, updateSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice add-variation", + description: "Add a variation to a slice.", + positionals: { + name: { description: "Name of the variation", required: true }, + }, + options: { + to: { type: "string", required: true, description: "Name of the slice" }, + id: { type: "string", description: "Custom ID for the variation" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { to, id = camelCase(name), repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((s) => s.name === to); + + if (!slice) { + throw new CommandError(`Slice not found: ${to}`); + } + + if (slice.variations.some((v) => v.id === id)) { + throw new CommandError(`Variation "${id}" already exists in slice "${to}".`); + } + + const updatedSlice: SharedSlice = { + ...slice, + variations: [ + ...slice.variations, + { + id, + name, + description: name, + docURL: "", + imageUrl: "", + version: "", + primary: {}, + }, + ], + }; + + try { + await updateSlice(updatedSlice, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to add variation: ${message}`); + } + throw error; + } + + console.info(`Added variation "${name}" (id: "${id}") to slice "${to}"`); +}); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts new file mode 100644 index 0000000..301290f --- /dev/null +++ b/src/commands/slice-connect.ts @@ -0,0 +1,81 @@ +import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; + +import { getHost, getToken } from "../auth"; +import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice connect", + description: "Connect a slice to a type's slice zone.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + to: { + type: "string", + required: true, + description: "Name of the page type or custom type", + }, + "slice-zone": { + type: "string", + description: 'Slice zone field ID (default: "slices")', + }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { to, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const apiConfig = { repo, token, host }; + + const slices = await getSlices(apiConfig); + const slice = slices.find((s) => s.name === name); + if (!slice) { + throw new CommandError(`Slice not found: ${name}`); + } + + const customTypes = await getCustomTypes(apiConfig); + const customType = customTypes.find((ct) => ct.label === to); + if (!customType) { + throw new CommandError(`Type not found: ${to}`); + } + + const allFields: Record = Object.assign( + {}, + ...Object.values(customType.json), + ); + + const sliceZoneField = allFields[sliceZone]; + if (!sliceZoneField || sliceZoneField.type !== "Slices") { + throw new CommandError(`Slice zone "${sliceZone}" not found in "${to}".`); + } + + sliceZoneField.config ??= {}; + sliceZoneField.config.choices ??= {}; + + if (slice.id in sliceZoneField.config.choices) { + throw new CommandError( + `Slice "${slice.id}" is already connected to "${to}" in slice zone "${sliceZone}".`, + ); + } + + sliceZoneField.config.choices[slice.id] = { type: "SharedSlice" }; + + try { + await updateCustomType(customType, apiConfig); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to connect slice: ${message}`); + } + throw error; + } + + console.info(`Connected slice "${name}" to "${to}"`); +}); diff --git a/src/commands/slice-create.ts b/src/commands/slice-create.ts new file mode 100644 index 0000000..6191d04 --- /dev/null +++ b/src/commands/slice-create.ts @@ -0,0 +1,58 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { snakeCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { insertSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice create", + description: "Create a new slice.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + id: { type: "string", description: "Custom ID for the slice" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { id = snakeCase(name), repo = await getRepositoryName() } = values; + + const model: SharedSlice = { + id, + name, + type: "SharedSlice", + variations: [ + { + id: "default", + name: "Default", + description: "Default", + docURL: "", + imageUrl: "", + version: "", + primary: {}, + }, + ], + }; + + const token = await getToken(); + const host = await getHost(); + + try { + await insertSlice(model, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to create page type: ${message}`); + } + throw error; + } + + console.info(`Created page type "${name}" (id: "${id}")`); +}); diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts new file mode 100644 index 0000000..aaa786f --- /dev/null +++ b/src/commands/slice-disconnect.ts @@ -0,0 +1,78 @@ +import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; + +import { getHost, getToken } from "../auth"; +import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice disconnect", + description: "Disconnect a slice from a type's slice zone.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + from: { + type: "string", + required: true, + description: "Name of the page type or custom type", + }, + "slice-zone": { + type: "string", + description: 'Slice zone field ID (default: "slices")', + }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { from, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const apiConfig = { repo, token, host }; + + const slices = await getSlices(apiConfig); + const slice = slices.find((s) => s.name === name); + if (!slice) { + throw new CommandError(`Slice not found: ${name}`); + } + + const customTypes = await getCustomTypes(apiConfig); + const customType = customTypes.find((ct) => ct.label === from); + if (!customType) { + throw new CommandError(`Type not found: ${from}`); + } + + const allFields: Record = Object.assign( + {}, + ...Object.values(customType.json), + ); + + const sliceZoneField = allFields[sliceZone]; + if (!sliceZoneField || sliceZoneField.type !== "Slices") { + throw new CommandError(`Slice zone "${sliceZone}" not found in "${from}".`); + } + + if (!sliceZoneField.config?.choices || !(slice.id in sliceZoneField.config.choices)) { + throw new CommandError( + `Slice "${slice.id}" is not connected to "${from}" in slice zone "${sliceZone}".`, + ); + } + + delete sliceZoneField.config.choices[slice.id]; + + try { + await updateCustomType(customType, apiConfig); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to disconnect slice: ${message}`); + } + throw error; + } + + console.info(`Disconnected slice "${name}" from "${from}"`); +}); diff --git a/src/commands/slice-list.ts b/src/commands/slice-list.ts new file mode 100644 index 0000000..d046ba6 --- /dev/null +++ b/src/commands/slice-list.ts @@ -0,0 +1,38 @@ +import { getHost, getToken } from "../auth"; +import { getSlices } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice list", + description: "List all slices.", + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ values }) => { + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + + if (json) { + console.info(stringify(slices)); + return; + } + + if (slices.length === 0) { + console.info("No slices found."); + return; + } + + for (const slice of slices) { + console.info(`${slice.name} (id: ${slice.id})`); + } + + throw new CommandError("Not implemented."); +}); diff --git a/src/commands/slice-remove-variation.ts b/src/commands/slice-remove-variation.ts new file mode 100644 index 0000000..69b53fe --- /dev/null +++ b/src/commands/slice-remove-variation.ts @@ -0,0 +1,56 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { getHost, getToken } from "../auth"; +import { getSlices, updateSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice remove-variation", + description: "Remove a variation from a slice.", + positionals: { + name: { description: "Name of the variation", required: true }, + }, + options: { + from: { type: "string", required: true, description: "Name of the slice" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { from, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((s) => s.name === from); + + if (!slice) { + throw new CommandError(`Slice not found: ${from}`); + } + + const variation = slice.variations.find((v) => v.name === name); + + if (!variation) { + throw new CommandError(`Variation "${name}" not found in slice "${from}".`); + } + + const updatedSlice: SharedSlice = { + ...slice, + variations: slice.variations.filter((v) => v.id !== variation.id), + }; + + try { + await updateSlice(updatedSlice, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to remove variation: ${message}`); + } + throw error; + } + + console.info(`Removed variation "${name}" from slice "${from}"`); +}); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts new file mode 100644 index 0000000..35cec45 --- /dev/null +++ b/src/commands/slice-remove.ts @@ -0,0 +1,42 @@ +import { getHost, getToken } from "../auth"; +import { getSlices, removeSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice remove", + description: "Remove a slice.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((s) => s.name === name); + + if (!slice) { + throw new CommandError(`Slice not found: ${name}`); + } + + try { + await removeSlice(slice.id, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to remove custom type: ${message}`); + } + throw error; + } + + console.info(`Slice removed: "${name}" (id: ${slice.id})`); +}); diff --git a/src/commands/slice-view.ts b/src/commands/slice-view.ts new file mode 100644 index 0000000..b0b0542 --- /dev/null +++ b/src/commands/slice-view.ts @@ -0,0 +1,41 @@ +import { getHost, getToken } from "../auth"; +import { getSlices } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice view", + description: "View details of a slice.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + json: { type: "boolean", description: "Output as JSON" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { json, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((slice) => slice.name === name); + + if (!slice) { + throw new CommandError(`Slice not found: ${name}`); + } + + if (json) { + console.info(stringify(slice)); + return; + } + + console.info(`ID: ${slice.id}`); + console.info(`Name: ${slice.name}`); + const variations = slice.variations?.map((v) => v.id).join(", ") || "(none)"; + console.info(`Variations: ${variations}`); +}); diff --git a/src/commands/slice.ts b/src/commands/slice.ts new file mode 100644 index 0000000..b99c8f1 --- /dev/null +++ b/src/commands/slice.ts @@ -0,0 +1,49 @@ +import { createCommandRouter } from "../lib/command"; + +import sliceAddVariation from "./slice-add-variation"; +import sliceConnect from "./slice-connect"; +import sliceCreate from "./slice-create"; +import sliceDisconnect from "./slice-disconnect"; +import sliceList from "./slice-list"; +import sliceRemove from "./slice-remove"; +import sliceRemoveVariation from "./slice-remove-variation"; +import sliceView from "./slice-view"; + +export default createCommandRouter({ + name: "prismic slice", + description: "Manage slices.", + commands: { + create: { + handler: sliceCreate, + description: "Create a new slice", + }, + remove: { + handler: sliceRemove, + description: "Remove a slice", + }, + list: { + handler: sliceList, + description: "List slices", + }, + view: { + handler: sliceView, + description: "View a slice", + }, + connect: { + handler: sliceConnect, + description: "Connect a slice to a type's slice zone", + }, + disconnect: { + handler: sliceDisconnect, + description: "Disconnect a slice from a type's slice zone", + }, + "add-variation": { + handler: sliceAddVariation, + description: "Add a variation to a slice", + }, + "remove-variation": { + handler: sliceRemoveVariation, + description: "Remove a variation from a slice", + }, + }, +}); diff --git a/src/index.ts b/src/index.ts index 696df1b..99d5a07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,8 +11,11 @@ import init from "./commands/init"; import locale from "./commands/locale"; import login from "./commands/login"; import logout from "./commands/logout"; +import customType from "./commands/custom-type"; +import pageType from "./commands/page-type"; import preview from "./commands/preview"; import repo from "./commands/repo"; +import slice from "./commands/slice"; import sync from "./commands/sync"; import token from "./commands/token"; import webhook from "./commands/webhook"; @@ -63,6 +66,18 @@ const router = createCommandRouter({ handler: repo, description: "Manage repositories", }, + "custom-type": { + handler: customType, + description: "Manage custom types", + }, + "page-type": { + handler: pageType, + description: "Manage page types", + }, + slice: { + handler: slice, + description: "Manage slices", + }, preview: { handler: preview, description: "Manage preview configurations", From e59258b8e36d5eae203a7e912ce7a3dcdbad672f Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 16:53:28 -1000 Subject: [PATCH 02/29] fix: correct Custom Types API URLs and slice command messages The insert/update endpoints were using incorrect paths (e.g. `customtypes` instead of `customtypes/insert`), causing 401 errors on write operations. Also fixes copy-paste errors in slice command output messages. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/clients/custom-types.ts | 12 ++++++------ src/commands/slice-create.ts | 4 ++-- src/commands/slice-list.ts | 4 +--- src/commands/slice-remove.ts | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index 98ddeae..fc5640b 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -22,7 +22,7 @@ export async function insertCustomType( ): Promise { const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); - const url = new URL("customtypes", customTypesServiceUrl); + const url = new URL("customtypes/insert", customTypesServiceUrl); await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, @@ -36,9 +36,9 @@ export async function updateCustomType( ): Promise { const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); - const url = new URL(`customtypes/${encodeURIComponent(model.id)}`, customTypesServiceUrl); + const url = new URL("customtypes/update", customTypesServiceUrl); await request(url, { - method: "PUT", + method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, }); @@ -77,7 +77,7 @@ export async function insertSlice( ): Promise { const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); - const url = new URL("slices", customTypesServiceUrl); + const url = new URL("slices/insert", customTypesServiceUrl); await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, @@ -91,9 +91,9 @@ export async function updateSlice( ): Promise { const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); - const url = new URL(`slices/${encodeURIComponent(model.id)}`, customTypesServiceUrl); + const url = new URL("slices/update", customTypesServiceUrl); await request(url, { - method: "PUT", + method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, }); diff --git a/src/commands/slice-create.ts b/src/commands/slice-create.ts index 6191d04..4313a21 100644 --- a/src/commands/slice-create.ts +++ b/src/commands/slice-create.ts @@ -49,10 +49,10 @@ export default createCommand(config, async ({ positionals, values }) => { } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); - throw new CommandError(`Failed to create page type: ${message}`); + throw new CommandError(`Failed to create slice: ${message}`); } throw error; } - console.info(`Created page type "${name}" (id: "${id}")`); + console.info(`Created slice "${name}" (id: "${id}")`); }); diff --git a/src/commands/slice-list.ts b/src/commands/slice-list.ts index d046ba6..55f4736 100644 --- a/src/commands/slice-list.ts +++ b/src/commands/slice-list.ts @@ -1,6 +1,6 @@ import { getHost, getToken } from "../auth"; import { getSlices } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { getRepositoryName } from "../project"; @@ -33,6 +33,4 @@ export default createCommand(config, async ({ values }) => { for (const slice of slices) { console.info(`${slice.name} (id: ${slice.id})`); } - - throw new CommandError("Not implemented."); }); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts index 35cec45..5bf01b8 100644 --- a/src/commands/slice-remove.ts +++ b/src/commands/slice-remove.ts @@ -33,7 +33,7 @@ export default createCommand(config, async ({ positionals, values }) => { } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); - throw new CommandError(`Failed to remove custom type: ${message}`); + throw new CommandError(`Failed to remove slice: ${message}`); } throw error; } From ebb38ece43aa913d57ea72661d8f29ea9f78c415 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 16:55:11 -1000 Subject: [PATCH 03/29] test: add e2e tests for custom-type, page-type, and slice commands Covers create, list, view, remove for all three command groups, plus slice-specific connect, disconnect, add-variation, and remove-variation. All tests verify state against the Custom Types API. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/custom-type-create.test.ts | 44 +++++++++++++++++++++++++++++ test/custom-type-list.test.ts | 29 +++++++++++++++++++ test/custom-type-remove.test.ts | 21 ++++++++++++++ test/custom-type-view.test.ts | 30 ++++++++++++++++++++ test/custom-type.test.ts | 13 +++++++++ test/page-type-create.test.ts | 44 +++++++++++++++++++++++++++++ test/page-type-list.test.ts | 29 +++++++++++++++++++ test/page-type-remove.test.ts | 21 ++++++++++++++ test/page-type-view.test.ts | 30 ++++++++++++++++++++ test/page-type.test.ts | 13 +++++++++ test/prismic.ts | 28 ++++++++++++++++++ test/slice-add-variation.test.ts | 30 ++++++++++++++++++++ test/slice-connect.test.ts | 41 +++++++++++++++++++++++++++ test/slice-create.test.ts | 33 ++++++++++++++++++++++ test/slice-disconnect.test.ts | 41 +++++++++++++++++++++++++++ test/slice-list.test.ts | 29 +++++++++++++++++++ test/slice-remove-variation.test.ts | 44 +++++++++++++++++++++++++++++ test/slice-remove.test.ts | 21 ++++++++++++++ test/slice-view.test.ts | 29 +++++++++++++++++++ test/slice.test.ts | 13 +++++++++ 20 files changed, 583 insertions(+) create mode 100644 test/custom-type-create.test.ts create mode 100644 test/custom-type-list.test.ts create mode 100644 test/custom-type-remove.test.ts create mode 100644 test/custom-type-view.test.ts create mode 100644 test/custom-type.test.ts create mode 100644 test/page-type-create.test.ts create mode 100644 test/page-type-list.test.ts create mode 100644 test/page-type-remove.test.ts create mode 100644 test/page-type-view.test.ts create mode 100644 test/page-type.test.ts create mode 100644 test/slice-add-variation.test.ts create mode 100644 test/slice-connect.test.ts create mode 100644 test/slice-create.test.ts create mode 100644 test/slice-disconnect.test.ts create mode 100644 test/slice-list.test.ts create mode 100644 test/slice-remove-variation.test.ts create mode 100644 test/slice-remove.test.ts create mode 100644 test/slice-view.test.ts create mode 100644 test/slice.test.ts diff --git a/test/custom-type-create.test.ts b/test/custom-type-create.test.ts new file mode 100644 index 0000000..36a5ef7 --- /dev/null +++ b/test/custom-type-create.test.ts @@ -0,0 +1,44 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type", ["create", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type create [options]"); +}); + +it("creates a custom type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + + const { stdout, exitCode } = await prismic("custom-type", ["create", label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created custom type "${label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "custom", repeatable: true }); +}); + +it("creates a single custom type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + + const { exitCode } = await prismic("custom-type", ["create", label!, "--single"]); + expect(exitCode).toBe(0); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "custom", repeatable: false }); +}); + +it("creates a custom type with a custom id", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + const id = `CustomType${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("custom-type", ["create", label!, "--id", id]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created custom type "${label}" (id: "${id}")`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.id === id); + expect(created).toBeDefined(); +}); diff --git a/test/custom-type-list.test.ts b/test/custom-type-list.test.ts new file mode 100644 index 0000000..069d270 --- /dev/null +++ b/test/custom-type-list.test.ts @@ -0,0 +1,29 @@ +import { buildCustomType, it } from "./it"; +import { insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type", ["list", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type list [options]"); +}); + +it("lists custom types", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("custom-type", ["list"]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`${customType.label} (id: ${customType.id})`); +}); + +it("lists custom types as JSON", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("custom-type", ["list", "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toEqual( + expect.arrayContaining([expect.objectContaining({ id: customType.id, format: "custom" })]), + ); +}); diff --git a/test/custom-type-remove.test.ts b/test/custom-type-remove.test.ts new file mode 100644 index 0000000..166471d --- /dev/null +++ b/test/custom-type-remove.test.ts @@ -0,0 +1,21 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type", ["remove", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type remove [options]"); +}); + +it("removes a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("custom-type", ["remove", customType.label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Custom type removed: "${customType.label}" (id: ${customType.id})`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const removed = customTypes.find((ct) => ct.id === customType.id); + expect(removed).toBeUndefined(); +}); diff --git a/test/custom-type-view.test.ts b/test/custom-type-view.test.ts new file mode 100644 index 0000000..4d6493b --- /dev/null +++ b/test/custom-type-view.test.ts @@ -0,0 +1,30 @@ +import { buildCustomType, it } from "./it"; +import { insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type", ["view", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type view [options]"); +}); + +it("views a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("custom-type", ["view", customType.label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`ID: ${customType.id}`); + expect(stdout).toContain(`Name: ${customType.label}`); + expect(stdout).toContain("Repeatable: true"); + expect(stdout).toContain("Tabs: Main"); +}); + +it("views a custom type as JSON", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("custom-type", ["view", customType.label!, "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toMatchObject({ id: customType.id, label: customType.label, format: "custom" }); +}); diff --git a/test/custom-type.test.ts b/test/custom-type.test.ts new file mode 100644 index 0000000..cd896bf --- /dev/null +++ b/test/custom-type.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("custom-type", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic custom-type [options]"); +}); diff --git a/test/page-type-create.test.ts b/test/page-type-create.test.ts new file mode 100644 index 0000000..da101f0 --- /dev/null +++ b/test/page-type-create.test.ts @@ -0,0 +1,44 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type", ["create", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type create [options]"); +}); + +it("creates a page type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "page" }); + + const { stdout, exitCode } = await prismic("page-type", ["create", label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created page type "${label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "page", repeatable: true }); +}); + +it("creates a single page type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "page" }); + + const { exitCode } = await prismic("page-type", ["create", label!, "--single"]); + expect(exitCode).toBe(0); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "page", repeatable: false }); +}); + +it("creates a page type with a custom id", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "page" }); + const id = `PageType${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("page-type", ["create", label!, "--id", id]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created page type "${label}" (id: "${id}")`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.id === id); + expect(created).toBeDefined(); +}); diff --git a/test/page-type-list.test.ts b/test/page-type-list.test.ts new file mode 100644 index 0000000..bb4f658 --- /dev/null +++ b/test/page-type-list.test.ts @@ -0,0 +1,29 @@ +import { buildCustomType, it } from "./it"; +import { insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type", ["list", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type list [options]"); +}); + +it("lists page types", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("page-type", ["list"]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`${pageType.label} (id: ${pageType.id})`); +}); + +it("lists page types as JSON", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("page-type", ["list", "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toEqual( + expect.arrayContaining([expect.objectContaining({ id: pageType.id, format: "page" })]), + ); +}); diff --git a/test/page-type-remove.test.ts b/test/page-type-remove.test.ts new file mode 100644 index 0000000..3059d90 --- /dev/null +++ b/test/page-type-remove.test.ts @@ -0,0 +1,21 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type", ["remove", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type remove [options]"); +}); + +it("removes a page type", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("page-type", ["remove", pageType.label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Page type removed: "${pageType.label}" (id: ${pageType.id})`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const removed = customTypes.find((ct) => ct.id === pageType.id); + expect(removed).toBeUndefined(); +}); diff --git a/test/page-type-view.test.ts b/test/page-type-view.test.ts new file mode 100644 index 0000000..65a4afd --- /dev/null +++ b/test/page-type-view.test.ts @@ -0,0 +1,30 @@ +import { buildCustomType, it } from "./it"; +import { insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type", ["view", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type view [options]"); +}); + +it("views a page type", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("page-type", ["view", pageType.label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`ID: ${pageType.id}`); + expect(stdout).toContain(`Name: ${pageType.label}`); + expect(stdout).toContain("Repeatable: true"); + expect(stdout).toContain("Tabs: Main"); +}); + +it("views a page type as JSON", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("page-type", ["view", pageType.label!, "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toMatchObject({ id: pageType.id, label: pageType.label, format: "page" }); +}); diff --git a/test/page-type.test.ts b/test/page-type.test.ts new file mode 100644 index 0000000..32f9bc7 --- /dev/null +++ b/test/page-type.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("page-type", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic page-type [options]"); +}); diff --git a/test/prismic.ts b/test/prismic.ts index 3a1c50b..941baac 100644 --- a/test/prismic.ts +++ b/test/prismic.ts @@ -1,3 +1,5 @@ +import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + const DEFAULT_HOST = "prismic.io"; type HostConfig = { host?: string }; @@ -47,6 +49,19 @@ export async function deleteRepository( } } +export async function getCustomTypes(config: RepoConfig): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("customtypes", `https://customtypes.${host}/`); + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${config.token}`, + repository: config.repo, + }, + }); + if (!res.ok) throw new Error(`Failed to get custom types: ${res.status} ${await res.text()}`); + return await res.json(); +} + export async function insertCustomType(customType: object, config: RepoConfig): Promise { const host = config.host ?? DEFAULT_HOST; const url = new URL("customtypes/insert", `https://customtypes.${host}/`); @@ -75,6 +90,19 @@ export async function deleteCustomType(customTypeId: string, config: RepoConfig) if (!res.ok) throw new Error(`Failed to delete custom type: ${res.status} ${await res.text()}`); } +export async function getSlices(config: RepoConfig): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("slices", `https://customtypes.${host}/`); + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${config.token}`, + repository: config.repo, + }, + }); + if (!res.ok) throw new Error(`Failed to get slices: ${res.status} ${await res.text()}`); + return await res.json(); +} + export async function insertSlice(slice: object, config: RepoConfig): Promise { const host = config.host ?? DEFAULT_HOST; const url = new URL("slices/insert", `https://customtypes.${host}/`); diff --git a/test/slice-add-variation.test.ts b/test/slice-add-variation.test.ts new file mode 100644 index 0000000..bfcbfbf --- /dev/null +++ b/test/slice-add-variation.test.ts @@ -0,0 +1,30 @@ +import { buildSlice, it } from "./it"; +import { getSlices, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["add-variation", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice add-variation [options]"); +}); + +it("adds a variation to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const variationName = `Variation${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", [ + "add-variation", + variationName, + "--to", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Added variation "${variationName}"`); + expect(stdout).toContain(`to slice "${slice.name}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.name === variationName); + expect(variation).toBeDefined(); +}); diff --git a/test/slice-connect.test.ts b/test/slice-connect.test.ts new file mode 100644 index 0000000..d7e20b0 --- /dev/null +++ b/test/slice-connect.test.ts @@ -0,0 +1,41 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["connect", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice connect [options]"); +}); + +it("connects a slice to a type", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + const customType = buildCustomType({ + format: "page", + json: { + Main: { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }, + }, + }, + }); + + await insertSlice(slice, { repo, token, host }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", [ + "connect", + slice.name, + "--to", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Connected slice "${slice.name}" to "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const choices = (updated?.json.Main as any).slices.config.choices; + expect(choices[slice.id]).toEqual({ type: "SharedSlice" }); +}); diff --git a/test/slice-create.test.ts b/test/slice-create.test.ts new file mode 100644 index 0000000..f4f78fc --- /dev/null +++ b/test/slice-create.test.ts @@ -0,0 +1,33 @@ +import { buildSlice, it } from "./it"; +import { getSlices } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["create", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice create [options]"); +}); + +it("creates a slice", async ({ expect, prismic, repo, token, host }) => { + const { name } = buildSlice(); + + const { stdout, exitCode } = await prismic("slice", ["create", name]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created slice "${name}"`); + + const slices = await getSlices({ repo, token, host }); + const created = slices.find((s) => s.name === name); + expect(created).toBeDefined(); +}); + +it("creates a slice with a custom id", async ({ expect, prismic, repo, token, host }) => { + const { name } = buildSlice(); + const id = `slice_${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", ["create", name, "--id", id]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created slice "${name}" (id: "${id}")`); + + const slices = await getSlices({ repo, token, host }); + const created = slices.find((s) => s.id === id); + expect(created).toBeDefined(); +}); diff --git a/test/slice-disconnect.test.ts b/test/slice-disconnect.test.ts new file mode 100644 index 0000000..ddea578 --- /dev/null +++ b/test/slice-disconnect.test.ts @@ -0,0 +1,41 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["disconnect", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice disconnect [options]"); +}); + +it("disconnects a slice from a type", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + const customType = buildCustomType({ + format: "page", + json: { + Main: { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: { [slice.id]: { type: "SharedSlice" } } }, + }, + }, + }, + }); + + await insertSlice(slice, { repo, token, host }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", [ + "disconnect", + slice.name, + "--from", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Disconnected slice "${slice.name}" from "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const choices = (updated?.json.Main as any).slices.config.choices; + expect(choices[slice.id]).toBeUndefined(); +}); diff --git a/test/slice-list.test.ts b/test/slice-list.test.ts new file mode 100644 index 0000000..010ad4b --- /dev/null +++ b/test/slice-list.test.ts @@ -0,0 +1,29 @@ +import { buildSlice, it } from "./it"; +import { insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["list", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice list [options]"); +}); + +it("lists slices", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["list"]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`${slice.name} (id: ${slice.id})`); +}); + +it("lists slices as JSON", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["list", "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toEqual( + expect.arrayContaining([expect.objectContaining({ id: slice.id })]), + ); +}); diff --git a/test/slice-remove-variation.test.ts b/test/slice-remove-variation.test.ts new file mode 100644 index 0000000..1adcf71 --- /dev/null +++ b/test/slice-remove-variation.test.ts @@ -0,0 +1,44 @@ +import { buildSlice, it } from "./it"; +import { getSlices, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["remove-variation", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice remove-variation [options]"); +}); + +it("removes a variation from a slice", async ({ expect, prismic, repo, token, host }) => { + const variationName = `Variation${crypto.randomUUID().split("-")[0]}`; + const variationId = `variation${crypto.randomUUID().split("-")[0]}`; + const slice = buildSlice(); + const sliceWithVariation = { + ...slice, + variations: [ + ...slice.variations, + { + id: variationId, + name: variationName, + description: variationName, + docURL: "", + imageUrl: "", + version: "", + }, + ], + }; + + await insertSlice(sliceWithVariation, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", [ + "remove-variation", + variationName, + "--from", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Removed variation "${variationName}" from slice "${slice.name}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const removed = updated?.variations.find((v) => v.id === variationId); + expect(removed).toBeUndefined(); +}); diff --git a/test/slice-remove.test.ts b/test/slice-remove.test.ts new file mode 100644 index 0000000..c89a638 --- /dev/null +++ b/test/slice-remove.test.ts @@ -0,0 +1,21 @@ +import { buildSlice, it } from "./it"; +import { getSlices, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["remove", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice remove [options]"); +}); + +it("removes a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["remove", slice.name]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Slice removed: "${slice.name}" (id: ${slice.id})`); + + const slices = await getSlices({ repo, token, host }); + const removed = slices.find((s) => s.id === slice.id); + expect(removed).toBeUndefined(); +}); diff --git a/test/slice-view.test.ts b/test/slice-view.test.ts new file mode 100644 index 0000000..466f8ab --- /dev/null +++ b/test/slice-view.test.ts @@ -0,0 +1,29 @@ +import { buildSlice, it } from "./it"; +import { insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["view", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice view [options]"); +}); + +it("views a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["view", slice.name]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`ID: ${slice.id}`); + expect(stdout).toContain(`Name: ${slice.name}`); + expect(stdout).toContain("Variations: default"); +}); + +it("views a slice as JSON", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["view", slice.name, "--json"]); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toMatchObject({ id: slice.id, name: slice.name }); +}); diff --git a/test/slice.test.ts b/test/slice.test.ts new file mode 100644 index 0000000..2ed59a6 --- /dev/null +++ b/test/slice.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice [options]"); +}); From a90b5705dc0591214390b492fd8f26b74f3d2ecd Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 17:05:25 -1000 Subject: [PATCH 04/29] fix: add default slice zone to page type and fix test types Page types need a slice zone in Main tab by default. Replace `as any` casts in slice connect/disconnect tests with proper types. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/page-type-create.ts | 8 +++++++- test/slice-connect.test.ts | 4 +++- test/slice-disconnect.test.ts | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/commands/page-type-create.ts b/src/commands/page-type-create.ts index 80bbf24..464191b 100644 --- a/src/commands/page-type-create.ts +++ b/src/commands/page-type-create.ts @@ -32,7 +32,13 @@ export default createCommand(config, async ({ positionals, values }) => { status: true, format: "page", json: { - Main: {}, + Main: { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }, + }, "SEO & Metadata": { meta_title: { type: "Text", diff --git a/test/slice-connect.test.ts b/test/slice-connect.test.ts index d7e20b0..791c30e 100644 --- a/test/slice-connect.test.ts +++ b/test/slice-connect.test.ts @@ -1,3 +1,5 @@ +import type { DynamicSlices } from "@prismicio/types-internal/lib/customtypes"; + import { buildCustomType, buildSlice, it } from "./it"; import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; @@ -36,6 +38,6 @@ it("connects a slice to a type", async ({ expect, prismic, repo, token, host }) const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); - const choices = (updated?.json.Main as any).slices.config.choices; + const choices = (updated!.json.Main.slices as DynamicSlices).config!.choices!; expect(choices[slice.id]).toEqual({ type: "SharedSlice" }); }); diff --git a/test/slice-disconnect.test.ts b/test/slice-disconnect.test.ts index ddea578..49cb054 100644 --- a/test/slice-disconnect.test.ts +++ b/test/slice-disconnect.test.ts @@ -1,3 +1,5 @@ +import type { DynamicSlices } from "@prismicio/types-internal/lib/customtypes"; + import { buildCustomType, buildSlice, it } from "./it"; import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; @@ -36,6 +38,6 @@ it("disconnects a slice from a type", async ({ expect, prismic, repo, token, hos const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); - const choices = (updated?.json.Main as any).slices.config.choices; + const choices = (updated!.json.Main.slices as DynamicSlices).config!.choices!; expect(choices[slice.id]).toBeUndefined(); }); From 16d120f95a31a2534c5e6183ebad91e6c4995cbd Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 17:43:49 -1000 Subject: [PATCH 05/29] feat: add field management commands for remote modeling Adds `field add`, `field list`, and `field remove` commands that operate on custom types and slices via the Custom Types API, along with per-type `field add ` subcommands for all supported field types. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/field-add-boolean.ts | 55 ++++++ src/commands/field-add-color.ts | 45 +++++ .../field-add-content-relationship.ts | 53 ++++++ src/commands/field-add-date.ts | 47 +++++ src/commands/field-add-embed.ts | 45 +++++ src/commands/field-add-geopoint.ts | 43 +++++ src/commands/field-add-group.ts | 43 +++++ src/commands/field-add-image.ts | 45 +++++ src/commands/field-add-integration.ts | 47 +++++ src/commands/field-add-link-to-media.ts | 53 ++++++ src/commands/field-add-link.ts | 58 ++++++ src/commands/field-add-number.ts | 64 +++++++ src/commands/field-add-rich-text.ts | 70 +++++++ src/commands/field-add-select.ts | 55 ++++++ src/commands/field-add-table.ts | 43 +++++ src/commands/field-add-text.ts | 45 +++++ src/commands/field-add-timestamp.ts | 47 +++++ src/commands/field-add-uid.ts | 41 ++++ src/commands/field-add.ts | 98 ++++++++++ src/commands/field-list.ts | 45 +++++ src/commands/field-remove.ts | 28 +++ src/commands/field.ts | 23 +++ src/index.ts | 5 + src/models.ts | 179 ++++++++++++++++++ test/field-add-boolean.test.ts | 48 +++++ test/field-add-color.test.ts | 48 +++++ test/field-add-content-relationship.test.ts | 48 +++++ test/field-add-date.test.ts | 48 +++++ test/field-add-embed.test.ts | 48 +++++ test/field-add-geopoint.test.ts | 48 +++++ test/field-add-group.test.ts | 122 ++++++++++++ test/field-add-image.test.ts | 48 +++++ test/field-add-integration.test.ts | 48 +++++ test/field-add-link-to-media.test.ts | 48 +++++ test/field-add-link.test.ts | 48 +++++ test/field-add-number.test.ts | 48 +++++ test/field-add-rich-text.test.ts | 48 +++++ test/field-add-select.test.ts | 48 +++++ test/field-add-table.test.ts | 48 +++++ test/field-add-text.test.ts | 68 +++++++ test/field-add-timestamp.test.ts | 48 +++++ test/field-add-uid.test.ts | 46 +++++ test/field-add.test.ts | 13 ++ test/field-list.test.ts | 86 +++++++++ test/field-remove.test.ts | 108 +++++++++++ test/field.test.ts | 13 ++ 46 files changed, 2453 insertions(+) create mode 100644 src/commands/field-add-boolean.ts create mode 100644 src/commands/field-add-color.ts create mode 100644 src/commands/field-add-content-relationship.ts create mode 100644 src/commands/field-add-date.ts create mode 100644 src/commands/field-add-embed.ts create mode 100644 src/commands/field-add-geopoint.ts create mode 100644 src/commands/field-add-group.ts create mode 100644 src/commands/field-add-image.ts create mode 100644 src/commands/field-add-integration.ts create mode 100644 src/commands/field-add-link-to-media.ts create mode 100644 src/commands/field-add-link.ts create mode 100644 src/commands/field-add-number.ts create mode 100644 src/commands/field-add-rich-text.ts create mode 100644 src/commands/field-add-select.ts create mode 100644 src/commands/field-add-table.ts create mode 100644 src/commands/field-add-text.ts create mode 100644 src/commands/field-add-timestamp.ts create mode 100644 src/commands/field-add-uid.ts create mode 100644 src/commands/field-add.ts create mode 100644 src/commands/field-list.ts create mode 100644 src/commands/field-remove.ts create mode 100644 src/commands/field.ts create mode 100644 src/models.ts create mode 100644 test/field-add-boolean.test.ts create mode 100644 test/field-add-color.test.ts create mode 100644 test/field-add-content-relationship.test.ts create mode 100644 test/field-add-date.test.ts create mode 100644 test/field-add-embed.test.ts create mode 100644 test/field-add-geopoint.test.ts create mode 100644 test/field-add-group.test.ts create mode 100644 test/field-add-image.test.ts create mode 100644 test/field-add-integration.test.ts create mode 100644 test/field-add-link-to-media.test.ts create mode 100644 test/field-add-link.test.ts create mode 100644 test/field-add-number.test.ts create mode 100644 test/field-add-rich-text.test.ts create mode 100644 test/field-add-select.test.ts create mode 100644 test/field-add-table.test.ts create mode 100644 test/field-add-text.test.ts create mode 100644 test/field-add-timestamp.test.ts create mode 100644 test/field-add-uid.test.ts create mode 100644 test/field-add.test.ts create mode 100644 test/field-list.test.ts create mode 100644 test/field-remove.test.ts create mode 100644 test/field.test.ts diff --git a/src/commands/field-add-boolean.ts b/src/commands/field-add-boolean.ts new file mode 100644 index 0000000..6f856a9 --- /dev/null +++ b/src/commands/field-add-boolean.ts @@ -0,0 +1,55 @@ +import type { BooleanField } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add boolean", + description: "Add a boolean field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + "default-value": { type: "boolean", description: "Default value" }, + "true-label": { type: "string", description: "Label for true value" }, + "false-label": { type: "string", description: "Label for false value" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + "default-value": default_value, + "true-label": placeholder_true, + "false-label": placeholder_false, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: BooleanField = { + type: "Boolean", + config: { + label: label ?? capitalCase(fieldId), + default_value, + placeholder_true, + placeholder_false, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-color.ts b/src/commands/field-add-color.ts new file mode 100644 index 0000000..ac0daf6 --- /dev/null +++ b/src/commands/field-add-color.ts @@ -0,0 +1,45 @@ +import type { Color } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add color", + description: "Add a color field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Color = { + type: "Color", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts new file mode 100644 index 0000000..7eb46e4 --- /dev/null +++ b/src/commands/field-add-content-relationship.ts @@ -0,0 +1,53 @@ +import type { Link } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add content-relationship", + description: "Add a content relationship field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + tag: { type: "string", multiple: true, description: "Allowed tag (can be repeated)" }, + "custom-type": { type: "string", multiple: true, description: "Allowed custom type (can be repeated)" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + tag: tags, + "custom-type": customtypes, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Link = { + type: "Link", + config: { + label: label ?? capitalCase(fieldId), + select: "document", + tags, + customtypes, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-date.ts b/src/commands/field-add-date.ts new file mode 100644 index 0000000..f0d3c8a --- /dev/null +++ b/src/commands/field-add-date.ts @@ -0,0 +1,47 @@ +import type { Date as DateField } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add date", + description: "Add a date field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + default: { type: "string", description: "Default value" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: DateField = { + type: "Date", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + default: defaultValue, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-embed.ts b/src/commands/field-add-embed.ts new file mode 100644 index 0000000..c58a249 --- /dev/null +++ b/src/commands/field-add-embed.ts @@ -0,0 +1,45 @@ +import type { Embed } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add embed", + description: "Add an embed field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Embed = { + type: "Embed", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-geopoint.ts b/src/commands/field-add-geopoint.ts new file mode 100644 index 0000000..07d8e76 --- /dev/null +++ b/src/commands/field-add-geopoint.ts @@ -0,0 +1,43 @@ +import type { GeoPoint } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add geopoint", + description: "Add a geopoint field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: GeoPoint = { + type: "GeoPoint", + config: { + label: label ?? capitalCase(fieldId), + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-group.ts b/src/commands/field-add-group.ts new file mode 100644 index 0000000..6b5a0e6 --- /dev/null +++ b/src/commands/field-add-group.ts @@ -0,0 +1,43 @@ +import type { Group } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add group", + description: "Add a group field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Group = { + type: "Group", + config: { + label: label ?? capitalCase(fieldId), + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-image.ts b/src/commands/field-add-image.ts new file mode 100644 index 0000000..ea19993 --- /dev/null +++ b/src/commands/field-add-image.ts @@ -0,0 +1,45 @@ +import type { Image } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add image", + description: "Add an image field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Image = { + type: "Image", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-integration.ts b/src/commands/field-add-integration.ts new file mode 100644 index 0000000..5489e54 --- /dev/null +++ b/src/commands/field-add-integration.ts @@ -0,0 +1,47 @@ +import type { IntegrationField } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add integration", + description: "Add an integration field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + catalog: { type: "string", description: "Integration catalog ID" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, catalog, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: IntegrationField = { + type: "IntegrationFields", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + catalog, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-link-to-media.ts b/src/commands/field-add-link-to-media.ts new file mode 100644 index 0000000..1d62691 --- /dev/null +++ b/src/commands/field-add-link-to-media.ts @@ -0,0 +1,53 @@ +import type { Link } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add link-to-media", + description: "Add a link to media field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + "allow-text": { type: "boolean", description: "Allow custom link text" }, + variant: { type: "string", multiple: true, description: "Allowed variant (can be repeated)" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + "allow-text": allowText, + variant: variants, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Link = { + type: "Link", + config: { + label: label ?? capitalCase(fieldId), + select: "media", + allowText, + variants, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-link.ts b/src/commands/field-add-link.ts new file mode 100644 index 0000000..9bd07eb --- /dev/null +++ b/src/commands/field-add-link.ts @@ -0,0 +1,58 @@ +import type { Link } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add link", + description: "Add a link field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + "allow-target-blank": { type: "boolean", description: "Allow opening in new tab" }, + "allow-text": { type: "boolean", description: "Allow custom link text" }, + repeatable: { type: "boolean", description: "Allow multiple links" }, + variant: { type: "string", multiple: true, description: "Allowed variant (can be repeated)" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + "allow-target-blank": allowTargetBlank, + "allow-text": allowText, + repeatable: repeat, + variant: variants, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Link = { + type: "Link", + config: { + label: label ?? capitalCase(fieldId), + allowTargetBlank, + allowText, + repeat, + variants, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-number.ts b/src/commands/field-add-number.ts new file mode 100644 index 0000000..68756d4 --- /dev/null +++ b/src/commands/field-add-number.ts @@ -0,0 +1,64 @@ +import type { Number as NumberField } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add number", + description: "Add a number field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + min: { type: "string", description: "Minimum value" }, + max: { type: "string", description: "Maximum value" }, + step: { type: "string", description: "Step increment" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, repo = await getRepositoryName() } = values; + + const min = parseNumber(values.min, "min"); + const max = parseNumber(values.max, "max"); + const step = parseNumber(values.step, "step"); + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: NumberField = { + type: "Number", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + min, + max, + step, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); + +function parseNumber(value: string | undefined, optionName: string): number | undefined { + if (value === undefined) return undefined; + const number = Number(value); + if (Number.isNaN(number)) { + throw new CommandError(`--${optionName} must be a valid number, got "${value}"`); + } + return number; +} diff --git a/src/commands/field-add-rich-text.ts b/src/commands/field-add-rich-text.ts new file mode 100644 index 0000000..61d4a65 --- /dev/null +++ b/src/commands/field-add-rich-text.ts @@ -0,0 +1,70 @@ +import type { RichText } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const ALL_BLOCKS = + "paragraph,preformatted,heading1,heading2,heading3,heading4,heading5,heading6,strong,em,hyperlink,image,embed,list-item,o-list-item,rtl"; + +const config = { + name: "prismic field add rich-text", + description: "Add a rich text field to a slice or custom type.", + sections: { + BLOCKS: ` + heading1, heading2, heading3, heading4, heading5, heading6, + paragraph, strong, em, preformatted, hyperlink, image, embed, + list-item, o-list-item, rtl + `, + }, + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + allow: { + type: "string", + description: "Comma-separated allowed block types (e.g. heading1,heading2,paragraph)", + }, + single: { type: "boolean", description: "Restrict to a single block" }, + "allow-target-blank": { type: "boolean", description: "Allow opening links in new tab" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + placeholder, + allow = ALL_BLOCKS, + single: isSingle, + "allow-target-blank": allowTargetBlank, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: RichText = { + type: "StructuredText", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + ...(isSingle ? { single: allow } : { multi: allow }), + allowTargetBlank, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-select.ts b/src/commands/field-add-select.ts new file mode 100644 index 0000000..683b1d6 --- /dev/null +++ b/src/commands/field-add-select.ts @@ -0,0 +1,55 @@ +import type { Select } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add select", + description: "Add a select field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + "default-value": { type: "string", description: "Default selected value" }, + option: { type: "string", multiple: true, description: "Select option value (can be repeated)" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { + label, + placeholder, + "default-value": default_value, + option: options, + repo = await getRepositoryName(), + } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Select = { + type: "Select", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + default_value, + options, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-table.ts b/src/commands/field-add-table.ts new file mode 100644 index 0000000..624d94b --- /dev/null +++ b/src/commands/field-add-table.ts @@ -0,0 +1,43 @@ +import type { Table } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add table", + description: "Add a table field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Table = { + type: "Table", + config: { + label: label ?? capitalCase(fieldId), + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-text.ts b/src/commands/field-add-text.ts new file mode 100644 index 0000000..3787517 --- /dev/null +++ b/src/commands/field-add-text.ts @@ -0,0 +1,45 @@ +import type { Text } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add text", + description: "Add a text field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Text = { + type: "Text", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-timestamp.ts b/src/commands/field-add-timestamp.ts new file mode 100644 index 0000000..669dc12 --- /dev/null +++ b/src/commands/field-add-timestamp.ts @@ -0,0 +1,47 @@ +import type { Timestamp } from "@prismicio/types-internal/lib/customtypes"; + +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add timestamp", + description: "Add a timestamp field to a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...TARGET_OPTIONS, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + default: { type: "string", description: "Default value" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field: Timestamp = { + type: "Timestamp", + config: { + label: label ?? capitalCase(fieldId), + placeholder, + default: defaultValue, + }, + }; + + if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); + targetFields[fieldId] = field; + await saveModel(); + + console.info(`Field added: ${id}`); +}); diff --git a/src/commands/field-add-uid.ts b/src/commands/field-add-uid.ts new file mode 100644 index 0000000..f44b8bb --- /dev/null +++ b/src/commands/field-add-uid.ts @@ -0,0 +1,41 @@ +import type { UID } from "@prismicio/types-internal/lib/customtypes"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveModel } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field add uid", + description: "Add a UID field to a custom type.", + options: { + "to-page-type": { type: "string", description: "Name of the target page type" }, + "to-custom-type": { type: "string", description: "Name of the target custom type" }, + tab: { type: "string", description: 'Custom type tab name (default: "Main")' }, + repo: { type: "string", short: "r", description: "Repository domain" }, + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ values }) => { + const { label = "UID", placeholder, repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + + const field: UID = { + type: "UID", + config: { + label, + placeholder, + }, + }; + + if ("uid" in fields) throw new CommandError('Field "uid" already exists.'); + fields.uid = field; + await saveModel(); + + console.info("Field added: uid"); +}); diff --git a/src/commands/field-add.ts b/src/commands/field-add.ts new file mode 100644 index 0000000..fef56bd --- /dev/null +++ b/src/commands/field-add.ts @@ -0,0 +1,98 @@ +import { createCommandRouter } from "../lib/command"; +import fieldAddBoolean from "./field-add-boolean"; +import fieldAddColor from "./field-add-color"; +import fieldAddContentRelationship from "./field-add-content-relationship"; +import fieldAddDate from "./field-add-date"; +import fieldAddEmbed from "./field-add-embed"; +import fieldAddGeopoint from "./field-add-geopoint"; +import fieldAddGroup from "./field-add-group"; +import fieldAddImage from "./field-add-image"; +import fieldAddIntegration from "./field-add-integration"; +import fieldAddLink from "./field-add-link"; +import fieldAddLinkToMedia from "./field-add-link-to-media"; +import fieldAddNumber from "./field-add-number"; +import fieldAddRichText from "./field-add-rich-text"; +import fieldAddSelect from "./field-add-select"; +import fieldAddTable from "./field-add-table"; +import fieldAddText from "./field-add-text"; +import fieldAddTimestamp from "./field-add-timestamp"; +import fieldAddUid from "./field-add-uid"; + +export default createCommandRouter({ + name: "prismic field add", + description: "Add a field to a slice or custom type.", + commands: { + boolean: { + handler: fieldAddBoolean, + description: "Add a boolean field", + }, + color: { + handler: fieldAddColor, + description: "Add a color field", + }, + "content-relationship": { + handler: fieldAddContentRelationship, + description: "Add a content relationship field", + }, + date: { + handler: fieldAddDate, + description: "Add a date field", + }, + embed: { + handler: fieldAddEmbed, + description: "Add an embed field", + }, + geopoint: { + handler: fieldAddGeopoint, + description: "Add a geopoint field", + }, + group: { + handler: fieldAddGroup, + description: "Add a group field", + }, + image: { + handler: fieldAddImage, + description: "Add an image field", + }, + integration: { + handler: fieldAddIntegration, + description: "Add an integration field", + }, + link: { + handler: fieldAddLink, + description: "Add a link field", + }, + "link-to-media": { + handler: fieldAddLinkToMedia, + description: "Add a link to media field", + }, + number: { + handler: fieldAddNumber, + description: "Add a number field", + }, + "rich-text": { + handler: fieldAddRichText, + description: "Add a rich text field", + }, + select: { + handler: fieldAddSelect, + description: "Add a select field", + }, + table: { + handler: fieldAddTable, + description: "Add a table field", + }, + text: { + handler: fieldAddText, + description: "Add a text field", + }, + timestamp: { + handler: fieldAddTimestamp, + description: "Add a timestamp field", + }, + uid: { + handler: fieldAddUid, + description: "Add a UID field", + }, + }, +}); diff --git a/src/commands/field-list.ts b/src/commands/field-list.ts new file mode 100644 index 0000000..da74057 --- /dev/null +++ b/src/commands/field-list.ts @@ -0,0 +1,45 @@ +import { getHost, getToken } from "../auth"; +import { createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { resolveModel, SOURCE_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field list", + description: "List fields in a slice or custom type.", + options: { + ...SOURCE_OPTIONS, + json: { type: "boolean", description: "Output as JSON" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ values }) => { + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [rawFields] = await resolveModel(values, { repo, token, host }); + + const fields = Object.entries(rawFields).map(([id, field]) => { + return { + id, + type: field.type, + label: field.config?.label || undefined, + }; + }); + + if (values.json) { + console.info(stringify(fields)); + return; + } + + if (fields.length === 0) { + console.info("No fields found."); + return; + } + + for (const field of fields) { + const label = field.label ? ` ${field.label}` : ""; + console.info(`${field.id} ${field.type}${label}`); + } +}); diff --git a/src/commands/field-remove.ts b/src/commands/field-remove.ts new file mode 100644 index 0000000..4665640 --- /dev/null +++ b/src/commands/field-remove.ts @@ -0,0 +1,28 @@ +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldTarget, resolveModel, SOURCE_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field remove", + description: "Remove a field from a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: SOURCE_OPTIONS, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + if (!(fieldId in targetFields)) throw new CommandError(`Field "${id}" does not exist.`); + delete targetFields[fieldId]; + await saveModel(); + + console.info(`Field removed: ${id}`); +}); diff --git a/src/commands/field.ts b/src/commands/field.ts new file mode 100644 index 0000000..3e0ce34 --- /dev/null +++ b/src/commands/field.ts @@ -0,0 +1,23 @@ +import { createCommandRouter } from "../lib/command"; +import fieldAdd from "./field-add"; +import fieldList from "./field-list"; +import fieldRemove from "./field-remove"; + +export default createCommandRouter({ + name: "prismic field", + description: "Manage fields in slices and custom types.", + commands: { + add: { + handler: fieldAdd, + description: "Add a field", + }, + list: { + handler: fieldList, + description: "List fields", + }, + remove: { + handler: fieldRemove, + description: "Remove a field", + }, + }, +}); diff --git a/src/index.ts b/src/index.ts index 99d5a07..b17912c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import locale from "./commands/locale"; import login from "./commands/login"; import logout from "./commands/logout"; import customType from "./commands/custom-type"; +import field from "./commands/field"; import pageType from "./commands/page-type"; import preview from "./commands/preview"; import repo from "./commands/repo"; @@ -70,6 +71,10 @@ const router = createCommandRouter({ handler: customType, description: "Manage custom types", }, + field: { + handler: field, + description: "Manage fields", + }, "page-type": { handler: pageType, description: "Manage page types", diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..184181c --- /dev/null +++ b/src/models.ts @@ -0,0 +1,179 @@ +import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; + +import type { CommandConfig } from "./lib/command"; + +import { + getCustomTypes, + getSlices, + updateCustomType, + updateSlice, +} from "./clients/custom-types"; +import { CommandError } from "./lib/command"; +import { UnknownRequestError } from "./lib/request"; + +type Field = DynamicWidget; +type Fields = Record; +type EntityType = "slice" | "customType"; +type ApiConfig = { repo: string; token: string | undefined; host: string }; +type Target = [fields: Fields, save: () => Promise, entityType: EntityType]; + +export const TARGET_OPTIONS = { + "to-slice": { type: "string", description: "Name of the target slice" }, + "to-page-type": { type: "string", description: "Name of the target page type" }, + "to-custom-type": { type: "string", description: "Name of the target custom type" }, + variation: { type: "string", description: 'Slice variation ID (default: "default")' }, + tab: { type: "string", description: 'Custom type tab name (default: "Main")' }, + repo: { type: "string", short: "r", description: "Repository domain" }, +} satisfies CommandConfig["options"]; + +export const SOURCE_OPTIONS = { + "from-slice": { type: "string", description: "Name of the source slice" }, + "from-page-type": { type: "string", description: "Name of the source page type" }, + "from-custom-type": { type: "string", description: "Name of the source custom type" }, + variation: TARGET_OPTIONS.variation, + tab: TARGET_OPTIONS.tab, + repo: TARGET_OPTIONS.repo, +} satisfies CommandConfig["options"]; + +export async function resolveModel( + values: { + "to-slice"?: string; + "to-page-type"?: string; + "to-custom-type"?: string; + "from-slice"?: string; + "from-page-type"?: string; + "from-custom-type"?: string; + variation?: string; + tab?: string; + }, + apiConfig: ApiConfig, +): Promise { + const sliceName = values["to-slice"] ?? values["from-slice"]; + const pageTypeName = values["to-page-type"] ?? values["from-page-type"]; + const customTypeName = values["to-custom-type"] ?? values["from-custom-type"]; + + const providedCount = [sliceName, pageTypeName, customTypeName].filter(Boolean).length; + if (providedCount === 0) { + throw new CommandError( + "Specify a target with --to-slice, --to-page-type, or --to-custom-type.", + ); + } + if (providedCount > 1) { + throw new CommandError( + "Only one of --to-slice, --to-page-type, or --to-custom-type can be specified.", + ); + } + + if (sliceName) { + if ("tab" in values) { + throw new CommandError("--tab is only valid for page types or custom types."); + } + + const variation = values.variation ?? "default"; + const slices = await getSlices(apiConfig); + const slice = slices.find((s) => s.name === sliceName); + if (!slice) { + throw new CommandError(`Slice not found: ${sliceName}`); + } + + const newModel = structuredClone(slice); + const newVariation = newModel.variations?.find((v) => v.id === variation); + if (!newVariation) { + const variationIds = slice.variations?.map((v) => v.id).join(", ") || "(none)"; + throw new CommandError(`Variation "${variation}" not found. Available: ${variationIds}`); + } + newVariation.primary ??= {}; + + return [ + newVariation.primary, + async () => { + try { + await updateSlice(newModel, apiConfig); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update slice: ${message}`); + } + throw error; + } + }, + "slice", + ]; + } + + // Page type or custom type + const name = pageTypeName ?? customTypeName; + const entityLabel = pageTypeName ? "Page type" : "Custom type"; + + if ("variation" in values) { + throw new CommandError("--variation is only valid for slices."); + } + + const tab = values.tab ?? "Main"; + const customTypes = await getCustomTypes(apiConfig); + const customType = customTypes.find((ct) => { + if (ct.label !== name) return false; + return pageTypeName ? ct.format === "page" : ct.format !== "page"; + }); + if (!customType) { + throw new CommandError(`${entityLabel} not found: ${name}`); + } + + const newModel = structuredClone(customType); + const newTab = newModel.json[tab]; + if (!newTab) { + const tabNames = Object.keys(customType.json).join(", ") || "(none)"; + throw new CommandError(`Tab "${tab}" not found. Available: ${tabNames}`); + } + + return [ + newTab, + async () => { + try { + await updateCustomType(newModel, apiConfig); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update ${entityLabel.toLowerCase()}: ${message}`); + } + throw error; + } + }, + "customType", + ]; +} + +export function resolveFieldTarget( + fields: Fields, + id: string, +): [targetFields: Fields, fieldId: string] { + if (!id.includes(".")) { + return [fields, id]; + } + + const segments = id.split("."); + const fieldId = segments.pop(); + if (!fieldId) { + throw new Error("This is a bug. We cannot continue."); + } + + let currentFields = fields; + for (const segment of segments) { + const field = currentFields[segment]; + + if (!field) { + throw new CommandError(`Field "${segment}" does not exist.`); + } + + if (field.type !== "Group") { + throw new CommandError(`Field "${segment}" is not a group field.`); + } + + field.config ??= {}; + field.config.fields ??= {}; + + currentFields = field.config.fields; + } + + return [currentFields, fieldId]; +} diff --git a/test/field-add-boolean.test.ts b/test/field-add-boolean.test.ts new file mode 100644 index 0000000..a08475b --- /dev/null +++ b/test/field-add-boolean.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "boolean", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add boolean [options]"); +}); + +it("adds a boolean field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "boolean", + "my_field", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_field"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_field; + expect(field).toMatchObject({ type: "Boolean" }); +}); + +it("adds a boolean field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "boolean", + "is_active", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: is_active"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.is_active; + expect(field).toMatchObject({ type: "Boolean" }); +}); diff --git a/test/field-add-color.test.ts b/test/field-add-color.test.ts new file mode 100644 index 0000000..7abdccc --- /dev/null +++ b/test/field-add-color.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "color", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add color [options]"); +}); + +it("adds a color field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "color", + "my_color", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_color"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_color; + expect(field).toMatchObject({ type: "Color" }); +}); + +it("adds a color field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "color", + "my_color", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_color"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_color; + expect(field).toMatchObject({ type: "Color" }); +}); diff --git a/test/field-add-content-relationship.test.ts b/test/field-add-content-relationship.test.ts new file mode 100644 index 0000000..c365b47 --- /dev/null +++ b/test/field-add-content-relationship.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "content-relationship", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add content-relationship [options]"); +}); + +it("adds a content relationship field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "content-relationship", + "my_link", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_link"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_link; + expect(field).toMatchObject({ type: "Link", config: { select: "document" } }); +}); + +it("adds a content relationship field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "content-relationship", + "my_link", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_link"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_link; + expect(field).toMatchObject({ type: "Link", config: { select: "document" } }); +}); diff --git a/test/field-add-date.test.ts b/test/field-add-date.test.ts new file mode 100644 index 0000000..997a623 --- /dev/null +++ b/test/field-add-date.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "date", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add date [options]"); +}); + +it("adds a date field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "date", + "my_date", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_date"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_date; + expect(field).toMatchObject({ type: "Date" }); +}); + +it("adds a date field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "date", + "my_date", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_date"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_date; + expect(field).toMatchObject({ type: "Date" }); +}); diff --git a/test/field-add-embed.test.ts b/test/field-add-embed.test.ts new file mode 100644 index 0000000..ab834ce --- /dev/null +++ b/test/field-add-embed.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "embed", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add embed [options]"); +}); + +it("adds an embed field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "embed", + "my_embed", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_embed"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_embed; + expect(field).toMatchObject({ type: "Embed" }); +}); + +it("adds an embed field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "embed", + "my_embed", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_embed"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_embed; + expect(field).toMatchObject({ type: "Embed" }); +}); diff --git a/test/field-add-geopoint.test.ts b/test/field-add-geopoint.test.ts new file mode 100644 index 0000000..4d50574 --- /dev/null +++ b/test/field-add-geopoint.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "geopoint", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add geopoint [options]"); +}); + +it("adds a geopoint field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "geopoint", + "my_location", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_location"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_location; + expect(field).toMatchObject({ type: "GeoPoint" }); +}); + +it("adds a geopoint field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "geopoint", + "my_location", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_location"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_location; + expect(field).toMatchObject({ type: "GeoPoint" }); +}); diff --git a/test/field-add-group.test.ts b/test/field-add-group.test.ts new file mode 100644 index 0000000..fc2915f --- /dev/null +++ b/test/field-add-group.test.ts @@ -0,0 +1,122 @@ +import type { Group } from "@prismicio/types-internal/lib/customtypes"; + +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "group", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add group [options]"); +}); + +it("adds a group field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "group", + "my_group", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_group"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_group; + expect(field).toMatchObject({ type: "Group" }); +}); + +it("adds a group field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "group", + "my_group", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_group"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_group; + expect(field).toMatchObject({ type: "Group" }); +}); + +it("adds a field inside a group using dot syntax", async ({ + expect, + prismic, + repo, + token, + host, +}) => { + const slice = buildSlice(); + slice.variations[0].primary!.my_group = { type: "Group", config: { label: "My Group" } }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "text", + "my_group.subtitle", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_group.subtitle"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const group = updated!.variations[0].primary!.my_group as Group; + expect(group.config?.fields).toMatchObject({ + subtitle: { type: "Text", config: { label: "Subtitle" } }, + }); +}); + +it("errors when dot syntax targets a non-existent field", async ({ + expect, + prismic, + repo, + token, + host, +}) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stderr, exitCode } = await prismic("field", [ + "add", + "text", + "nonexistent.subtitle", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(1); + expect(stderr).toContain('Field "nonexistent" does not exist.'); +}); + +it("errors when dot syntax targets a non-group field", async ({ + expect, + prismic, + repo, + token, + host, +}) => { + const slice = buildSlice(); + slice.variations[0].primary!.my_text = { type: "Text", config: { label: "My Text" } }; + await insertSlice(slice, { repo, token, host }); + + const { stderr, exitCode } = await prismic("field", [ + "add", + "text", + "my_text.subtitle", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(1); + expect(stderr).toContain('Field "my_text" is not a group field.'); +}); diff --git a/test/field-add-image.test.ts b/test/field-add-image.test.ts new file mode 100644 index 0000000..b06e515 --- /dev/null +++ b/test/field-add-image.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "image", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add image [options]"); +}); + +it("adds an image field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "image", + "my_image", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_image"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_image; + expect(field).toMatchObject({ type: "Image" }); +}); + +it("adds an image field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "image", + "my_image", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_image"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_image; + expect(field).toMatchObject({ type: "Image" }); +}); diff --git a/test/field-add-integration.test.ts b/test/field-add-integration.test.ts new file mode 100644 index 0000000..c14cd2a --- /dev/null +++ b/test/field-add-integration.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "integration", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add integration [options]"); +}); + +it("adds an integration field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "integration", + "my_integration", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_integration"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_integration; + expect(field).toMatchObject({ type: "IntegrationFields" }); +}); + +it("adds an integration field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "integration", + "my_integration", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_integration"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_integration; + expect(field).toMatchObject({ type: "IntegrationFields" }); +}); diff --git a/test/field-add-link-to-media.test.ts b/test/field-add-link-to-media.test.ts new file mode 100644 index 0000000..97cb812 --- /dev/null +++ b/test/field-add-link-to-media.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "link-to-media", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add link-to-media [options]"); +}); + +it("adds a link to media field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "link-to-media", + "my_media", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_media"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_media; + expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); +}); + +it("adds a link to media field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "link-to-media", + "my_media", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_media"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_media; + expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); +}); diff --git a/test/field-add-link.test.ts b/test/field-add-link.test.ts new file mode 100644 index 0000000..8439069 --- /dev/null +++ b/test/field-add-link.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "link", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add link [options]"); +}); + +it("adds a link field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "link", + "my_link", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_link"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_link; + expect(field).toMatchObject({ type: "Link" }); +}); + +it("adds a link field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "link", + "my_link", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_link"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_link; + expect(field).toMatchObject({ type: "Link" }); +}); diff --git a/test/field-add-number.test.ts b/test/field-add-number.test.ts new file mode 100644 index 0000000..7237ee8 --- /dev/null +++ b/test/field-add-number.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "number", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add number [options]"); +}); + +it("adds a number field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "number", + "my_number", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_number"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_number; + expect(field).toMatchObject({ type: "Number" }); +}); + +it("adds a number field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "number", + "my_number", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_number"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_number; + expect(field).toMatchObject({ type: "Number" }); +}); diff --git a/test/field-add-rich-text.test.ts b/test/field-add-rich-text.test.ts new file mode 100644 index 0000000..9368104 --- /dev/null +++ b/test/field-add-rich-text.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "rich-text", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add rich-text [options]"); +}); + +it("adds a rich text field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "rich-text", + "my_content", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_content"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_content; + expect(field).toMatchObject({ type: "StructuredText" }); +}); + +it("adds a rich text field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "rich-text", + "my_content", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_content"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_content; + expect(field).toMatchObject({ type: "StructuredText" }); +}); diff --git a/test/field-add-select.test.ts b/test/field-add-select.test.ts new file mode 100644 index 0000000..6778c46 --- /dev/null +++ b/test/field-add-select.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "select", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add select [options]"); +}); + +it("adds a select field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "select", + "my_select", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_select"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_select; + expect(field).toMatchObject({ type: "Select" }); +}); + +it("adds a select field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "select", + "my_select", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_select"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_select; + expect(field).toMatchObject({ type: "Select" }); +}); diff --git a/test/field-add-table.test.ts b/test/field-add-table.test.ts new file mode 100644 index 0000000..b4616e4 --- /dev/null +++ b/test/field-add-table.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "table", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add table [options]"); +}); + +it("adds a table field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "table", + "my_table", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_table"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_table; + expect(field).toMatchObject({ type: "Table" }); +}); + +it("adds a table field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "table", + "my_table", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_table"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_table; + expect(field).toMatchObject({ type: "Table" }); +}); diff --git a/test/field-add-text.test.ts b/test/field-add-text.test.ts new file mode 100644 index 0000000..369dfdd --- /dev/null +++ b/test/field-add-text.test.ts @@ -0,0 +1,68 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "text", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add text [options]"); +}); + +it("adds a text field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "text", + "subtitle", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: subtitle"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.subtitle; + expect(field).toMatchObject({ type: "Text" }); +}); + +it("adds a text field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "text", + "subtitle", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: subtitle"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.subtitle; + expect(field).toMatchObject({ type: "Text" }); +}); + +it("adds a text field to a page type", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "text", + "subtitle", + "--to-page-type", + pageType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: subtitle"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === pageType.id); + const field = updated!.json.Main.subtitle; + expect(field).toMatchObject({ type: "Text" }); +}); diff --git a/test/field-add-timestamp.test.ts b/test/field-add-timestamp.test.ts new file mode 100644 index 0000000..90ce657 --- /dev/null +++ b/test/field-add-timestamp.test.ts @@ -0,0 +1,48 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "timestamp", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add timestamp [options]"); +}); + +it("adds a timestamp field to a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "timestamp", + "my_timestamp", + "--to-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_timestamp"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_timestamp; + expect(field).toMatchObject({ type: "Timestamp" }); +}); + +it("adds a timestamp field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "timestamp", + "my_timestamp", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_timestamp"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.my_timestamp; + expect(field).toMatchObject({ type: "Timestamp" }); +}); diff --git a/test/field-add-uid.test.ts b/test/field-add-uid.test.ts new file mode 100644 index 0000000..1fad76c --- /dev/null +++ b/test/field-add-uid.test.ts @@ -0,0 +1,46 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "uid", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add uid [options]"); +}); + +it("adds a uid field to a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "uid", + "--to-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: uid"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const field = updated!.json.Main.uid; + expect(field).toMatchObject({ type: "UID" }); +}); + +it("adds a uid field to a page type", async ({ expect, prismic, repo, token, host }) => { + const pageType = buildCustomType({ format: "page" }); + await insertCustomType(pageType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "uid", + "--to-page-type", + pageType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: uid"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === pageType.id); + const field = updated!.json.Main.uid; + expect(field).toMatchObject({ type: "UID" }); +}); diff --git a/test/field-add.test.ts b/test/field-add.test.ts new file mode 100644 index 0000000..b60ff4e --- /dev/null +++ b/test/field-add.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["add", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field add [options]"); +}); diff --git a/test/field-list.test.ts b/test/field-list.test.ts new file mode 100644 index 0000000..40a7de0 --- /dev/null +++ b/test/field-list.test.ts @@ -0,0 +1,86 @@ +import { buildSlice, it } from "./it"; +import { insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["list", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field list [options]"); +}); + +it("lists fields in a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice({ + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + title: { type: "StructuredText", config: { label: "Title" } }, + is_active: { type: "Boolean", config: { label: "Is Active" } }, + }, + }, + ], + }); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "list", + "--from-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("title"); + expect(stdout).toContain("StructuredText"); + expect(stdout).toContain("is_active"); + expect(stdout).toContain("Boolean"); +}); + +it("lists fields as JSON with --json", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice({ + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + title: { type: "StructuredText", config: { label: "Title" } }, + }, + }, + ], + }); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "list", + "--from-slice", + slice.name, + "--json", + ]); + expect(exitCode).toBe(0); + + const fields = JSON.parse(stdout); + expect(fields).toContainEqual({ + id: "title", + type: "StructuredText", + label: "Title", + }); +}); + +it("prints message when no fields exist", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "list", + "--from-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("No fields found."); +}); diff --git a/test/field-remove.test.ts b/test/field-remove.test.ts new file mode 100644 index 0000000..1150ee9 --- /dev/null +++ b/test/field-remove.test.ts @@ -0,0 +1,108 @@ +import type { Group } from "@prismicio/types-internal/lib/customtypes"; + +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["remove", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field remove [options]"); +}); + +it("removes a field from a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice({ + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + my_field: { type: "Boolean", config: { label: "My Field" } }, + }, + }, + ], + }); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "remove", + "my_field", + "--from-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field removed: my_field"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + expect(updated!.variations[0].primary!.my_field).toBeUndefined(); +}); + +it("removes a field from a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ + json: { + Main: { + title: { type: "StructuredText", config: { label: "Title" } }, + }, + }, + }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "remove", + "title", + "--from-custom-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field removed: title"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated!.json.Main.title).toBeUndefined(); +}); + +it("removes a nested field using dot notation", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice({ + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + my_group: { + type: "Group", + config: { + label: "My Group", + fields: { + subtitle: { type: "StructuredText", config: { label: "Subtitle" } }, + }, + }, + }, + }, + }, + ], + }); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "remove", + "my_group.subtitle", + "--from-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field removed: my_group.subtitle"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const group = updated!.variations[0].primary!.my_group as Group; + expect(group.config?.fields).toMatchObject({}); + expect(group.config?.fields?.subtitle).toBeUndefined(); +}); diff --git a/test/field.test.ts b/test/field.test.ts new file mode 100644 index 0000000..040421b --- /dev/null +++ b/test/field.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field [options]"); +}); From 56a739ebda9a8fc274ef385c8e4ee2d3df24ffec Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 30 Mar 2026 18:25:41 -1000 Subject: [PATCH 06/29] fix: add missing primary field to test slice builder Co-Authored-By: Claude Opus 4.6 (1M context) --- test/it.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/it.ts b/test/it.ts index 096b60f..007523b 100644 --- a/test/it.ts +++ b/test/it.ts @@ -152,6 +152,7 @@ export function buildSlice(overrides?: Partial): SharedSlice { version: "initial", description: "Default", imageUrl: "", + primary: {}, }, ], ...overrides, From 9cfd1c425f9704fa37b2816fefb6f157a3b19193 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 31 Mar 2026 11:35:34 -1000 Subject: [PATCH 07/29] feat: add field edit command for remote modeling Adds `prismic field edit` to modify existing field properties (label, placeholder, type-specific options) on slices and custom types via the API. Extracts shared `resolveFieldContainer` from `resolveModel` for reuse across field-edit and field-remove. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/field-edit.ts | 183 ++++++++++++++++++++++++++++++++++ src/commands/field-remove.ts | 4 +- src/commands/field.ts | 5 + src/models.ts | 74 ++++++++++++-- test/field-edit.test.ts | 186 +++++++++++++++++++++++++++++++++++ 5 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 src/commands/field-edit.ts create mode 100644 test/field-edit.test.ts diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts new file mode 100644 index 0000000..5540d5b --- /dev/null +++ b/src/commands/field-edit.ts @@ -0,0 +1,183 @@ +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field edit", + description: "Edit an existing field in a slice or custom type.", + sections: { + "FIELD TYPE OPTIONS": ` + Options vary by field type. Only options matching the field's + type will be applied. See \`prismic field add --help\` + for type-specific option details. + `, + }, + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...SOURCE_OPTIONS, + // Universal + label: { type: "string", description: "Field label" }, + placeholder: { type: "string", description: "Placeholder text" }, + // Boolean + "default-value": { + type: "string", + description: "Default value (boolean: true/false, select: option value)", + }, + "true-label": { type: "string", description: "Label for true value (boolean)" }, + "false-label": { type: "string", description: "Label for false value (boolean)" }, + // Date / Timestamp + default: { type: "string", description: "Default value (date/timestamp)" }, + // Number + min: { type: "string", description: "Minimum value (number)" }, + max: { type: "string", description: "Maximum value (number)" }, + step: { type: "string", description: "Step increment (number)" }, + // Select + option: { + type: "string", + multiple: true, + description: "Select option value (can be repeated)", + }, + // Link + "allow-target-blank": { + type: "boolean", + description: "Allow opening in new tab (link/rich-text)", + }, + "allow-text": { + type: "boolean", + description: "Allow custom link text (link/link-to-media)", + }, + repeatable: { type: "boolean", description: "Allow multiple links (link)" }, + variant: { + type: "string", + multiple: true, + description: "Allowed variant (link/link-to-media, can be repeated)", + }, + // Content Relationship + tag: { + type: "string", + multiple: true, + description: "Allowed tag (content-relationship, can be repeated)", + }, + "custom-type": { + type: "string", + multiple: true, + description: "Allowed custom type (content-relationship, can be repeated)", + }, + // Rich Text + allow: { + type: "string", + description: "Comma-separated allowed block types (rich-text)", + }, + single: { type: "boolean", description: "Restrict to a single block (rich-text)" }, + // Integration + catalog: { type: "string", description: "Integration catalog ID (integration)" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field = targetFields[fieldId]; + field.config ??= {}; + + if ("label" in values) field.config.label = values.label; + if ("placeholder" in values && "placeholder" in field.config) + field.config.placeholder = values.placeholder; + + switch (field.type) { + case "Boolean": { + if ("default-value" in values) { + const raw = values["default-value"]; + if (raw !== "true" && raw !== "false") { + throw new CommandError('--default-value for boolean fields must be "true" or "false"'); + } + field.config.default_value = raw === "true"; + } + if ("true-label" in values) field.config.placeholder_true = values["true-label"]; + if ("false-label" in values) field.config.placeholder_false = values["false-label"]; + break; + } + case "Number": { + if ("min" in values) field.config.min = parseNumber(values.min, "min"); + if ("max" in values) field.config.max = parseNumber(values.max, "max"); + if ("step" in values) field.config.step = parseNumber(values.step, "step"); + break; + } + case "Select": { + if ("default-value" in values) field.config.default_value = values["default-value"]; + if ("option" in values) field.config.options = values.option; + break; + } + case "Date": + case "Timestamp": { + if ("default" in values) field.config.default = values.default; + break; + } + case "IntegrationFields": { + if ("catalog" in values) field.config.catalog = values.catalog; + break; + } + case "StructuredText": { + if ("allow-target-blank" in values) { + field.config.allowTargetBlank = values["allow-target-blank"]; + } + if ("single" in values) { + // Switch from multi to single mode + const allowList = + "allow" in values ? values.allow : (field.config.multi ?? field.config.single); + delete field.config.multi; + field.config.single = allowList; + } else if ("allow" in values) { + // Update whichever mode is currently set + if ("single" in config) { + config.single = values.allow; + } else { + field.config.multi = values.allow; + } + } + break; + } + case "Link": { + if (field.config.select === "document") { + // Content relationship + if ("tag" in values) field.config.tags = values.tag; + if ("custom-type" in values) field.config.customtypes = values["custom-type"]; + } else if (field.config.select === "media") { + // Link to media + if ("allow-text" in values) field.config.allowText = values["allow-text"]; + if ("variant" in values) field.config.variants = values.variant; + } else { + // Generic link + if ("allow-target-blank" in values) { + field.config.allowTargetBlank = values["allow-target-blank"]; + } + if ("allow-text" in values) field.config.allowText = values["allow-text"]; + if ("repeatable" in values) field.config.repeat = values.repeatable; + if ("variant" in values) field.config.variants = values.variant; + } + break; + } + } + + await saveModel(); + + console.info(`Field updated: ${id}`); +}); + +function parseNumber(value: string | undefined, optionName: string): number | undefined { + if (value === undefined) return undefined; + const number = Number(value); + if (Number.isNaN(number)) { + throw new CommandError(`--${optionName} must be a valid number, got "${value}"`); + } + return number; +} diff --git a/src/commands/field-remove.ts b/src/commands/field-remove.ts index 4665640..75ddfa5 100644 --- a/src/commands/field-remove.ts +++ b/src/commands/field-remove.ts @@ -1,6 +1,6 @@ import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { resolveFieldTarget, resolveModel, SOURCE_OPTIONS } from "../models"; +import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; import { getRepositoryName } from "../project"; const config = { @@ -18,7 +18,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); - const [fields, saveModel] = await resolveModel(values, { repo, token, host }); + const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); const [targetFields, fieldId] = resolveFieldTarget(fields, id); if (!(fieldId in targetFields)) throw new CommandError(`Field "${id}" does not exist.`); delete targetFields[fieldId]; diff --git a/src/commands/field.ts b/src/commands/field.ts index 3e0ce34..ba030e1 100644 --- a/src/commands/field.ts +++ b/src/commands/field.ts @@ -1,5 +1,6 @@ import { createCommandRouter } from "../lib/command"; import fieldAdd from "./field-add"; +import fieldEdit from "./field-edit"; import fieldList from "./field-list"; import fieldRemove from "./field-remove"; @@ -11,6 +12,10 @@ export default createCommandRouter({ handler: fieldAdd, description: "Add a field", }, + edit: { + handler: fieldEdit, + description: "Edit a field", + }, list: { handler: fieldList, description: "List fields", diff --git a/src/models.ts b/src/models.ts index 184181c..d46a381 100644 --- a/src/models.ts +++ b/src/models.ts @@ -2,12 +2,7 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; import type { CommandConfig } from "./lib/command"; -import { - getCustomTypes, - getSlices, - updateCustomType, - updateSlice, -} from "./clients/custom-types"; +import { getCustomTypes, getSlices, updateCustomType, updateSlice } from "./clients/custom-types"; import { CommandError } from "./lib/command"; import { UnknownRequestError } from "./lib/request"; @@ -35,6 +30,73 @@ export const SOURCE_OPTIONS = { repo: TARGET_OPTIONS.repo, } satisfies CommandConfig["options"]; +export async function resolveFieldContainer( + id: string, + values: { + "from-slice"?: string; + "from-page-type"?: string; + "from-custom-type"?: string; + variation?: string; + }, + apiConfig: ApiConfig, +): Promise { + const { + "from-slice": fromSlice, + "from-page-type": fromPageType, + "from-custom-type": fromCustomType, + variation: variationId = "default", + } = values; + + const providedCount = [fromSlice, fromPageType, fromCustomType].filter(Boolean).length; + if (providedCount === 0) { + throw new CommandError( + "Specify a target with --from-slice, --from-page-type, or --from-custom-type.", + ); + } + if (providedCount > 1) { + throw new CommandError( + "Only one of --from-slice, --from-page-type, or --from-custom-type can be specified.", + ); + } + + if (fromSlice) { + const slices = await getSlices(apiConfig); + const slice = slices.find((s) => s.name === fromSlice); + if (!slice) { + throw new CommandError(`Slice not found: ${fromSlice}`); + } + const variation = slice.variations.find((v) => v.id === variationId); + if (!variation) { + const variationIds = slice.variations?.map((v) => v.id).join(", ") || "(none)"; + throw new CommandError(`Variation "${variation}" not found. Available: ${variationIds}`); + } + variation.primary ??= {}; + resolveFieldTarget(variation.primary, id); + return [variation.primary, () => updateSlice(slice, apiConfig), "slice"]; + } + + const fromType = fromPageType || fromCustomType; + const entityLabel = fromPageType ? "Page type" : "Custom type"; + const customTypes = await getCustomTypes(apiConfig); + const customType = customTypes.find((ct) => { + if (ct.label !== fromType) return false; + return fromPageType ? ct.format === "page" : ct.format !== "page"; + }); + if (!customType) { + throw new CommandError(`${entityLabel} not found: ${fromType}`); + } + let tab: Record | undefined; + for (const tabName in customType.json) { + if (id in customType.json[tabName]) tab = customType.json[tabName]; + } + if (!tab) { + const fieldIds = Object.keys(Object.assign({}, ...Object.values(customType.json))) || "(none)"; + throw new CommandError(`Field "${id}" not found. Available: ${fieldIds}`); + } + resolveFieldTarget(tab, id); + return [tab, () => updateCustomType(customType, apiConfig), "customType"]; +} + export async function resolveModel( values: { "to-slice"?: string; diff --git a/test/field-edit.test.ts b/test/field-edit.test.ts new file mode 100644 index 0000000..8a98fd4 --- /dev/null +++ b/test/field-edit.test.ts @@ -0,0 +1,186 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["edit", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field edit [options]"); +}); + +it("edits a field label on a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.my_field = { type: "Boolean", config: { label: "Old Label" } }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "my_field", + "--from-slice", + slice.name, + "--label", + "New Label", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: my_field"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_field; + expect(field).toMatchObject({ type: "Boolean", config: { label: "New Label" } }); +}); + +it("edits a field label on a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ + json: { + Main: { + title: { + type: "StructuredText", + config: { label: "Title", single: "heading1" }, + }, + }, + }, + }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "title", + "--from-custom-type", + customType.label!, + "--label", + "Page Title", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: title"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated!.json.Main.title).toMatchObject({ + type: "StructuredText", + config: { label: "Page Title" }, + }); +}); + +it("edits boolean field options", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.is_active = { type: "Boolean", config: { label: "Active" } }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "is_active", + "--from-slice", + slice.name, + "--default-value", + "true", + "--true-label", + "Yes", + "--false-label", + "No", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: is_active"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.is_active; + expect(field).toMatchObject({ + type: "Boolean", + config: { + default_value: true, + placeholder_true: "Yes", + placeholder_false: "No", + }, + }); +}); + +it("edits number field options", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.quantity = { type: "Number", config: { label: "Quantity" } }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "quantity", + "--from-slice", + slice.name, + "--min", + "0", + "--max", + "100", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: quantity"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.quantity; + expect(field).toMatchObject({ + type: "Number", + config: { min: 0, max: 100 }, + }); +}); + +it("edits select field options", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.color = { + type: "Select", + config: { label: "Color", options: ["red", "blue"] }, + }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "color", + "--from-slice", + slice.name, + "--default-value", + "green", + "--option", + "red", + "--option", + "green", + "--option", + "blue", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: color"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.color; + expect(field).toMatchObject({ + type: "Select", + config: { + default_value: "green", + options: ["red", "green", "blue"], + }, + }); +}); + +it("edits link field options", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.cta_link = { + type: "Link", + config: { label: "CTA Link", allowTargetBlank: false }, + }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "cta_link", + "--from-slice", + slice.name, + "--allow-target-blank", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: cta_link"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.cta_link; + expect(field).toMatchObject({ + type: "Link", + config: { allowTargetBlank: true }, + }); +}); From 8c765e399bd82b4ee2c8d24937cc8888f0dd228e Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 31 Mar 2026 13:31:50 -1000 Subject: [PATCH 08/29] feat: move sync logic into Adapter and sync after modeling commands Consolidates sync logic (syncModels, syncSlices, syncCustomTypes) into the Adapter class so all modeling commands can sync local files after remote changes. This ensures local models stay up-to-date after any create, remove, or field mutation command. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/adapters/index.ts | 84 ++++++++++++++++++ src/commands/custom-type-create.ts | 4 + src/commands/custom-type-remove.ts | 4 + src/commands/field-add-boolean.ts | 4 + src/commands/field-add-color.ts | 4 + .../field-add-content-relationship.ts | 17 ++-- src/commands/field-add-date.ts | 4 + src/commands/field-add-embed.ts | 4 + src/commands/field-add-geopoint.ts | 4 + src/commands/field-add-group.ts | 4 + src/commands/field-add-image.ts | 4 + src/commands/field-add-integration.ts | 4 + src/commands/field-add-link-to-media.ts | 4 + src/commands/field-add-link.ts | 4 + src/commands/field-add-number.ts | 4 + src/commands/field-add-rich-text.ts | 4 + src/commands/field-add-select.ts | 4 + src/commands/field-add-table.ts | 4 + src/commands/field-add-text.ts | 4 + src/commands/field-add-timestamp.ts | 4 + src/commands/field-add-uid.ts | 4 + src/commands/field-edit.ts | 4 + src/commands/field-remove.ts | 4 + src/commands/init.ts | 13 +-- src/commands/page-type-create.ts | 4 + src/commands/page-type-remove.ts | 4 + src/commands/slice-add-variation.ts | 4 + src/commands/slice-connect.ts | 4 + src/commands/slice-create.ts | 4 + src/commands/slice-disconnect.ts | 4 + src/commands/slice-remove-variation.ts | 4 + src/commands/slice-remove.ts | 4 + src/commands/sync.ts | 87 ++----------------- src/models.ts | 4 +- 34 files changed, 222 insertions(+), 99 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 2568408..ee50322 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { generateTypes } from "prismic-ts-codegen"; import { glob } from "tinyglobby"; +import { getCustomTypes, getSlices } from "../clients/custom-types"; import { addRoute, removeRoute, updateRoute } from "../config"; import { readJsonFile, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; @@ -180,6 +181,89 @@ export abstract class Adapter { await this.onCustomTypeDeleted(id); } + async syncModels(config: { + repo: string; + token: string | undefined; + host: string; + }): Promise { + const { repo, token, host } = config; + await Promise.all([ + this.syncSlices({ repo, token, host, generateTypes: false }), + this.syncCustomTypes({ repo, token, host, generateTypes: false }), + ]); + await this.generateTypes(); + } + + async syncSlices(config: { + repo: string; + token: string | undefined; + host: string; + generateTypes?: boolean; + }): Promise { + const { repo, token, host, generateTypes = true } = config; + + const remoteSlices = await getSlices({ repo, token, host }); + const localSlices = await this.getSlices(); + + // Handle slices update + for (const remoteSlice of remoteSlices) { + const localSlice = localSlices.find((slice) => slice.model.id === remoteSlice.id); + if (localSlice) await this.updateSlice(remoteSlice); + } + + // Handle slices deletion + for (const localSlice of localSlices) { + const existsRemotely = remoteSlices.some((slice) => slice.id === localSlice.model.id); + if (!existsRemotely) await this.deleteSlice(localSlice.model.id); + } + + // Handle slices creation + for (const remoteSlice of remoteSlices) { + const existsLocally = localSlices.some((slice) => slice.model.id === remoteSlice.id); + if (!existsLocally) await this.createSlice(remoteSlice); + } + + if (generateTypes) await this.generateTypes(); + } + + async syncCustomTypes(config: { + repo: string; + token: string | undefined; + host: string; + generateTypes?: boolean; + }): Promise { + const { repo, token, host, generateTypes = true } = config; + + const remoteCustomTypes = await getCustomTypes({ repo, token, host }); + const localCustomTypes = await this.getCustomTypes(); + + // Handle custom types update + for (const remoteCustomType of remoteCustomTypes) { + const localCustomType = localCustomTypes.find( + (customType) => customType.model.id === remoteCustomType.id, + ); + if (localCustomType) await this.updateCustomType(remoteCustomType); + } + + // Handle custom types deletion + for (const localCustomType of localCustomTypes) { + const existsRemotely = remoteCustomTypes.some( + (customType) => customType.id === localCustomType.model.id, + ); + if (!existsRemotely) await this.deleteCustomType(localCustomType.model.id); + } + + // Handle custom types creation + for (const remoteCustomType of remoteCustomTypes) { + const existsLocally = localCustomTypes.some( + (customType) => customType.model.id === remoteCustomType.id, + ); + if (!existsLocally) await this.createCustomType(remoteCustomType); + } + + if (generateTypes) await this.generateTypes(); + } + async generateTypes(): Promise { const projectRoot = await findProjectRoot(); const output = new URL(TYPES_FILENAME, projectRoot); diff --git a/src/commands/custom-type-create.ts b/src/commands/custom-type-create.ts index dd1e6d0..7603047 100644 --- a/src/commands/custom-type-create.ts +++ b/src/commands/custom-type-create.ts @@ -2,6 +2,7 @@ import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; import { snakeCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { insertCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -36,6 +37,7 @@ export default createCommand(config, async ({ positionals, values }) => { }, }; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); @@ -49,5 +51,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Created custom type "${name}" (id: "${id}")`); }); diff --git a/src/commands/custom-type-remove.ts b/src/commands/custom-type-remove.ts index e5154ab..c6af7b3 100644 --- a/src/commands/custom-type-remove.ts +++ b/src/commands/custom-type-remove.ts @@ -1,3 +1,4 @@ +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getCustomTypes, removeCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -19,6 +20,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); @@ -44,5 +46,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Custom type removed: "${name}" (id: ${customType.id})`); }); diff --git a/src/commands/field-add-boolean.ts b/src/commands/field-add-boolean.ts index 6f856a9..976a0fc 100644 --- a/src/commands/field-add-boolean.ts +++ b/src/commands/field-add-boolean.ts @@ -2,6 +2,7 @@ import type { BooleanField } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -32,6 +33,7 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -51,5 +53,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-color.ts b/src/commands/field-add-color.ts index ac0daf6..c156933 100644 --- a/src/commands/field-add-color.ts +++ b/src/commands/field-add-color.ts @@ -2,6 +2,7 @@ import type { Color } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,6 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,5 +43,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts index 7eb46e4..ae17ef2 100644 --- a/src/commands/field-add-content-relationship.ts +++ b/src/commands/field-add-content-relationship.ts @@ -2,6 +2,7 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -17,19 +18,19 @@ const config = { ...TARGET_OPTIONS, label: { type: "string", description: "Field label" }, tag: { type: "string", multiple: true, description: "Allowed tag (can be repeated)" }, - "custom-type": { type: "string", multiple: true, description: "Allowed custom type (can be repeated)" }, + "custom-type": { + type: "string", + multiple: true, + description: "Allowed custom type (can be repeated)", + }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; - const { - label, - tag: tags, - "custom-type": customtypes, - repo = await getRepositoryName(), - } = values; + const { label, tag: tags, "custom-type": customtypes, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -49,5 +50,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-date.ts b/src/commands/field-add-date.ts index f0d3c8a..a0b846b 100644 --- a/src/commands/field-add-date.ts +++ b/src/commands/field-add-date.ts @@ -2,6 +2,7 @@ import type { Date as DateField } from "@prismicio/types-internal/lib/customtype import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,6 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,5 +45,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-embed.ts b/src/commands/field-add-embed.ts index c58a249..1edd2e8 100644 --- a/src/commands/field-add-embed.ts +++ b/src/commands/field-add-embed.ts @@ -2,6 +2,7 @@ import type { Embed } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,6 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,5 +43,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-geopoint.ts b/src/commands/field-add-geopoint.ts index 07d8e76..6460922 100644 --- a/src/commands/field-add-geopoint.ts +++ b/src/commands/field-add-geopoint.ts @@ -2,6 +2,7 @@ import type { GeoPoint } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -23,6 +24,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -39,5 +41,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-group.ts b/src/commands/field-add-group.ts index 6b5a0e6..6fc7cfe 100644 --- a/src/commands/field-add-group.ts +++ b/src/commands/field-add-group.ts @@ -2,6 +2,7 @@ import type { Group } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -23,6 +24,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -39,5 +41,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-image.ts b/src/commands/field-add-image.ts index ea19993..894aa16 100644 --- a/src/commands/field-add-image.ts +++ b/src/commands/field-add-image.ts @@ -2,6 +2,7 @@ import type { Image } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,6 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,5 +43,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-integration.ts b/src/commands/field-add-integration.ts index 5489e54..279a7a9 100644 --- a/src/commands/field-add-integration.ts +++ b/src/commands/field-add-integration.ts @@ -2,6 +2,7 @@ import type { IntegrationField } from "@prismicio/types-internal/lib/customtypes import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,6 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, catalog, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,5 +45,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-link-to-media.ts b/src/commands/field-add-link-to-media.ts index 1d62691..3a34747 100644 --- a/src/commands/field-add-link-to-media.ts +++ b/src/commands/field-add-link-to-media.ts @@ -2,6 +2,7 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -30,6 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -49,5 +51,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-link.ts b/src/commands/field-add-link.ts index 9bd07eb..b581f03 100644 --- a/src/commands/field-add-link.ts +++ b/src/commands/field-add-link.ts @@ -2,6 +2,7 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -34,6 +35,7 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -54,5 +56,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-number.ts b/src/commands/field-add-number.ts index 68756d4..f5ef799 100644 --- a/src/commands/field-add-number.ts +++ b/src/commands/field-add-number.ts @@ -2,6 +2,7 @@ import type { Number as NumberField } from "@prismicio/types-internal/lib/custom import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -31,6 +32,7 @@ export default createCommand(config, async ({ positionals, values }) => { const max = parseNumber(values.max, "max"); const step = parseNumber(values.step, "step"); + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -51,6 +53,8 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-rich-text.ts b/src/commands/field-add-rich-text.ts index 61d4a65..202fdd9 100644 --- a/src/commands/field-add-rich-text.ts +++ b/src/commands/field-add-rich-text.ts @@ -2,6 +2,7 @@ import type { RichText } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -47,6 +48,7 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -66,5 +68,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-select.ts b/src/commands/field-add-select.ts index 683b1d6..c63aea2 100644 --- a/src/commands/field-add-select.ts +++ b/src/commands/field-add-select.ts @@ -2,6 +2,7 @@ import type { Select } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -32,6 +33,7 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -51,5 +53,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-table.ts b/src/commands/field-add-table.ts index 624d94b..c6b3723 100644 --- a/src/commands/field-add-table.ts +++ b/src/commands/field-add-table.ts @@ -2,6 +2,7 @@ import type { Table } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -23,6 +24,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -39,5 +41,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-text.ts b/src/commands/field-add-text.ts index 3787517..6510449 100644 --- a/src/commands/field-add-text.ts +++ b/src/commands/field-add-text.ts @@ -2,6 +2,7 @@ import type { Text } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,6 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,5 +43,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-timestamp.ts b/src/commands/field-add-timestamp.ts index 669dc12..6d30ef3 100644 --- a/src/commands/field-add-timestamp.ts +++ b/src/commands/field-add-timestamp.ts @@ -2,6 +2,7 @@ import type { Timestamp } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,6 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,5 +45,7 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-uid.ts b/src/commands/field-add-uid.ts index f44b8bb..fb949a1 100644 --- a/src/commands/field-add-uid.ts +++ b/src/commands/field-add-uid.ts @@ -1,5 +1,6 @@ import type { UID } from "@prismicio/types-internal/lib/customtypes"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveModel } from "../models"; @@ -21,6 +22,7 @@ const config = { export default createCommand(config, async ({ values }) => { const { label = "UID", placeholder, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -37,5 +39,7 @@ export default createCommand(config, async ({ values }) => { fields.uid = field; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info("Field added: uid"); }); diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index 5540d5b..0bce527 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -1,3 +1,4 @@ +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; @@ -81,6 +82,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); @@ -170,6 +172,8 @@ export default createCommand(config, async ({ positionals, values }) => { await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field updated: ${id}`); }); diff --git a/src/commands/field-remove.ts b/src/commands/field-remove.ts index 75ddfa5..0911a5c 100644 --- a/src/commands/field-remove.ts +++ b/src/commands/field-remove.ts @@ -1,3 +1,4 @@ +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; @@ -16,6 +17,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); @@ -24,5 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { delete targetFields[fieldId]; await saveModel(); + await adapter.syncModels({ repo, token, host }); + console.info(`Field removed: ${id}`); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 877b53b..cb65056 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -17,7 +17,6 @@ import { openBrowser } from "../lib/browser"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { installDependencies } from "../lib/packageJson"; import { ForbiddenRequestError, UnauthorizedRequestError } from "../lib/request"; -import { syncCustomTypes, syncSlices } from "./sync"; const config = { name: "prismic init", @@ -69,7 +68,7 @@ export default createCommand(config, async ({ values }) => { } // Validate repo membership - const token = await getToken(); + let token = await getToken(); const host = await getHost(); let profile: Profile; try { @@ -89,7 +88,7 @@ export default createCommand(config, async ({ values }) => { }, }); console.info(`Logged in as ${email}`); - const token = await getToken(); + token = await getToken(); profile = await getProfile({ token, host }); } else { throw error; @@ -144,12 +143,8 @@ export default createCommand(config, async ({ values }) => { ); } - // Sync models from remote - await syncSlices(repo, adapter); - await syncCustomTypes(repo, adapter); - - // Generate TypeScript types from synced models - await adapter.generateTypes(); + // Sync models from remote and generate types + await adapter.syncModels({ repo, token, host }); console.info(`\nInitialized Prismic for repository "${repo}".`); }); diff --git a/src/commands/page-type-create.ts b/src/commands/page-type-create.ts index 464191b..76f572e 100644 --- a/src/commands/page-type-create.ts +++ b/src/commands/page-type-create.ts @@ -2,6 +2,7 @@ import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; import { snakeCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { insertCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -66,6 +67,7 @@ export default createCommand(config, async ({ positionals, values }) => { }, }; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); @@ -79,5 +81,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Created page type "${name}" (id: "${id}")`); }); diff --git a/src/commands/page-type-remove.ts b/src/commands/page-type-remove.ts index 8c6ab5c..842bd3b 100644 --- a/src/commands/page-type-remove.ts +++ b/src/commands/page-type-remove.ts @@ -1,3 +1,4 @@ +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getCustomTypes, removeCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -19,6 +20,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); @@ -44,5 +46,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Page type removed: "${name}" (id: ${pageType.id})`); }); diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index ad1b86e..038c97c 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -2,6 +2,7 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { camelCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getSlices, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -25,6 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { to, id = camelCase(name), repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); @@ -64,5 +66,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Added variation "${name}" (id: "${id}") to slice "${to}"`); }); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index 301290f..c5db873 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -1,5 +1,6 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -30,6 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { to, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const apiConfig = { repo, token, host }; @@ -77,5 +79,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Connected slice "${name}" to "${to}"`); }); diff --git a/src/commands/slice-create.ts b/src/commands/slice-create.ts index 4313a21..3a62376 100644 --- a/src/commands/slice-create.ts +++ b/src/commands/slice-create.ts @@ -2,6 +2,7 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { snakeCase } from "change-case"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { insertSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -41,6 +42,7 @@ export default createCommand(config, async ({ positionals, values }) => { ], }; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); @@ -54,5 +56,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Created slice "${name}" (id: "${id}")`); }); diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index aaa786f..7e24671 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -1,5 +1,6 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -30,6 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { from, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const apiConfig = { repo, token, host }; @@ -74,5 +76,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Disconnected slice "${name}" from "${from}"`); }); diff --git a/src/commands/slice-remove-variation.ts b/src/commands/slice-remove-variation.ts index 69b53fe..ef19a07 100644 --- a/src/commands/slice-remove-variation.ts +++ b/src/commands/slice-remove-variation.ts @@ -1,5 +1,6 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getSlices, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -22,6 +23,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { from, repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); @@ -52,5 +54,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Removed variation "${name}" from slice "${from}"`); }); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts index 5bf01b8..17d0307 100644 --- a/src/commands/slice-remove.ts +++ b/src/commands/slice-remove.ts @@ -1,3 +1,4 @@ +import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { getSlices, removeSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; @@ -19,6 +20,7 @@ export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; const { repo = await getRepositoryName() } = values; + const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); @@ -38,5 +40,7 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } + await adapter.syncModels({ repo, token, host }); + console.info(`Slice removed: "${name}" (id: ${slice.id})`); }); diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 899d561..678a644 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -41,9 +41,9 @@ export default createCommand(config, async ({ values }) => { if (watch) { await watchForChanges(repo, adapter); } else { - await syncSlices(repo, adapter); - await syncCustomTypes(repo, adapter); - await adapter.generateTypes(); + const token = await getToken(); + const host = await getHost(); + await adapter.syncModels({ repo, token, host }); segmentTrackEnd("sync", { watch }); console.info("Sync complete"); @@ -57,9 +57,7 @@ async function watchForChanges(repo: string, adapter: Adapter) { const initialRemoteSlices = await getSlices({ repo, token, host }); const initialRemoteCustomTypes = await getCustomTypes({ repo, token, host }); - await syncSlices(repo, adapter); - await syncCustomTypes(repo, adapter); - await adapter.generateTypes(); + await adapter.syncModels({ repo, token, host }); console.info(dedent` Initial sync completed! @@ -98,18 +96,16 @@ async function watchForChanges(repo: string, adapter: Adapter) { const changed = []; if (slicesChanged) { - await syncSlices(repo, adapter); + await adapter.syncSlices({ repo, token, host }); lastRemoteSlicesHash = remoteSlicesHash; changed.push("slices"); } if (customTypesChanged) { - await syncCustomTypes(repo, adapter); + await adapter.syncCustomTypes({ repo, token, host }); lastRemoteCustomTypesHash = remoteCustomTypesHash; changed.push("custom types"); } - await adapter.generateTypes(); - const timestamp = new Date().toLocaleTimeString(); console.info(`[${timestamp}] Changes detected in ${changed.join(" and ")}`); } @@ -135,77 +131,6 @@ async function watchForChanges(repo: string, adapter: Adapter) { } } -export async function syncSlices(repo: string, adapter: Adapter): Promise { - const token = await getToken(); - const host = await getHost(); - - const remoteSlices = await getSlices({ repo, token, host }); - const localSlices = await adapter.getSlices(); - - // Handle slices update - for (const remoteSlice of remoteSlices) { - const localSlice = localSlices.find((slice) => slice.model.id === remoteSlice.id); - if (localSlice) { - await adapter.updateSlice(remoteSlice); - } - } - - // Handle slices deletion - for (const localSlice of localSlices) { - const existsRemotely = remoteSlices.some((slice) => slice.id === localSlice.model.id); - if (!existsRemotely) { - await adapter.deleteSlice(localSlice.model.id); - } - } - - // Handle slices creation - const defaultLibrary = await adapter.getDefaultSliceLibrary(); - for (const remoteSlice of remoteSlices) { - const existsLocally = localSlices.some((slice) => slice.model.id === remoteSlice.id); - if (!existsLocally) { - await adapter.createSlice(remoteSlice, defaultLibrary); - } - } -} - -export async function syncCustomTypes(repo: string, adapter: Adapter): Promise { - const token = await getToken(); - const host = await getHost(); - - const remoteCustomTypes = await getCustomTypes({ repo, token, host }); - const localCustomTypes = await adapter.getCustomTypes(); - - // Handle custom types update - for (const remoteCustomType of remoteCustomTypes) { - const localCustomType = localCustomTypes.find( - (customType) => customType.model.id === remoteCustomType.id, - ); - if (localCustomType) { - await adapter.updateCustomType(remoteCustomType); - } - } - - // Handle custom types deletion - for (const localCustomType of localCustomTypes) { - const existsRemotely = remoteCustomTypes.some( - (customType) => customType.id === localCustomType.model.id, - ); - if (!existsRemotely) { - await adapter.deleteCustomType(localCustomType.model.id); - } - } - - // Handle custom types creation - for (const remoteCustomType of remoteCustomTypes) { - const existsLocally = localCustomTypes.some( - (customType) => customType.model.id === remoteCustomType.id, - ); - if (!existsLocally) { - await adapter.createCustomType(remoteCustomType); - } - } -} - function shutdown(): void { console.info("Watch stopped. Goodbye!"); segmentTrackEnd("sync", { watch: true }); diff --git a/src/models.ts b/src/models.ts index d46a381..af6c1dd 100644 --- a/src/models.ts +++ b/src/models.ts @@ -8,9 +8,9 @@ import { UnknownRequestError } from "./lib/request"; type Field = DynamicWidget; type Fields = Record; -type EntityType = "slice" | "customType"; +type ModelKind = "slice" | "customType"; type ApiConfig = { repo: string; token: string | undefined; host: string }; -type Target = [fields: Fields, save: () => Promise, entityType: EntityType]; +type Target = [fields: Fields, save: () => Promise, modelKind: ModelKind]; export const TARGET_OPTIONS = { "to-slice": { type: "string", description: "Name of the target slice" }, From 11dc23c91e6ace2bc0e24ce30849658af1038915 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 31 Mar 2026 15:41:03 -1000 Subject: [PATCH 09/29] feat: replace syncModels with granular adapter methods Move adapter sync into resolveModel/resolveFieldContainer so field commands no longer depend on the adapter directly. Type/slice CRUD commands now call specific adapter methods (create/update/delete) instead of a full sync. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/custom-type-create.ts | 3 +- src/commands/custom-type-remove.ts | 5 ++- src/commands/custom-type.ts | 1 - src/commands/field-add-boolean.ts | 4 -- src/commands/field-add-color.ts | 4 -- .../field-add-content-relationship.ts | 4 -- src/commands/field-add-date.ts | 4 -- src/commands/field-add-embed.ts | 4 -- src/commands/field-add-geopoint.ts | 4 -- src/commands/field-add-group.ts | 4 -- src/commands/field-add-image.ts | 4 -- src/commands/field-add-integration.ts | 4 -- src/commands/field-add-link-to-media.ts | 4 -- src/commands/field-add-link.ts | 4 -- src/commands/field-add-number.ts | 4 -- src/commands/field-add-rich-text.ts | 4 -- src/commands/field-add-select.ts | 10 ++--- src/commands/field-add-table.ts | 4 -- src/commands/field-add-text.ts | 4 -- src/commands/field-add-timestamp.ts | 4 -- src/commands/field-add-uid.ts | 4 -- src/commands/field-edit.ts | 4 -- src/commands/field-remove.ts | 4 -- src/commands/page-type-create.ts | 3 +- src/commands/page-type-remove.ts | 5 ++- src/commands/page-type.ts | 1 - src/commands/slice-add-variation.ts | 7 ++- src/commands/slice-connect.ts | 7 ++- src/commands/slice-create.ts | 3 +- src/commands/slice-disconnect.ts | 7 ++- src/commands/slice-remove-variation.ts | 7 ++- src/commands/slice-remove.ts | 5 ++- src/commands/slice.ts | 1 - src/index.ts | 4 +- src/models.ts | 43 ++++++++++++++++++- test/field-add-content-relationship.test.ts | 16 ++++++- test/field-add-link-to-media.test.ts | 8 +++- test/field-list.test.ts | 12 +----- test/slice-list.test.ts | 4 +- 39 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/commands/custom-type-create.ts b/src/commands/custom-type-create.ts index 7603047..be182f7 100644 --- a/src/commands/custom-type-create.ts +++ b/src/commands/custom-type-create.ts @@ -51,7 +51,8 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + await adapter.createCustomType(model); + await adapter.generateTypes(); console.info(`Created custom type "${name}" (id: "${id}")`); }); diff --git a/src/commands/custom-type-remove.ts b/src/commands/custom-type-remove.ts index c6af7b3..8c317e3 100644 --- a/src/commands/custom-type-remove.ts +++ b/src/commands/custom-type-remove.ts @@ -46,7 +46,10 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.deleteCustomType(customType.id); + } catch {} + await adapter.generateTypes(); console.info(`Custom type removed: "${name}" (id: ${customType.id})`); }); diff --git a/src/commands/custom-type.ts b/src/commands/custom-type.ts index 64119ce..fb912d2 100644 --- a/src/commands/custom-type.ts +++ b/src/commands/custom-type.ts @@ -1,5 +1,4 @@ import { createCommandRouter } from "../lib/command"; - import customTypeCreate from "./custom-type-create"; import customTypeList from "./custom-type-list"; import customTypeRemove from "./custom-type-remove"; diff --git a/src/commands/field-add-boolean.ts b/src/commands/field-add-boolean.ts index 976a0fc..6f856a9 100644 --- a/src/commands/field-add-boolean.ts +++ b/src/commands/field-add-boolean.ts @@ -2,7 +2,6 @@ import type { BooleanField } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -33,7 +32,6 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -53,7 +51,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-color.ts b/src/commands/field-add-color.ts index c156933..ac0daf6 100644 --- a/src/commands/field-add-color.ts +++ b/src/commands/field-add-color.ts @@ -2,7 +2,6 @@ import type { Color } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,7 +24,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,7 +41,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts index ae17ef2..75b52bb 100644 --- a/src/commands/field-add-content-relationship.ts +++ b/src/commands/field-add-content-relationship.ts @@ -2,7 +2,6 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -30,7 +29,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, tag: tags, "custom-type": customtypes, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -50,7 +48,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-date.ts b/src/commands/field-add-date.ts index a0b846b..f0d3c8a 100644 --- a/src/commands/field-add-date.ts +++ b/src/commands/field-add-date.ts @@ -2,7 +2,6 @@ import type { Date as DateField } from "@prismicio/types-internal/lib/customtype import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -26,7 +25,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -45,7 +43,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-embed.ts b/src/commands/field-add-embed.ts index 1edd2e8..c58a249 100644 --- a/src/commands/field-add-embed.ts +++ b/src/commands/field-add-embed.ts @@ -2,7 +2,6 @@ import type { Embed } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,7 +24,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,7 +41,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-geopoint.ts b/src/commands/field-add-geopoint.ts index 6460922..07d8e76 100644 --- a/src/commands/field-add-geopoint.ts +++ b/src/commands/field-add-geopoint.ts @@ -2,7 +2,6 @@ import type { GeoPoint } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,7 +23,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,7 +39,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-group.ts b/src/commands/field-add-group.ts index 6fc7cfe..6b5a0e6 100644 --- a/src/commands/field-add-group.ts +++ b/src/commands/field-add-group.ts @@ -2,7 +2,6 @@ import type { Group } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,7 +23,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,7 +39,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-image.ts b/src/commands/field-add-image.ts index 894aa16..ea19993 100644 --- a/src/commands/field-add-image.ts +++ b/src/commands/field-add-image.ts @@ -2,7 +2,6 @@ import type { Image } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,7 +24,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,7 +41,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-integration.ts b/src/commands/field-add-integration.ts index 279a7a9..5489e54 100644 --- a/src/commands/field-add-integration.ts +++ b/src/commands/field-add-integration.ts @@ -2,7 +2,6 @@ import type { IntegrationField } from "@prismicio/types-internal/lib/customtypes import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -26,7 +25,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, catalog, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -45,7 +43,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-link-to-media.ts b/src/commands/field-add-link-to-media.ts index 3a34747..1d62691 100644 --- a/src/commands/field-add-link-to-media.ts +++ b/src/commands/field-add-link-to-media.ts @@ -2,7 +2,6 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -31,7 +30,6 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -51,7 +49,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-link.ts b/src/commands/field-add-link.ts index b581f03..9bd07eb 100644 --- a/src/commands/field-add-link.ts +++ b/src/commands/field-add-link.ts @@ -2,7 +2,6 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -35,7 +34,6 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -56,7 +54,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-number.ts b/src/commands/field-add-number.ts index f5ef799..68756d4 100644 --- a/src/commands/field-add-number.ts +++ b/src/commands/field-add-number.ts @@ -2,7 +2,6 @@ import type { Number as NumberField } from "@prismicio/types-internal/lib/custom import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -32,7 +31,6 @@ export default createCommand(config, async ({ positionals, values }) => { const max = parseNumber(values.max, "max"); const step = parseNumber(values.step, "step"); - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -53,8 +51,6 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-rich-text.ts b/src/commands/field-add-rich-text.ts index 202fdd9..61d4a65 100644 --- a/src/commands/field-add-rich-text.ts +++ b/src/commands/field-add-rich-text.ts @@ -2,7 +2,6 @@ import type { RichText } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -48,7 +47,6 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -68,7 +66,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-select.ts b/src/commands/field-add-select.ts index c63aea2..949d946 100644 --- a/src/commands/field-add-select.ts +++ b/src/commands/field-add-select.ts @@ -2,7 +2,6 @@ import type { Select } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -19,7 +18,11 @@ const config = { label: { type: "string", description: "Field label" }, placeholder: { type: "string", description: "Placeholder text" }, "default-value": { type: "string", description: "Default selected value" }, - option: { type: "string", multiple: true, description: "Select option value (can be repeated)" }, + option: { + type: "string", + multiple: true, + description: "Select option value (can be repeated)", + }, }, } satisfies CommandConfig; @@ -33,7 +36,6 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -53,7 +55,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-table.ts b/src/commands/field-add-table.ts index c6b3723..624d94b 100644 --- a/src/commands/field-add-table.ts +++ b/src/commands/field-add-table.ts @@ -2,7 +2,6 @@ import type { Table } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -24,7 +23,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -41,7 +39,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-text.ts b/src/commands/field-add-text.ts index 6510449..3787517 100644 --- a/src/commands/field-add-text.ts +++ b/src/commands/field-add-text.ts @@ -2,7 +2,6 @@ import type { Text } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -25,7 +24,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,7 +41,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-timestamp.ts b/src/commands/field-add-timestamp.ts index 6d30ef3..669dc12 100644 --- a/src/commands/field-add-timestamp.ts +++ b/src/commands/field-add-timestamp.ts @@ -2,7 +2,6 @@ import type { Timestamp } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; @@ -26,7 +25,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { label, placeholder, default: defaultValue, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -45,7 +43,5 @@ export default createCommand(config, async ({ positionals, values }) => { targetFields[fieldId] = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field added: ${id}`); }); diff --git a/src/commands/field-add-uid.ts b/src/commands/field-add-uid.ts index fb949a1..f44b8bb 100644 --- a/src/commands/field-add-uid.ts +++ b/src/commands/field-add-uid.ts @@ -1,6 +1,5 @@ import type { UID } from "@prismicio/types-internal/lib/customtypes"; -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveModel } from "../models"; @@ -22,7 +21,6 @@ const config = { export default createCommand(config, async ({ values }) => { const { label = "UID", placeholder, repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -39,7 +37,5 @@ export default createCommand(config, async ({ values }) => { fields.uid = field; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info("Field added: uid"); }); diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index 0bce527..5540d5b 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -1,4 +1,3 @@ -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; @@ -82,7 +81,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); @@ -172,8 +170,6 @@ export default createCommand(config, async ({ positionals, values }) => { await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field updated: ${id}`); }); diff --git a/src/commands/field-remove.ts b/src/commands/field-remove.ts index 0911a5c..75ddfa5 100644 --- a/src/commands/field-remove.ts +++ b/src/commands/field-remove.ts @@ -1,4 +1,3 @@ -import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; @@ -17,7 +16,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; const { repo = await getRepositoryName() } = values; - const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveFieldContainer(id, values, { repo, token, host }); @@ -26,7 +24,5 @@ export default createCommand(config, async ({ positionals, values }) => { delete targetFields[fieldId]; await saveModel(); - await adapter.syncModels({ repo, token, host }); - console.info(`Field removed: ${id}`); }); diff --git a/src/commands/page-type-create.ts b/src/commands/page-type-create.ts index 76f572e..e7fda64 100644 --- a/src/commands/page-type-create.ts +++ b/src/commands/page-type-create.ts @@ -81,7 +81,8 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + await adapter.createCustomType(model); + await adapter.generateTypes(); console.info(`Created page type "${name}" (id: "${id}")`); }); diff --git a/src/commands/page-type-remove.ts b/src/commands/page-type-remove.ts index 842bd3b..6243ba1 100644 --- a/src/commands/page-type-remove.ts +++ b/src/commands/page-type-remove.ts @@ -46,7 +46,10 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.deleteCustomType(pageType.id); + } catch {} + await adapter.generateTypes(); console.info(`Page type removed: "${name}" (id: ${pageType.id})`); }); diff --git a/src/commands/page-type.ts b/src/commands/page-type.ts index ef8f57b..9ac8b73 100644 --- a/src/commands/page-type.ts +++ b/src/commands/page-type.ts @@ -1,5 +1,4 @@ import { createCommandRouter } from "../lib/command"; - import pageTypeCreate from "./page-type-create"; import pageTypeList from "./page-type-list"; import pageTypeRemove from "./page-type-remove"; diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index 038c97c..c1daaf9 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -66,7 +66,12 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.updateSlice(updatedSlice); + } catch { + await adapter.createSlice(updatedSlice); + } + await adapter.generateTypes(); console.info(`Added variation "${name}" (id: "${id}") to slice "${to}"`); }); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index c5db873..ed15092 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -79,7 +79,12 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.updateCustomType(customType); + } catch { + await adapter.createCustomType(customType); + } + await adapter.generateTypes(); console.info(`Connected slice "${name}" to "${to}"`); }); diff --git a/src/commands/slice-create.ts b/src/commands/slice-create.ts index 3a62376..c8f71a8 100644 --- a/src/commands/slice-create.ts +++ b/src/commands/slice-create.ts @@ -56,7 +56,8 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + await adapter.createSlice(model); + await adapter.generateTypes(); console.info(`Created slice "${name}" (id: "${id}")`); }); diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index 7e24671..8347e41 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -76,7 +76,12 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.updateCustomType(customType); + } catch { + await adapter.createCustomType(customType); + } + await adapter.generateTypes(); console.info(`Disconnected slice "${name}" from "${from}"`); }); diff --git a/src/commands/slice-remove-variation.ts b/src/commands/slice-remove-variation.ts index ef19a07..8b9a913 100644 --- a/src/commands/slice-remove-variation.ts +++ b/src/commands/slice-remove-variation.ts @@ -54,7 +54,12 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.updateSlice(updatedSlice); + } catch { + await adapter.createSlice(updatedSlice); + } + await adapter.generateTypes(); console.info(`Removed variation "${name}" from slice "${from}"`); }); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts index 17d0307..1ae76f7 100644 --- a/src/commands/slice-remove.ts +++ b/src/commands/slice-remove.ts @@ -40,7 +40,10 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.syncModels({ repo, token, host }); + try { + await adapter.deleteSlice(slice.id); + } catch {} + await adapter.generateTypes(); console.info(`Slice removed: "${name}" (id: ${slice.id})`); }); diff --git a/src/commands/slice.ts b/src/commands/slice.ts index b99c8f1..e96e27d 100644 --- a/src/commands/slice.ts +++ b/src/commands/slice.ts @@ -1,5 +1,4 @@ import { createCommandRouter } from "../lib/command"; - import sliceAddVariation from "./slice-add-variation"; import sliceConnect from "./slice-connect"; import sliceCreate from "./slice-create"; diff --git a/src/index.ts b/src/index.ts index b17912c..316eec6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,13 +6,13 @@ import packageJson from "../package.json" with { type: "json" }; import { getAdapter, NoSupportedFrameworkError } from "./adapters"; import { getHost, refreshToken } from "./auth"; import { getProfile } from "./clients/user"; +import customType from "./commands/custom-type"; +import field from "./commands/field"; import gen from "./commands/gen"; import init from "./commands/init"; import locale from "./commands/locale"; import login from "./commands/login"; import logout from "./commands/logout"; -import customType from "./commands/custom-type"; -import field from "./commands/field"; import pageType from "./commands/page-type"; import preview from "./commands/preview"; import repo from "./commands/repo"; diff --git a/src/models.ts b/src/models.ts index af6c1dd..b27db95 100644 --- a/src/models.ts +++ b/src/models.ts @@ -2,6 +2,7 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; import type { CommandConfig } from "./lib/command"; +import { getAdapter } from "./adapters"; import { getCustomTypes, getSlices, updateCustomType, updateSlice } from "./clients/custom-types"; import { CommandError } from "./lib/command"; import { UnknownRequestError } from "./lib/request"; @@ -40,6 +41,7 @@ export async function resolveFieldContainer( }, apiConfig: ApiConfig, ): Promise { + const adapter = await getAdapter(); const { "from-slice": fromSlice, "from-page-type": fromPageType, @@ -72,7 +74,19 @@ export async function resolveFieldContainer( } variation.primary ??= {}; resolveFieldTarget(variation.primary, id); - return [variation.primary, () => updateSlice(slice, apiConfig), "slice"]; + return [ + variation.primary, + async () => { + await updateSlice(slice, apiConfig); + try { + await adapter.updateSlice(slice); + } catch { + await adapter.createSlice(slice); + } + await adapter.generateTypes(); + }, + "slice", + ]; } const fromType = fromPageType || fromCustomType; @@ -94,7 +108,19 @@ export async function resolveFieldContainer( throw new CommandError(`Field "${id}" not found. Available: ${fieldIds}`); } resolveFieldTarget(tab, id); - return [tab, () => updateCustomType(customType, apiConfig), "customType"]; + return [ + tab, + async () => { + await updateCustomType(customType, apiConfig); + try { + await adapter.updateCustomType(customType); + } catch { + await adapter.createCustomType(customType); + } + await adapter.generateTypes(); + }, + "customType", + ]; } export async function resolveModel( @@ -110,6 +136,7 @@ export async function resolveModel( }, apiConfig: ApiConfig, ): Promise { + const adapter = await getAdapter(); const sliceName = values["to-slice"] ?? values["from-slice"]; const pageTypeName = values["to-page-type"] ?? values["from-page-type"]; const customTypeName = values["to-custom-type"] ?? values["from-custom-type"]; @@ -158,6 +185,12 @@ export async function resolveModel( } throw error; } + try { + await adapter.updateSlice(newModel); + } catch { + await adapter.createSlice(newModel); + } + await adapter.generateTypes(); }, "slice", ]; @@ -200,6 +233,12 @@ export async function resolveModel( } throw error; } + try { + await adapter.updateCustomType(newModel); + } catch { + await adapter.createCustomType(newModel); + } + await adapter.generateTypes(); }, "customType", ]; diff --git a/test/field-add-content-relationship.test.ts b/test/field-add-content-relationship.test.ts index c365b47..791c471 100644 --- a/test/field-add-content-relationship.test.ts +++ b/test/field-add-content-relationship.test.ts @@ -7,7 +7,13 @@ it("supports --help", async ({ expect, prismic }) => { expect(stdout).toContain("prismic field add content-relationship [options]"); }); -it("adds a content relationship field to a slice", async ({ expect, prismic, repo, token, host }) => { +it("adds a content relationship field to a slice", async ({ + expect, + prismic, + repo, + token, + host, +}) => { const slice = buildSlice(); await insertSlice(slice, { repo, token, host }); @@ -27,7 +33,13 @@ it("adds a content relationship field to a slice", async ({ expect, prismic, rep expect(field).toMatchObject({ type: "Link", config: { select: "document" } }); }); -it("adds a content relationship field to a custom type", async ({ expect, prismic, repo, token, host }) => { +it("adds a content relationship field to a custom type", async ({ + expect, + prismic, + repo, + token, + host, +}) => { const customType = buildCustomType(); await insertCustomType(customType, { repo, token, host }); diff --git a/test/field-add-link-to-media.test.ts b/test/field-add-link-to-media.test.ts index 97cb812..635c13c 100644 --- a/test/field-add-link-to-media.test.ts +++ b/test/field-add-link-to-media.test.ts @@ -27,7 +27,13 @@ it("adds a link to media field to a slice", async ({ expect, prismic, repo, toke expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); }); -it("adds a link to media field to a custom type", async ({ expect, prismic, repo, token, host }) => { +it("adds a link to media field to a custom type", async ({ + expect, + prismic, + repo, + token, + host, +}) => { const customType = buildCustomType(); await insertCustomType(customType, { repo, token, host }); diff --git a/test/field-list.test.ts b/test/field-list.test.ts index 40a7de0..2257f29 100644 --- a/test/field-list.test.ts +++ b/test/field-list.test.ts @@ -26,11 +26,7 @@ it("lists fields in a slice", async ({ expect, prismic, repo, token, host }) => }); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("field", [ - "list", - "--from-slice", - slice.name, - ]); + const { stdout, exitCode } = await prismic("field", ["list", "--from-slice", slice.name]); expect(exitCode).toBe(0); expect(stdout).toContain("title"); expect(stdout).toContain("StructuredText"); @@ -76,11 +72,7 @@ it("prints message when no fields exist", async ({ expect, prismic, repo, token, const slice = buildSlice(); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("field", [ - "list", - "--from-slice", - slice.name, - ]); + const { stdout, exitCode } = await prismic("field", ["list", "--from-slice", slice.name]); expect(exitCode).toBe(0); expect(stdout).toContain("No fields found."); }); diff --git a/test/slice-list.test.ts b/test/slice-list.test.ts index 010ad4b..b06dda2 100644 --- a/test/slice-list.test.ts +++ b/test/slice-list.test.ts @@ -23,7 +23,5 @@ it("lists slices as JSON", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("slice", ["list", "--json"]); expect(exitCode).toBe(0); const parsed = JSON.parse(stdout); - expect(parsed).toEqual( - expect.arrayContaining([expect.objectContaining({ id: slice.id })]), - ); + expect(parsed).toEqual(expect.arrayContaining([expect.objectContaining({ id: slice.id })])); }); From cf1f0d637e9b62be942aa34fe53e57960487fe83 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Apr 2026 14:07:25 -1000 Subject: [PATCH 10/29] feat: unify `page-type` and `custom-type` into single `type` command (#104) * feat: unify `page-type` and `custom-type` into single `type` command Consolidates the separate `page-type` and `custom-type` top-level commands into a single `type` command with a `--format` flag on create. - `prismic type create --format page` for page types - `prismic type create ` defaults to custom format - `prismic type list` shows all types with format in output - `prismic type view/remove` work regardless of format - Field targeting simplified: `--to-type`/`--from-type` replace `--to-page-type`/`--to-custom-type`/`--from-page-type`/`--from-custom-type` Closes prismicio/cli#96 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use "content type" in command descriptions for clarity Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/custom-type-create.ts | 58 --------- src/commands/custom-type-remove.ts | 55 --------- src/commands/custom-type-view.ts | 48 -------- src/commands/custom-type.ts | 28 ----- src/commands/field-add-uid.ts | 7 +- src/commands/field.ts | 2 +- src/commands/gen-types.ts | 2 +- src/commands/page-type-create.ts | 88 -------------- src/commands/page-type-list.ts | 39 ------ src/commands/page-type.ts | 28 ----- src/commands/slice-connect.ts | 2 +- src/commands/slice-disconnect.ts | 2 +- src/commands/sync.ts | 2 +- src/commands/type-create.ts | 112 ++++++++++++++++++ .../{custom-type-list.ts => type-list.ts} | 11 +- .../{page-type-remove.ts => type-remove.ts} | 26 ++-- .../{page-type-view.ts => type-view.ts} | 29 ++--- src/commands/type.ts | 28 +++++ src/index.ts | 13 +- src/models.ts | 67 ++++------- test/custom-type-create.test.ts | 44 ------- test/custom-type.test.ts | 13 -- test/field-add-boolean.test.ts | 2 +- test/field-add-color.test.ts | 2 +- test/field-add-content-relationship.test.ts | 2 +- test/field-add-date.test.ts | 2 +- test/field-add-embed.test.ts | 2 +- test/field-add-geopoint.test.ts | 2 +- test/field-add-group.test.ts | 2 +- test/field-add-image.test.ts | 2 +- test/field-add-integration.test.ts | 2 +- test/field-add-link-to-media.test.ts | 2 +- test/field-add-link.test.ts | 2 +- test/field-add-number.test.ts | 2 +- test/field-add-rich-text.test.ts | 2 +- test/field-add-select.test.ts | 2 +- test/field-add-table.test.ts | 2 +- test/field-add-text.test.ts | 4 +- test/field-add-timestamp.test.ts | 2 +- test/field-add-uid.test.ts | 4 +- test/field-edit.test.ts | 2 +- test/field-remove.test.ts | 2 +- test/page-type-create.test.ts | 44 ------- test/page-type-list.test.ts | 29 ----- test/page-type-remove.test.ts | 21 ---- test/page-type-view.test.ts | 30 ----- test/page-type.test.ts | 13 -- test/type-create.test.ts | 66 +++++++++++ ...om-type-list.test.ts => type-list.test.ts} | 17 +-- ...ype-remove.test.ts => type-remove.test.ts} | 10 +- ...om-type-view.test.ts => type-view.test.ts} | 13 +- test/type.test.ts | 13 ++ 52 files changed, 322 insertions(+), 682 deletions(-) delete mode 100644 src/commands/custom-type-create.ts delete mode 100644 src/commands/custom-type-remove.ts delete mode 100644 src/commands/custom-type-view.ts delete mode 100644 src/commands/custom-type.ts delete mode 100644 src/commands/page-type-create.ts delete mode 100644 src/commands/page-type-list.ts delete mode 100644 src/commands/page-type.ts create mode 100644 src/commands/type-create.ts rename src/commands/{custom-type-list.ts => type-list.ts} (72%) rename src/commands/{page-type-remove.ts => type-remove.ts} (60%) rename src/commands/{page-type-view.ts => type-view.ts} (55%) create mode 100644 src/commands/type.ts delete mode 100644 test/custom-type-create.test.ts delete mode 100644 test/custom-type.test.ts delete mode 100644 test/page-type-create.test.ts delete mode 100644 test/page-type-list.test.ts delete mode 100644 test/page-type-remove.test.ts delete mode 100644 test/page-type-view.test.ts delete mode 100644 test/page-type.test.ts create mode 100644 test/type-create.test.ts rename test/{custom-type-list.test.ts => type-list.test.ts} (52%) rename test/{custom-type-remove.test.ts => type-remove.test.ts} (54%) rename test/{custom-type-view.test.ts => type-view.test.ts} (61%) create mode 100644 test/type.test.ts diff --git a/src/commands/custom-type-create.ts b/src/commands/custom-type-create.ts deleted file mode 100644 index be182f7..0000000 --- a/src/commands/custom-type-create.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; - -import { snakeCase } from "change-case"; - -import { getAdapter } from "../adapters"; -import { getHost, getToken } from "../auth"; -import { insertCustomType } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { UnknownRequestError } from "../lib/request"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic custom-type create", - description: "Create a new custom type.", - positionals: { - name: { description: "Name of the custom type", required: true }, - }, - options: { - single: { type: "boolean", short: "s", description: "Allow only one of this type" }, - id: { type: "string", description: "Custom ID for the custom type" }, - repo: { type: "string", short: "r", description: "Repository domain" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; - const { single = false, id = snakeCase(name), repo = await getRepositoryName() } = values; - - const model: CustomType = { - id, - label: name, - repeatable: !single, - status: true, - format: "custom", - json: { - Main: {}, - }, - }; - - const adapter = await getAdapter(); - const token = await getToken(); - const host = await getHost(); - - try { - await insertCustomType(model, { repo, host, token }); - } catch (error) { - if (error instanceof UnknownRequestError) { - const message = await error.text(); - throw new CommandError(`Failed to create custom type: ${message}`); - } - throw error; - } - - await adapter.createCustomType(model); - await adapter.generateTypes(); - - console.info(`Created custom type "${name}" (id: "${id}")`); -}); diff --git a/src/commands/custom-type-remove.ts b/src/commands/custom-type-remove.ts deleted file mode 100644 index 8c317e3..0000000 --- a/src/commands/custom-type-remove.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAdapter } from "../adapters"; -import { getHost, getToken } from "../auth"; -import { getCustomTypes, removeCustomType } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { UnknownRequestError } from "../lib/request"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic custom-type remove", - description: "Remove a custom type.", - positionals: { - name: { description: "Name of the custom type", required: true }, - }, - options: { - repo: { type: "string", short: "r", description: "Repository domain" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; - const { repo = await getRepositoryName() } = values; - - const adapter = await getAdapter(); - const token = await getToken(); - const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const customType = customTypes.find((ct) => ct.label === name); - - if (!customType) { - throw new CommandError(`Custom type not found: ${name}`); - } - - if (customType.format === "page") { - throw new CommandError( - `"${name}" is not a custom type. Use \`prismic page-type remove\` instead.`, - ); - } - - try { - await removeCustomType(customType.id, { repo, host, token }); - } catch (error) { - if (error instanceof UnknownRequestError) { - const message = await error.text(); - throw new CommandError(`Failed to remove custom type: ${message}`); - } - throw error; - } - - try { - await adapter.deleteCustomType(customType.id); - } catch {} - await adapter.generateTypes(); - - console.info(`Custom type removed: "${name}" (id: ${customType.id})`); -}); diff --git a/src/commands/custom-type-view.ts b/src/commands/custom-type-view.ts deleted file mode 100644 index 40ef025..0000000 --- a/src/commands/custom-type-view.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { getHost, getToken } from "../auth"; -import { getCustomTypes } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { stringify } from "../lib/json"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic custom-type view", - description: "View details of a custom type.", - positionals: { - name: { description: "Name of the custom type", required: true }, - }, - options: { - json: { type: "boolean", description: "Output as JSON" }, - repo: { type: "string", short: "r", description: "Repository domain" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; - const { json, repo = await getRepositoryName() } = values; - - const token = await getToken(); - const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const customType = customTypes.find((ct) => ct.label === name); - - if (!customType) { - throw new CommandError(`Custom type not found: ${name}`); - } - - if (customType.format === "page") { - throw new CommandError( - `"${name}" is not a custom type. Use \`prismic page-type view\` instead.`, - ); - } - - if (json) { - console.info(stringify(customType)); - return; - } - - console.info(`ID: ${customType.id}`); - console.info(`Name: ${customType.label || "(no name)"}`); - console.info(`Repeatable: ${customType.repeatable}`); - const tabs = Object.keys(customType.json).join(", ") || "(none)"; - console.info(`Tabs: ${tabs}`); -}); diff --git a/src/commands/custom-type.ts b/src/commands/custom-type.ts deleted file mode 100644 index fb912d2..0000000 --- a/src/commands/custom-type.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createCommandRouter } from "../lib/command"; -import customTypeCreate from "./custom-type-create"; -import customTypeList from "./custom-type-list"; -import customTypeRemove from "./custom-type-remove"; -import customTypeView from "./custom-type-view"; - -export default createCommandRouter({ - name: "prismic custom-type", - description: "Manage custom types.", - commands: { - create: { - handler: customTypeCreate, - description: "Create a new custom type", - }, - remove: { - handler: customTypeRemove, - description: "Remove a custom type", - }, - list: { - handler: customTypeList, - description: "List custom types", - }, - view: { - handler: customTypeView, - description: "View a custom type", - }, - }, -}); diff --git a/src/commands/field-add-uid.ts b/src/commands/field-add-uid.ts index f44b8bb..cc3bb6f 100644 --- a/src/commands/field-add-uid.ts +++ b/src/commands/field-add-uid.ts @@ -7,11 +7,10 @@ import { getRepositoryName } from "../project"; const config = { name: "prismic field add uid", - description: "Add a UID field to a custom type.", + description: "Add a UID field to a content type.", options: { - "to-page-type": { type: "string", description: "Name of the target page type" }, - "to-custom-type": { type: "string", description: "Name of the target custom type" }, - tab: { type: "string", description: 'Custom type tab name (default: "Main")' }, + "to-type": { type: "string", description: "Name of the target content type" }, + tab: { type: "string", description: 'Content type tab name (default: "Main")' }, repo: { type: "string", short: "r", description: "Repository domain" }, label: { type: "string", description: "Field label" }, placeholder: { type: "string", description: "Placeholder text" }, diff --git a/src/commands/field.ts b/src/commands/field.ts index ba030e1..cf1e996 100644 --- a/src/commands/field.ts +++ b/src/commands/field.ts @@ -6,7 +6,7 @@ import fieldRemove from "./field-remove"; export default createCommandRouter({ name: "prismic field", - description: "Manage fields in slices and custom types.", + description: "Manage fields in slices and content types.", commands: { add: { handler: fieldAdd, diff --git a/src/commands/gen-types.ts b/src/commands/gen-types.ts index 2fda9fd..6d9856b 100644 --- a/src/commands/gen-types.ts +++ b/src/commands/gen-types.ts @@ -5,7 +5,7 @@ import { findProjectRoot } from "../project"; const config = { name: "prismic gen types", - description: "Generate TypeScript types for slices, page types, and custom types.", + description: "Generate TypeScript types for slices and content types.", } satisfies CommandConfig; export default createCommand(config, async () => { diff --git a/src/commands/page-type-create.ts b/src/commands/page-type-create.ts deleted file mode 100644 index e7fda64..0000000 --- a/src/commands/page-type-create.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; - -import { snakeCase } from "change-case"; - -import { getAdapter } from "../adapters"; -import { getHost, getToken } from "../auth"; -import { insertCustomType } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { UnknownRequestError } from "../lib/request"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic page-type create", - description: "Create a new page type.", - positionals: { - name: { description: "Name of the page type", required: true }, - }, - options: { - single: { type: "boolean", short: "s", description: "Allow only page one of this type" }, - id: { type: "string", description: "Custom ID for the page type" }, - repo: { type: "string", short: "r", description: "Repository domain" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; - const { single = false, id = snakeCase(name), repo = await getRepositoryName() } = values; - - const model: CustomType = { - id, - label: name, - repeatable: !single, - status: true, - format: "page", - json: { - Main: { - slices: { - type: "Slices", - fieldset: "Slice Zone", - config: { choices: {} }, - }, - }, - "SEO & Metadata": { - meta_title: { - type: "Text", - config: { - label: "Meta Title", - placeholder: "A title of the page used for social media and search engines", - }, - }, - meta_description: { - type: "Text", - config: { - label: "Meta Description", - placeholder: "A brief summary of the page", - }, - }, - meta_image: { - type: "Image", - config: { - label: "Meta Image", - constraint: { width: 2400, height: 1260 }, - thumbnails: [], - }, - }, - }, - }, - }; - - const adapter = await getAdapter(); - const token = await getToken(); - const host = await getHost(); - - try { - await insertCustomType(model, { repo, host, token }); - } catch (error) { - if (error instanceof UnknownRequestError) { - const message = await error.text(); - throw new CommandError(`Failed to create page type: ${message}`); - } - throw error; - } - - await adapter.createCustomType(model); - await adapter.generateTypes(); - - console.info(`Created page type "${name}" (id: "${id}")`); -}); diff --git a/src/commands/page-type-list.ts b/src/commands/page-type-list.ts deleted file mode 100644 index d0ac691..0000000 --- a/src/commands/page-type-list.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getHost, getToken } from "../auth"; -import { getCustomTypes } from "../clients/custom-types"; -import { createCommand, type CommandConfig } from "../lib/command"; -import { stringify } from "../lib/json"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic page-type list", - description: "List all page types.", - options: { - json: { type: "boolean", description: "Output as JSON" }, - repo: { type: "string", short: "r", description: "Repository domain" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ values }) => { - const { json, repo = await getRepositoryName() } = values; - - const token = await getToken(); - const host = await getHost(); - - const customTypes = await getCustomTypes({ repo, token, host }); - const pageTypes = customTypes.filter((customType) => customType.format === "page"); - - if (json) { - console.info(stringify(pageTypes)); - return; - } - - if (pageTypes.length === 0) { - console.info("No page types found."); - return; - } - - for (const pageType of pageTypes) { - const label = pageType.label || "(no name)"; - console.info(`${label} (id: ${pageType.id})`); - } -}); diff --git a/src/commands/page-type.ts b/src/commands/page-type.ts deleted file mode 100644 index 9ac8b73..0000000 --- a/src/commands/page-type.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createCommandRouter } from "../lib/command"; -import pageTypeCreate from "./page-type-create"; -import pageTypeList from "./page-type-list"; -import pageTypeRemove from "./page-type-remove"; -import pageTypeView from "./page-type-view"; - -export default createCommandRouter({ - name: "prismic page-type", - description: "Manage page types.", - commands: { - create: { - handler: pageTypeCreate, - description: "Create a new page type", - }, - remove: { - handler: pageTypeRemove, - description: "Remove a page type", - }, - list: { - handler: pageTypeList, - description: "List page types", - }, - view: { - handler: pageTypeView, - description: "View a page type", - }, - }, -}); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index ed15092..9f27176 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -17,7 +17,7 @@ const config = { to: { type: "string", required: true, - description: "Name of the page type or custom type", + description: "Name of the content type", }, "slice-zone": { type: "string", diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index 8347e41..5bde826 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -17,7 +17,7 @@ const config = { from: { type: "string", required: true, - description: "Name of the page type or custom type", + description: "Name of the content type", }, "slice-zone": { type: "string", diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 5d806e4..adabf9d 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -18,7 +18,7 @@ const MAX_CONSECUTIVE_ERRORS = 10; const config = { name: "prismic sync", description: ` - Sync slices, page types, and custom types from Prismic to local files. + Sync content types and slices from Prismic to local files. Remote models are the source of truth. Local files are created, updated, or deleted to match. diff --git a/src/commands/type-create.ts b/src/commands/type-create.ts new file mode 100644 index 0000000..64a2cc0 --- /dev/null +++ b/src/commands/type-create.ts @@ -0,0 +1,112 @@ +import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; + +import { snakeCase } from "change-case"; + +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { insertCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic type create", + description: "Create a new content type.", + positionals: { + name: { description: "Name of the content type", required: true }, + }, + options: { + format: { type: "string", short: "f", description: 'Type format: "custom" (default) or "page"' }, + single: { type: "boolean", short: "s", description: "Allow only one document of this type" }, + id: { type: "string", description: "Custom ID for the content type" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, + sections: { + FORMATS: ` + custom A non-page type (e.g. settings, navigation, author, blog + category). This is the default. + page A page type with a URL (e.g. homepage, blog post, landing + page). Includes a slice zone and SEO & Metadata tab by + default, and configures a route in prismic.config.json. + `, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { + format = "custom", + single = false, + id = snakeCase(name), + repo = await getRepositoryName(), + } = values; + + if (format !== "custom" && format !== "page") { + throw new CommandError(`Invalid format: "${format}". Use "custom" or "page".`); + } + + const json: CustomType["json"] = + format === "page" + ? { + Main: { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }, + }, + "SEO & Metadata": { + meta_title: { + type: "Text", + config: { + label: "Meta Title", + placeholder: "A title of the page used for social media and search engines", + }, + }, + meta_description: { + type: "Text", + config: { + label: "Meta Description", + placeholder: "A brief summary of the page", + }, + }, + meta_image: { + type: "Image", + config: { + label: "Meta Image", + constraint: { width: 2400, height: 1260 }, + thumbnails: [], + }, + }, + }, + } + : { Main: {} }; + + const model: CustomType = { + id, + label: name, + repeatable: !single, + status: true, + format, + json, + }; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + + try { + await insertCustomType(model, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to create type: ${message}`); + } + throw error; + } + + await adapter.createCustomType(model); + await adapter.generateTypes(); + + console.info(`Created type "${name}" (id: "${id}", format: "${format}")`); +}); diff --git a/src/commands/custom-type-list.ts b/src/commands/type-list.ts similarity index 72% rename from src/commands/custom-type-list.ts rename to src/commands/type-list.ts index 8dc41cc..59b5137 100644 --- a/src/commands/custom-type-list.ts +++ b/src/commands/type-list.ts @@ -5,8 +5,8 @@ import { stringify } from "../lib/json"; import { getRepositoryName } from "../project"; const config = { - name: "prismic custom-type list", - description: "List all custom types.", + name: "prismic type list", + description: "List all content types.", options: { json: { type: "boolean", description: "Output as JSON" }, repo: { type: "string", short: "r", description: "Repository domain" }, @@ -19,8 +19,7 @@ export default createCommand(config, async ({ values }) => { const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const types = customTypes.filter((customType) => customType.format !== "page"); + const types = await getCustomTypes({ repo, token, host }); if (json) { console.info(stringify(types)); @@ -28,12 +27,12 @@ export default createCommand(config, async ({ values }) => { } if (types.length === 0) { - console.info("No custom types found."); + console.info("No types found."); return; } for (const type of types) { const label = type.label || "(no name)"; - console.info(`${label} (id: ${type.id})`); + console.info(`${label} (id: ${type.id}, format: ${type.format})`); } }); diff --git a/src/commands/page-type-remove.ts b/src/commands/type-remove.ts similarity index 60% rename from src/commands/page-type-remove.ts rename to src/commands/type-remove.ts index 6243ba1..ec4e54c 100644 --- a/src/commands/page-type-remove.ts +++ b/src/commands/type-remove.ts @@ -6,10 +6,10 @@ import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; const config = { - name: "prismic page-type remove", - description: "Remove a page type.", + name: "prismic type remove", + description: "Remove a content type.", positionals: { - name: { description: "Name of the page type", required: true }, + name: { description: "Name of the content type", required: true }, }, options: { repo: { type: "string", short: "r", description: "Repository domain" }, @@ -24,32 +24,26 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const pageType = customTypes.find((ct) => ct.label === name); + const type = customTypes.find((ct) => ct.label === name); - if (!pageType) { - throw new CommandError(`Page type not found: ${name}`); - } - - if (pageType.format !== "page") { - throw new CommandError( - `"${name}" is not a page type. Use \`prismic custom-type remove\` instead.`, - ); + if (!type) { + throw new CommandError(`Type not found: ${name}`); } try { - await removeCustomType(pageType.id, { repo, host, token }); + await removeCustomType(type.id, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); - throw new CommandError(`Failed to create page type: ${message}`); + throw new CommandError(`Failed to remove type: ${message}`); } throw error; } try { - await adapter.deleteCustomType(pageType.id); + await adapter.deleteCustomType(type.id); } catch {} await adapter.generateTypes(); - console.info(`Page type removed: "${name}" (id: ${pageType.id})`); + console.info(`Type removed: "${name}" (id: ${type.id})`); }); diff --git a/src/commands/page-type-view.ts b/src/commands/type-view.ts similarity index 55% rename from src/commands/page-type-view.ts rename to src/commands/type-view.ts index 1f0dd52..c0e7e56 100644 --- a/src/commands/page-type-view.ts +++ b/src/commands/type-view.ts @@ -5,10 +5,10 @@ import { stringify } from "../lib/json"; import { getRepositoryName } from "../project"; const config = { - name: "prismic page-type view", - description: "View details of a page type.", + name: "prismic type view", + description: "View details of a content type.", positionals: { - name: { description: "Name of the page type", required: true }, + name: { description: "Name of the content type", required: true }, }, options: { json: { type: "boolean", description: "Output as JSON" }, @@ -23,26 +23,21 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const pageType = customTypes.find((ct) => ct.label === name); + const type = customTypes.find((ct) => ct.label === name); - if (!pageType) { - throw new CommandError(`Page type not found: ${name}`); - } - - if (pageType.format !== "page") { - throw new CommandError( - `"${name}" is not a page type. Use \`prismic custom-type view\` instead.`, - ); + if (!type) { + throw new CommandError(`Type not found: ${name}`); } if (json) { - console.info(stringify(pageType)); + console.info(stringify(type)); return; } - console.info(`ID: ${pageType.id}`); - console.info(`Name: ${pageType.label || "(no name)"}`); - console.info(`Repeatable: ${pageType.repeatable}`); - const tabs = Object.keys(pageType.json).join(", ") || "(none)"; + console.info(`ID: ${type.id}`); + console.info(`Name: ${type.label || "(no name)"}`); + console.info(`Format: ${type.format}`); + console.info(`Repeatable: ${type.repeatable}`); + const tabs = Object.keys(type.json).join(", ") || "(none)"; console.info(`Tabs: ${tabs}`); }); diff --git a/src/commands/type.ts b/src/commands/type.ts new file mode 100644 index 0000000..5cf5814 --- /dev/null +++ b/src/commands/type.ts @@ -0,0 +1,28 @@ +import { createCommandRouter } from "../lib/command"; +import typeCreate from "./type-create"; +import typeList from "./type-list"; +import typeRemove from "./type-remove"; +import typeView from "./type-view"; + +export default createCommandRouter({ + name: "prismic type", + description: "Manage content types.", + commands: { + create: { + handler: typeCreate, + description: "Create a new content type", + }, + remove: { + handler: typeRemove, + description: "Remove a content type", + }, + list: { + handler: typeList, + description: "List content types", + }, + view: { + handler: typeView, + description: "View a content type", + }, + }, +}); diff --git a/src/index.ts b/src/index.ts index 3be5677..a20961b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import packageJson from "../package.json" with { type: "json" }; import { getAdapter, NoSupportedFrameworkError } from "./adapters"; import { AUTH_FILE_PATH, getHost, refreshToken } from "./auth"; import { getProfile } from "./clients/user"; -import customType from "./commands/custom-type"; +import type_ from "./commands/type"; import docs from "./commands/docs"; import field from "./commands/field"; import gen from "./commands/gen"; @@ -14,7 +14,6 @@ import init from "./commands/init"; import locale from "./commands/locale"; import login from "./commands/login"; import logout from "./commands/logout"; -import pageType from "./commands/page-type"; import preview from "./commands/preview"; import repo from "./commands/repo"; import slice from "./commands/slice"; @@ -74,18 +73,14 @@ const router = createCommandRouter({ handler: repo, description: "Manage repositories", }, - "custom-type": { - handler: customType, - description: "Manage custom types", + type: { + handler: type_, + description: "Manage content types", }, field: { handler: field, description: "Manage fields", }, - "page-type": { - handler: pageType, - description: "Manage page types", - }, slice: { handler: slice, description: "Manage slices", diff --git a/src/models.ts b/src/models.ts index b27db95..48a9127 100644 --- a/src/models.ts +++ b/src/models.ts @@ -15,17 +15,15 @@ type Target = [fields: Fields, save: () => Promise, modelKind: ModelKind]; export const TARGET_OPTIONS = { "to-slice": { type: "string", description: "Name of the target slice" }, - "to-page-type": { type: "string", description: "Name of the target page type" }, - "to-custom-type": { type: "string", description: "Name of the target custom type" }, + "to-type": { type: "string", description: "Name of the target content type" }, variation: { type: "string", description: 'Slice variation ID (default: "default")' }, - tab: { type: "string", description: 'Custom type tab name (default: "Main")' }, + tab: { type: "string", description: 'Content type tab name (default: "Main")' }, repo: { type: "string", short: "r", description: "Repository domain" }, } satisfies CommandConfig["options"]; export const SOURCE_OPTIONS = { "from-slice": { type: "string", description: "Name of the source slice" }, - "from-page-type": { type: "string", description: "Name of the source page type" }, - "from-custom-type": { type: "string", description: "Name of the source custom type" }, + "from-type": { type: "string", description: "Name of the source content type" }, variation: TARGET_OPTIONS.variation, tab: TARGET_OPTIONS.tab, repo: TARGET_OPTIONS.repo, @@ -35,8 +33,7 @@ export async function resolveFieldContainer( id: string, values: { "from-slice"?: string; - "from-page-type"?: string; - "from-custom-type"?: string; + "from-type"?: string; variation?: string; }, apiConfig: ApiConfig, @@ -44,21 +41,16 @@ export async function resolveFieldContainer( const adapter = await getAdapter(); const { "from-slice": fromSlice, - "from-page-type": fromPageType, - "from-custom-type": fromCustomType, + "from-type": fromType, variation: variationId = "default", } = values; - const providedCount = [fromSlice, fromPageType, fromCustomType].filter(Boolean).length; + const providedCount = [fromSlice, fromType].filter(Boolean).length; if (providedCount === 0) { - throw new CommandError( - "Specify a target with --from-slice, --from-page-type, or --from-custom-type.", - ); + throw new CommandError("Specify a target with --from-slice or --from-type."); } if (providedCount > 1) { - throw new CommandError( - "Only one of --from-slice, --from-page-type, or --from-custom-type can be specified.", - ); + throw new CommandError("Only one of --from-slice or --from-type can be specified."); } if (fromSlice) { @@ -89,15 +81,10 @@ export async function resolveFieldContainer( ]; } - const fromType = fromPageType || fromCustomType; - const entityLabel = fromPageType ? "Page type" : "Custom type"; const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => { - if (ct.label !== fromType) return false; - return fromPageType ? ct.format === "page" : ct.format !== "page"; - }); + const customType = customTypes.find((ct) => ct.label === fromType); if (!customType) { - throw new CommandError(`${entityLabel} not found: ${fromType}`); + throw new CommandError(`Type not found: ${fromType}`); } let tab: Record | undefined; for (const tabName in customType.json) { @@ -126,11 +113,9 @@ export async function resolveFieldContainer( export async function resolveModel( values: { "to-slice"?: string; - "to-page-type"?: string; - "to-custom-type"?: string; + "to-type"?: string; "from-slice"?: string; - "from-page-type"?: string; - "from-custom-type"?: string; + "from-type"?: string; variation?: string; tab?: string; }, @@ -138,24 +123,19 @@ export async function resolveModel( ): Promise { const adapter = await getAdapter(); const sliceName = values["to-slice"] ?? values["from-slice"]; - const pageTypeName = values["to-page-type"] ?? values["from-page-type"]; - const customTypeName = values["to-custom-type"] ?? values["from-custom-type"]; + const typeName = values["to-type"] ?? values["from-type"]; - const providedCount = [sliceName, pageTypeName, customTypeName].filter(Boolean).length; + const providedCount = [sliceName, typeName].filter(Boolean).length; if (providedCount === 0) { - throw new CommandError( - "Specify a target with --to-slice, --to-page-type, or --to-custom-type.", - ); + throw new CommandError("Specify a target with --to-slice or --to-type."); } if (providedCount > 1) { - throw new CommandError( - "Only one of --to-slice, --to-page-type, or --to-custom-type can be specified.", - ); + throw new CommandError("Only one of --to-slice or --to-type can be specified."); } if (sliceName) { if ("tab" in values) { - throw new CommandError("--tab is only valid for page types or custom types."); + throw new CommandError("--tab is only valid for content types."); } const variation = values.variation ?? "default"; @@ -196,22 +176,15 @@ export async function resolveModel( ]; } - // Page type or custom type - const name = pageTypeName ?? customTypeName; - const entityLabel = pageTypeName ? "Page type" : "Custom type"; - if ("variation" in values) { throw new CommandError("--variation is only valid for slices."); } const tab = values.tab ?? "Main"; const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => { - if (ct.label !== name) return false; - return pageTypeName ? ct.format === "page" : ct.format !== "page"; - }); + const customType = customTypes.find((ct) => ct.label === typeName); if (!customType) { - throw new CommandError(`${entityLabel} not found: ${name}`); + throw new CommandError(`Type not found: ${typeName}`); } const newModel = structuredClone(customType); @@ -229,7 +202,7 @@ export async function resolveModel( } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); - throw new CommandError(`Failed to update ${entityLabel.toLowerCase()}: ${message}`); + throw new CommandError(`Failed to update type: ${message}`); } throw error; } diff --git a/test/custom-type-create.test.ts b/test/custom-type-create.test.ts deleted file mode 100644 index 36a5ef7..0000000 --- a/test/custom-type-create.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { buildCustomType, it } from "./it"; -import { getCustomTypes } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type", ["create", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type create [options]"); -}); - -it("creates a custom type", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "custom" }); - - const { stdout, exitCode } = await prismic("custom-type", ["create", label!]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`Created custom type "${label}"`); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.label === label); - expect(created).toMatchObject({ format: "custom", repeatable: true }); -}); - -it("creates a single custom type", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "custom" }); - - const { exitCode } = await prismic("custom-type", ["create", label!, "--single"]); - expect(exitCode).toBe(0); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.label === label); - expect(created).toMatchObject({ format: "custom", repeatable: false }); -}); - -it("creates a custom type with a custom id", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "custom" }); - const id = `CustomType${crypto.randomUUID().split("-")[0]}`; - - const { stdout, exitCode } = await prismic("custom-type", ["create", label!, "--id", id]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`Created custom type "${label}" (id: "${id}")`); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.id === id); - expect(created).toBeDefined(); -}); diff --git a/test/custom-type.test.ts b/test/custom-type.test.ts deleted file mode 100644 index cd896bf..0000000 --- a/test/custom-type.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { it } from "./it"; - -it("prints help by default", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type"); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type [options]"); -}); - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type", ["--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type [options]"); -}); diff --git a/test/field-add-boolean.test.ts b/test/field-add-boolean.test.ts index a08475b..ebbef70 100644 --- a/test/field-add-boolean.test.ts +++ b/test/field-add-boolean.test.ts @@ -35,7 +35,7 @@ it("adds a boolean field to a custom type", async ({ expect, prismic, repo, toke "add", "boolean", "is_active", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-color.test.ts b/test/field-add-color.test.ts index 7abdccc..cb72815 100644 --- a/test/field-add-color.test.ts +++ b/test/field-add-color.test.ts @@ -35,7 +35,7 @@ it("adds a color field to a custom type", async ({ expect, prismic, repo, token, "add", "color", "my_color", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-content-relationship.test.ts b/test/field-add-content-relationship.test.ts index 791c471..5879279 100644 --- a/test/field-add-content-relationship.test.ts +++ b/test/field-add-content-relationship.test.ts @@ -47,7 +47,7 @@ it("adds a content relationship field to a custom type", async ({ "add", "content-relationship", "my_link", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-date.test.ts b/test/field-add-date.test.ts index 997a623..f0deda9 100644 --- a/test/field-add-date.test.ts +++ b/test/field-add-date.test.ts @@ -35,7 +35,7 @@ it("adds a date field to a custom type", async ({ expect, prismic, repo, token, "add", "date", "my_date", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-embed.test.ts b/test/field-add-embed.test.ts index ab834ce..3aacedf 100644 --- a/test/field-add-embed.test.ts +++ b/test/field-add-embed.test.ts @@ -35,7 +35,7 @@ it("adds an embed field to a custom type", async ({ expect, prismic, repo, token "add", "embed", "my_embed", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-geopoint.test.ts b/test/field-add-geopoint.test.ts index 4d50574..2d13dce 100644 --- a/test/field-add-geopoint.test.ts +++ b/test/field-add-geopoint.test.ts @@ -35,7 +35,7 @@ it("adds a geopoint field to a custom type", async ({ expect, prismic, repo, tok "add", "geopoint", "my_location", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-group.test.ts b/test/field-add-group.test.ts index fc2915f..59d45e8 100644 --- a/test/field-add-group.test.ts +++ b/test/field-add-group.test.ts @@ -37,7 +37,7 @@ it("adds a group field to a custom type", async ({ expect, prismic, repo, token, "add", "group", "my_group", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-image.test.ts b/test/field-add-image.test.ts index b06e515..9632c6e 100644 --- a/test/field-add-image.test.ts +++ b/test/field-add-image.test.ts @@ -35,7 +35,7 @@ it("adds an image field to a custom type", async ({ expect, prismic, repo, token "add", "image", "my_image", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-integration.test.ts b/test/field-add-integration.test.ts index c14cd2a..78a7920 100644 --- a/test/field-add-integration.test.ts +++ b/test/field-add-integration.test.ts @@ -35,7 +35,7 @@ it("adds an integration field to a custom type", async ({ expect, prismic, repo, "add", "integration", "my_integration", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-link-to-media.test.ts b/test/field-add-link-to-media.test.ts index 635c13c..0a169c4 100644 --- a/test/field-add-link-to-media.test.ts +++ b/test/field-add-link-to-media.test.ts @@ -41,7 +41,7 @@ it("adds a link to media field to a custom type", async ({ "add", "link-to-media", "my_media", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-link.test.ts b/test/field-add-link.test.ts index 8439069..06c1bc0 100644 --- a/test/field-add-link.test.ts +++ b/test/field-add-link.test.ts @@ -35,7 +35,7 @@ it("adds a link field to a custom type", async ({ expect, prismic, repo, token, "add", "link", "my_link", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-number.test.ts b/test/field-add-number.test.ts index 7237ee8..69825d6 100644 --- a/test/field-add-number.test.ts +++ b/test/field-add-number.test.ts @@ -35,7 +35,7 @@ it("adds a number field to a custom type", async ({ expect, prismic, repo, token "add", "number", "my_number", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-rich-text.test.ts b/test/field-add-rich-text.test.ts index 9368104..8e35ca9 100644 --- a/test/field-add-rich-text.test.ts +++ b/test/field-add-rich-text.test.ts @@ -35,7 +35,7 @@ it("adds a rich text field to a custom type", async ({ expect, prismic, repo, to "add", "rich-text", "my_content", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-select.test.ts b/test/field-add-select.test.ts index 6778c46..bcc5ee5 100644 --- a/test/field-add-select.test.ts +++ b/test/field-add-select.test.ts @@ -35,7 +35,7 @@ it("adds a select field to a custom type", async ({ expect, prismic, repo, token "add", "select", "my_select", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-table.test.ts b/test/field-add-table.test.ts index b4616e4..4c78da1 100644 --- a/test/field-add-table.test.ts +++ b/test/field-add-table.test.ts @@ -35,7 +35,7 @@ it("adds a table field to a custom type", async ({ expect, prismic, repo, token, "add", "table", "my_table", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-text.test.ts b/test/field-add-text.test.ts index 369dfdd..e5ebea2 100644 --- a/test/field-add-text.test.ts +++ b/test/field-add-text.test.ts @@ -35,7 +35,7 @@ it("adds a text field to a custom type", async ({ expect, prismic, repo, token, "add", "text", "subtitle", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); @@ -55,7 +55,7 @@ it("adds a text field to a page type", async ({ expect, prismic, repo, token, ho "add", "text", "subtitle", - "--to-page-type", + "--to-type", pageType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-timestamp.test.ts b/test/field-add-timestamp.test.ts index 90ce657..c641b3e 100644 --- a/test/field-add-timestamp.test.ts +++ b/test/field-add-timestamp.test.ts @@ -35,7 +35,7 @@ it("adds a timestamp field to a custom type", async ({ expect, prismic, repo, to "add", "timestamp", "my_timestamp", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-add-uid.test.ts b/test/field-add-uid.test.ts index 1fad76c..d87d8e5 100644 --- a/test/field-add-uid.test.ts +++ b/test/field-add-uid.test.ts @@ -14,7 +14,7 @@ it("adds a uid field to a custom type", async ({ expect, prismic, repo, token, h const { stdout, exitCode } = await prismic("field", [ "add", "uid", - "--to-custom-type", + "--to-type", customType.label!, ]); expect(exitCode).toBe(0); @@ -33,7 +33,7 @@ it("adds a uid field to a page type", async ({ expect, prismic, repo, token, hos const { stdout, exitCode } = await prismic("field", [ "add", "uid", - "--to-page-type", + "--to-type", pageType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/field-edit.test.ts b/test/field-edit.test.ts index 8a98fd4..8b4c835 100644 --- a/test/field-edit.test.ts +++ b/test/field-edit.test.ts @@ -45,7 +45,7 @@ it("edits a field label on a custom type", async ({ expect, prismic, repo, token const { stdout, exitCode } = await prismic("field", [ "edit", "title", - "--from-custom-type", + "--from-type", customType.label!, "--label", "Page Title", diff --git a/test/field-remove.test.ts b/test/field-remove.test.ts index 1150ee9..bf4cca9 100644 --- a/test/field-remove.test.ts +++ b/test/field-remove.test.ts @@ -54,7 +54,7 @@ it("removes a field from a custom type", async ({ expect, prismic, repo, token, const { stdout, exitCode } = await prismic("field", [ "remove", "title", - "--from-custom-type", + "--from-type", customType.label!, ]); expect(exitCode).toBe(0); diff --git a/test/page-type-create.test.ts b/test/page-type-create.test.ts deleted file mode 100644 index da101f0..0000000 --- a/test/page-type-create.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { buildCustomType, it } from "./it"; -import { getCustomTypes } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type", ["create", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type create [options]"); -}); - -it("creates a page type", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "page" }); - - const { stdout, exitCode } = await prismic("page-type", ["create", label!]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`Created page type "${label}"`); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.label === label); - expect(created).toMatchObject({ format: "page", repeatable: true }); -}); - -it("creates a single page type", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "page" }); - - const { exitCode } = await prismic("page-type", ["create", label!, "--single"]); - expect(exitCode).toBe(0); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.label === label); - expect(created).toMatchObject({ format: "page", repeatable: false }); -}); - -it("creates a page type with a custom id", async ({ expect, prismic, repo, token, host }) => { - const { label } = buildCustomType({ format: "page" }); - const id = `PageType${crypto.randomUUID().split("-")[0]}`; - - const { stdout, exitCode } = await prismic("page-type", ["create", label!, "--id", id]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`Created page type "${label}" (id: "${id}")`); - - const customTypes = await getCustomTypes({ repo, token, host }); - const created = customTypes.find((ct) => ct.id === id); - expect(created).toBeDefined(); -}); diff --git a/test/page-type-list.test.ts b/test/page-type-list.test.ts deleted file mode 100644 index bb4f658..0000000 --- a/test/page-type-list.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { buildCustomType, it } from "./it"; -import { insertCustomType } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type", ["list", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type list [options]"); -}); - -it("lists page types", async ({ expect, prismic, repo, token, host }) => { - const pageType = buildCustomType({ format: "page" }); - await insertCustomType(pageType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("page-type", ["list"]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`${pageType.label} (id: ${pageType.id})`); -}); - -it("lists page types as JSON", async ({ expect, prismic, repo, token, host }) => { - const pageType = buildCustomType({ format: "page" }); - await insertCustomType(pageType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("page-type", ["list", "--json"]); - expect(exitCode).toBe(0); - const parsed = JSON.parse(stdout); - expect(parsed).toEqual( - expect.arrayContaining([expect.objectContaining({ id: pageType.id, format: "page" })]), - ); -}); diff --git a/test/page-type-remove.test.ts b/test/page-type-remove.test.ts deleted file mode 100644 index 3059d90..0000000 --- a/test/page-type-remove.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { buildCustomType, it } from "./it"; -import { getCustomTypes, insertCustomType } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type", ["remove", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type remove [options]"); -}); - -it("removes a page type", async ({ expect, prismic, repo, token, host }) => { - const pageType = buildCustomType({ format: "page" }); - await insertCustomType(pageType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("page-type", ["remove", pageType.label!]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`Page type removed: "${pageType.label}" (id: ${pageType.id})`); - - const customTypes = await getCustomTypes({ repo, token, host }); - const removed = customTypes.find((ct) => ct.id === pageType.id); - expect(removed).toBeUndefined(); -}); diff --git a/test/page-type-view.test.ts b/test/page-type-view.test.ts deleted file mode 100644 index 65a4afd..0000000 --- a/test/page-type-view.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { buildCustomType, it } from "./it"; -import { insertCustomType } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type", ["view", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type view [options]"); -}); - -it("views a page type", async ({ expect, prismic, repo, token, host }) => { - const pageType = buildCustomType({ format: "page" }); - await insertCustomType(pageType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("page-type", ["view", pageType.label!]); - expect(exitCode).toBe(0); - expect(stdout).toContain(`ID: ${pageType.id}`); - expect(stdout).toContain(`Name: ${pageType.label}`); - expect(stdout).toContain("Repeatable: true"); - expect(stdout).toContain("Tabs: Main"); -}); - -it("views a page type as JSON", async ({ expect, prismic, repo, token, host }) => { - const pageType = buildCustomType({ format: "page" }); - await insertCustomType(pageType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("page-type", ["view", pageType.label!, "--json"]); - expect(exitCode).toBe(0); - const parsed = JSON.parse(stdout); - expect(parsed).toMatchObject({ id: pageType.id, label: pageType.label, format: "page" }); -}); diff --git a/test/page-type.test.ts b/test/page-type.test.ts deleted file mode 100644 index 32f9bc7..0000000 --- a/test/page-type.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { it } from "./it"; - -it("prints help by default", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type"); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type [options]"); -}); - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("page-type", ["--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic page-type [options]"); -}); diff --git a/test/type-create.test.ts b/test/type-create.test.ts new file mode 100644 index 0000000..269406b --- /dev/null +++ b/test/type-create.test.ts @@ -0,0 +1,66 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["create", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type create [options]"); +}); + +it("creates a custom type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + + const { stdout, exitCode } = await prismic("type", ["create", label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created type "${label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "custom", repeatable: true }); +}); + +it("creates a page type with --format page", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "page" }); + + const { stdout, exitCode } = await prismic("type", ["create", label!, "--format", "page"]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created type "${label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "page", repeatable: true }); + expect(created!.json).toHaveProperty("SEO & Metadata"); + expect(created!.json.Main).toHaveProperty("slices"); +}); + +it("creates a single custom type", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + + const { exitCode } = await prismic("type", ["create", label!, "--single"]); + expect(exitCode).toBe(0); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.label === label); + expect(created).toMatchObject({ format: "custom", repeatable: false }); +}); + +it("creates a custom type with a custom id", async ({ expect, prismic, repo, token, host }) => { + const { label } = buildCustomType({ format: "custom" }); + const id = `CustomType${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("type", ["create", label!, "--id", id]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Created type "${label}" (id: "${id}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const created = customTypes.find((ct) => ct.id === id); + expect(created).toBeDefined(); +}); + +it("rejects invalid --format", async ({ expect, prismic }) => { + const { label } = buildCustomType(); + + const { stderr, exitCode } = await prismic("type", ["create", label!, "--format", "invalid"]); + expect(exitCode).toBe(1); + expect(stderr).toContain('Invalid format: "invalid"'); +}); diff --git a/test/custom-type-list.test.ts b/test/type-list.test.ts similarity index 52% rename from test/custom-type-list.test.ts rename to test/type-list.test.ts index 069d270..543300c 100644 --- a/test/custom-type-list.test.ts +++ b/test/type-list.test.ts @@ -2,25 +2,28 @@ import { buildCustomType, it } from "./it"; import { insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type", ["list", "--help"]); + const { stdout, exitCode } = await prismic("type", ["list", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type list [options]"); + expect(stdout).toContain("prismic type list [options]"); }); -it("lists custom types", async ({ expect, prismic, repo, token, host }) => { +it("lists all types", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); + const pageType = buildCustomType({ format: "page" }); await insertCustomType(customType, { repo, token, host }); + await insertCustomType(pageType, { repo, token, host }); - const { stdout, exitCode } = await prismic("custom-type", ["list"]); + const { stdout, exitCode } = await prismic("type", ["list"]); expect(exitCode).toBe(0); - expect(stdout).toContain(`${customType.label} (id: ${customType.id})`); + expect(stdout).toContain(`${customType.label} (id: ${customType.id}, format: custom)`); + expect(stdout).toContain(`${pageType.label} (id: ${pageType.id}, format: page)`); }); -it("lists custom types as JSON", async ({ expect, prismic, repo, token, host }) => { +it("lists types as JSON", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("custom-type", ["list", "--json"]); + const { stdout, exitCode } = await prismic("type", ["list", "--json"]); expect(exitCode).toBe(0); const parsed = JSON.parse(stdout); expect(parsed).toEqual( diff --git a/test/custom-type-remove.test.ts b/test/type-remove.test.ts similarity index 54% rename from test/custom-type-remove.test.ts rename to test/type-remove.test.ts index 166471d..1d51363 100644 --- a/test/custom-type-remove.test.ts +++ b/test/type-remove.test.ts @@ -2,18 +2,18 @@ import { buildCustomType, it } from "./it"; import { getCustomTypes, insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type", ["remove", "--help"]); + const { stdout, exitCode } = await prismic("type", ["remove", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type remove [options]"); + expect(stdout).toContain("prismic type remove [options]"); }); -it("removes a custom type", async ({ expect, prismic, repo, token, host }) => { +it("removes a type", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("custom-type", ["remove", customType.label!]); + const { stdout, exitCode } = await prismic("type", ["remove", customType.label!]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Custom type removed: "${customType.label}" (id: ${customType.id})`); + expect(stdout).toContain(`Type removed: "${customType.label}" (id: ${customType.id})`); const customTypes = await getCustomTypes({ repo, token, host }); const removed = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/custom-type-view.test.ts b/test/type-view.test.ts similarity index 61% rename from test/custom-type-view.test.ts rename to test/type-view.test.ts index 4d6493b..0c67b67 100644 --- a/test/custom-type-view.test.ts +++ b/test/type-view.test.ts @@ -2,28 +2,29 @@ import { buildCustomType, it } from "./it"; import { insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("custom-type", ["view", "--help"]); + const { stdout, exitCode } = await prismic("type", ["view", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic custom-type view [options]"); + expect(stdout).toContain("prismic type view [options]"); }); -it("views a custom type", async ({ expect, prismic, repo, token, host }) => { +it("views a type", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("custom-type", ["view", customType.label!]); + const { stdout, exitCode } = await prismic("type", ["view", customType.label!]); expect(exitCode).toBe(0); expect(stdout).toContain(`ID: ${customType.id}`); expect(stdout).toContain(`Name: ${customType.label}`); + expect(stdout).toContain("Format: custom"); expect(stdout).toContain("Repeatable: true"); expect(stdout).toContain("Tabs: Main"); }); -it("views a custom type as JSON", async ({ expect, prismic, repo, token, host }) => { +it("views a type as JSON", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("custom-type", ["view", customType.label!, "--json"]); + const { stdout, exitCode } = await prismic("type", ["view", customType.label!, "--json"]); expect(exitCode).toBe(0); const parsed = JSON.parse(stdout); expect(parsed).toMatchObject({ id: customType.id, label: customType.label, format: "custom" }); diff --git a/test/type.test.ts b/test/type.test.ts new file mode 100644 index 0000000..4daa8f5 --- /dev/null +++ b/test/type.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type [options]"); +}); + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type [options]"); +}); From 901a0bead6ed7b1b695b444bdff5f64186359fe7 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Apr 2026 14:56:58 -1000 Subject: [PATCH 11/29] feat: add `type edit` command (#106) Closes #91 Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/type-edit.ts | 64 +++++++++++++++++++++++++++++++++++++++ src/commands/type.ts | 5 +++ 2 files changed, 69 insertions(+) create mode 100644 src/commands/type-edit.ts diff --git a/src/commands/type-edit.ts b/src/commands/type-edit.ts new file mode 100644 index 0000000..cfe7ffb --- /dev/null +++ b/src/commands/type-edit.ts @@ -0,0 +1,64 @@ +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic type edit", + description: "Edit a content type.", + positionals: { + name: { description: "Name of the content type", required: true }, + }, + options: { + name: { type: "string", short: "n", description: "New name for the type" }, + format: { type: "string", short: "f", description: 'Type format: "custom" or "page"' }, + repeatable: { type: "boolean", description: "Allow multiple documents of this type" }, + single: { type: "boolean", short: "s", description: "Restrict to a single document" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [currentName] = positionals; + const { repo = await getRepositoryName() } = values; + + if ("repeatable" in values && "single" in values) { + throw new CommandError("Cannot use both --repeatable and --single"); + } + + if ("format" in values && values.format !== "custom" && values.format !== "page") { + throw new CommandError(`Invalid format: "${values.format}". Use "custom" or "page".`); + } + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const type = customTypes.find((ct) => ct.label === currentName); + + if (!type) { + throw new CommandError(`Type not found: ${currentName}`); + } + + if ("name" in values) type.label = values.name; + if ("format" in values) type.format = values.format as "custom" | "page"; + if ("repeatable" in values) type.repeatable = true; + if ("single" in values) type.repeatable = false; + + try { + await updateCustomType(type, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update type: ${message}`); + } + throw error; + } + + await adapter.updateCustomType(type); + await adapter.generateTypes(); + + console.info(`Type updated: "${type.label}" (id: ${type.id})`); +}); diff --git a/src/commands/type.ts b/src/commands/type.ts index 5cf5814..ffad94e 100644 --- a/src/commands/type.ts +++ b/src/commands/type.ts @@ -1,5 +1,6 @@ import { createCommandRouter } from "../lib/command"; import typeCreate from "./type-create"; +import typeEdit from "./type-edit"; import typeList from "./type-list"; import typeRemove from "./type-remove"; import typeView from "./type-view"; @@ -12,6 +13,10 @@ export default createCommandRouter({ handler: typeCreate, description: "Create a new content type", }, + edit: { + handler: typeEdit, + description: "Edit a content type", + }, remove: { handler: typeRemove, description: "Remove a content type", From ebf788ba14a824626557778822082f943a39c571 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Apr 2026 15:55:02 -1000 Subject: [PATCH 12/29] feat: add `slice edit` and `slice edit-variation` commands (#107) * feat: add `slice edit` and `slice edit-variation` commands Adds two new subcommands for editing slice and variation metadata after creation. Also adds E2E tests for the recently added `type edit` command. Closes #92 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add local fallback for `type edit` adapter call Wraps adapter.updateCustomType in try/catch with createCustomType fallback, matching the pattern used in other commands. Prevents crash when the local customtypes/ directory doesn't contain the edited type. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove --repeatable/--single flags from `type edit` The Custom Types API does not support updating the repeatable property; it must be changed from the writing room UI. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove --description from `slice edit-variation` Edit commands should only expose options that the corresponding create command exposes. `slice add-variation` does not accept --description. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/slice-edit-variation.ts | 65 ++++++++++++++++++++++++++++ src/commands/slice-edit.ts | 58 +++++++++++++++++++++++++ src/commands/slice.ts | 10 +++++ src/commands/type-edit.ts | 14 +++--- test/slice-edit-variation.test.ts | 48 ++++++++++++++++++++ test/slice-edit.test.ts | 23 ++++++++++ test/type-edit.test.ts | 37 ++++++++++++++++ 7 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 src/commands/slice-edit-variation.ts create mode 100644 src/commands/slice-edit.ts create mode 100644 test/slice-edit-variation.test.ts create mode 100644 test/slice-edit.test.ts create mode 100644 test/type-edit.test.ts diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts new file mode 100644 index 0000000..9eba13e --- /dev/null +++ b/src/commands/slice-edit-variation.ts @@ -0,0 +1,65 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getSlices, updateSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice edit-variation", + description: "Edit a variation of a slice.", + positionals: { + name: { description: "Name of the variation", required: true }, + }, + options: { + in: { type: "string", required: true, description: "Name of the slice" }, + name: { type: "string", short: "n", description: "New name for the variation" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [currentName] = positionals; + const { in: sliceName, repo = await getRepositoryName() } = values; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((s) => s.name === sliceName); + + if (!slice) { + throw new CommandError(`Slice not found: ${sliceName}`); + } + + const variation = slice.variations.find((v) => v.name === currentName); + + if (!variation) { + throw new CommandError(`Variation "${currentName}" not found in slice "${sliceName}".`); + } + + if ("name" in values) variation.name = values.name!; + + const updatedSlice: SharedSlice = { ...slice }; + + try { + await updateSlice(updatedSlice, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update variation: ${message}`); + } + throw error; + } + + try { + await adapter.updateSlice(updatedSlice); + } catch { + await adapter.createSlice(updatedSlice); + } + await adapter.generateTypes(); + + console.info(`Variation updated: "${variation.name}" in slice "${sliceName}"`); +}); diff --git a/src/commands/slice-edit.ts b/src/commands/slice-edit.ts new file mode 100644 index 0000000..f1b5c9a --- /dev/null +++ b/src/commands/slice-edit.ts @@ -0,0 +1,58 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getSlices, updateSlice } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic slice edit", + description: "Edit a slice.", + positionals: { + name: { description: "Name of the slice", required: true }, + }, + options: { + name: { type: "string", short: "n", description: "New name for the slice" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [currentName] = positionals; + const { repo = await getRepositoryName() } = values; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const slices = await getSlices({ repo, token, host }); + const slice = slices.find((s) => s.name === currentName); + + if (!slice) { + throw new CommandError(`Slice not found: ${currentName}`); + } + + const updatedSlice: SharedSlice = { ...slice }; + + if ("name" in values) updatedSlice.name = values.name!; + + try { + await updateSlice(updatedSlice, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update slice: ${message}`); + } + throw error; + } + + try { + await adapter.updateSlice(updatedSlice); + } catch { + await adapter.createSlice(updatedSlice); + } + await adapter.generateTypes(); + + console.info(`Slice updated: "${updatedSlice.name}" (id: ${updatedSlice.id})`); +}); diff --git a/src/commands/slice.ts b/src/commands/slice.ts index e96e27d..dae0e50 100644 --- a/src/commands/slice.ts +++ b/src/commands/slice.ts @@ -3,6 +3,8 @@ import sliceAddVariation from "./slice-add-variation"; import sliceConnect from "./slice-connect"; import sliceCreate from "./slice-create"; import sliceDisconnect from "./slice-disconnect"; +import sliceEdit from "./slice-edit"; +import sliceEditVariation from "./slice-edit-variation"; import sliceList from "./slice-list"; import sliceRemove from "./slice-remove"; import sliceRemoveVariation from "./slice-remove-variation"; @@ -16,6 +18,10 @@ export default createCommandRouter({ handler: sliceCreate, description: "Create a new slice", }, + edit: { + handler: sliceEdit, + description: "Edit a slice", + }, remove: { handler: sliceRemove, description: "Remove a slice", @@ -40,6 +46,10 @@ export default createCommandRouter({ handler: sliceAddVariation, description: "Add a variation to a slice", }, + "edit-variation": { + handler: sliceEditVariation, + description: "Edit a variation of a slice", + }, "remove-variation": { handler: sliceRemoveVariation, description: "Remove a variation from a slice", diff --git a/src/commands/type-edit.ts b/src/commands/type-edit.ts index cfe7ffb..bffabdc 100644 --- a/src/commands/type-edit.ts +++ b/src/commands/type-edit.ts @@ -14,8 +14,6 @@ const config = { options: { name: { type: "string", short: "n", description: "New name for the type" }, format: { type: "string", short: "f", description: 'Type format: "custom" or "page"' }, - repeatable: { type: "boolean", description: "Allow multiple documents of this type" }, - single: { type: "boolean", short: "s", description: "Restrict to a single document" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; @@ -24,10 +22,6 @@ export default createCommand(config, async ({ positionals, values }) => { const [currentName] = positionals; const { repo = await getRepositoryName() } = values; - if ("repeatable" in values && "single" in values) { - throw new CommandError("Cannot use both --repeatable and --single"); - } - if ("format" in values && values.format !== "custom" && values.format !== "page") { throw new CommandError(`Invalid format: "${values.format}". Use "custom" or "page".`); } @@ -44,8 +38,6 @@ export default createCommand(config, async ({ positionals, values }) => { if ("name" in values) type.label = values.name; if ("format" in values) type.format = values.format as "custom" | "page"; - if ("repeatable" in values) type.repeatable = true; - if ("single" in values) type.repeatable = false; try { await updateCustomType(type, { repo, host, token }); @@ -57,7 +49,11 @@ export default createCommand(config, async ({ positionals, values }) => { throw error; } - await adapter.updateCustomType(type); + try { + await adapter.updateCustomType(type); + } catch { + await adapter.createCustomType(type); + } await adapter.generateTypes(); console.info(`Type updated: "${type.label}" (id: ${type.id})`); diff --git a/test/slice-edit-variation.test.ts b/test/slice-edit-variation.test.ts new file mode 100644 index 0000000..cf78504 --- /dev/null +++ b/test/slice-edit-variation.test.ts @@ -0,0 +1,48 @@ +import { buildSlice, it } from "./it"; +import { getSlices, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["edit-variation", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice edit-variation [options]"); +}); + +it("edits a variation name", async ({ expect, prismic, repo, token, host }) => { + const variationName = `Variation${crypto.randomUUID().split("-")[0]}`; + const variationId = `variation${crypto.randomUUID().split("-")[0]}`; + const slice = buildSlice(); + const sliceWithVariation = { + ...slice, + variations: [ + ...slice.variations, + { + id: variationId, + name: variationName, + description: variationName, + docURL: "", + imageUrl: "", + version: "", + }, + ], + }; + + await insertSlice(sliceWithVariation, { repo, token, host }); + + const newName = `Variation${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", [ + "edit-variation", + variationName, + "--in", + slice.name, + "--name", + newName, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Variation updated: "${newName}" in slice "${slice.name}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.id === variationId); + expect(variation?.name).toBe(newName); +}); diff --git a/test/slice-edit.test.ts b/test/slice-edit.test.ts new file mode 100644 index 0000000..2f76eb9 --- /dev/null +++ b/test/slice-edit.test.ts @@ -0,0 +1,23 @@ +import { buildSlice, it } from "./it"; +import { getSlices, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("slice", ["edit", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic slice edit [options]"); +}); + +it("edits a slice name", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const newName = `SliceS${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", ["edit", slice.name, "--name", newName]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Slice updated: "${newName}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + expect(updated?.name).toBe(newName); +}); diff --git a/test/type-edit.test.ts b/test/type-edit.test.ts new file mode 100644 index 0000000..0df5449 --- /dev/null +++ b/test/type-edit.test.ts @@ -0,0 +1,37 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["edit", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type edit [options]"); +}); + +it("edits a type name", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const newName = `TypeT${crypto.randomUUID().split("-")[0]}`; + + const { stdout, stderr, exitCode } = await prismic("type", ["edit", customType.label!, "--name", newName]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Type updated: "${newName}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.label).toBe(newName); +}); + +it("edits a type format", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ format: "custom" }); + await insertCustomType(customType, { repo, token, host }); + + const { stderr, exitCode } = await prismic("type", ["edit", customType.label!, "--format", "page"]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.format).toBe("page"); +}); From 98ed067bbdddec4ad16d47ed3ce9ec5e4a349bbf Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Apr 2026 17:54:00 -1000 Subject: [PATCH 13/29] feat: add tab management commands (#108) * feat: add `type add-tab`, `type edit-tab`, and `type remove-tab` commands Adds tab management commands for content types: - `add-tab` creates a new tab, optionally with a slice zone - `edit-tab` renames a tab and/or adds/removes a slice zone - `remove-tab` deletes a tab (guards against removing the last one) Closes #94 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: validate mutually exclusive --with-slice-zone and --without-slice-zone flags Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/type-add-tab.ts | 67 +++++++++++++++++++ src/commands/type-edit-tab.ts | 113 ++++++++++++++++++++++++++++++++ src/commands/type-remove-tab.ts | 62 ++++++++++++++++++ src/commands/type.ts | 15 +++++ test/type-add-tab.test.ts | 52 +++++++++++++++ test/type-edit-tab.test.ts | 80 ++++++++++++++++++++++ test/type-remove-tab.test.ts | 27 ++++++++ 7 files changed, 416 insertions(+) create mode 100644 src/commands/type-add-tab.ts create mode 100644 src/commands/type-edit-tab.ts create mode 100644 src/commands/type-remove-tab.ts create mode 100644 test/type-add-tab.test.ts create mode 100644 test/type-edit-tab.test.ts create mode 100644 test/type-remove-tab.test.ts diff --git a/src/commands/type-add-tab.ts b/src/commands/type-add-tab.ts new file mode 100644 index 0000000..7744172 --- /dev/null +++ b/src/commands/type-add-tab.ts @@ -0,0 +1,67 @@ +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic type add-tab", + description: "Add a tab to a content type.", + positionals: { + name: { description: "Name of the tab", required: true }, + }, + options: { + to: { type: "string", required: true, description: "Name of the content type" }, + "with-slice-zone": { type: "boolean", description: "Add a slice zone to the tab" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { to, "with-slice-zone": withSliceZone, repo = await getRepositoryName() } = values; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const type = customTypes.find((ct) => ct.label === to); + + if (!type) { + throw new CommandError(`Type not found: ${to}`); + } + + if (name in type.json) { + throw new CommandError(`Tab "${name}" already exists in "${to}".`); + } + + type.json[name] = withSliceZone + ? { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }, + } + : {}; + + try { + await updateCustomType(type, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to add tab: ${message}`); + } + throw error; + } + + try { + await adapter.updateCustomType(type); + } catch { + await adapter.createCustomType(type); + } + await adapter.generateTypes(); + + console.info(`Added tab "${name}" to "${to}"`); +}); diff --git a/src/commands/type-edit-tab.ts b/src/commands/type-edit-tab.ts new file mode 100644 index 0000000..6a8e3af --- /dev/null +++ b/src/commands/type-edit-tab.ts @@ -0,0 +1,113 @@ +import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; + +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic type edit-tab", + description: "Edit a tab of a content type.", + positionals: { + name: { description: "Current name of the tab", required: true }, + }, + options: { + in: { type: "string", required: true, description: "Name of the content type" }, + name: { type: "string", short: "n", description: "New name for the tab" }, + "with-slice-zone": { type: "boolean", description: "Add a slice zone to the tab" }, + "without-slice-zone": { type: "boolean", description: "Remove the slice zone from the tab" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [currentName] = positionals; + const { in: typeName, repo = await getRepositoryName() } = values; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const type = customTypes.find((ct) => ct.label === typeName); + + if (!type) { + throw new CommandError(`Type not found: ${typeName}`); + } + + if (!(currentName in type.json)) { + throw new CommandError(`Tab "${currentName}" not found in "${typeName}".`); + } + + if ("with-slice-zone" in values && "without-slice-zone" in values) { + throw new CommandError("Cannot use --with-slice-zone and --without-slice-zone together."); + } + + if ("with-slice-zone" in values) { + const tab = type.json[currentName]; + const hasSliceZone = Object.values(tab).some((field) => field.type === "Slices"); + + if (hasSliceZone) { + throw new CommandError(`Tab "${currentName}" already has a slice zone.`); + } + + tab.slices = { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }; + } + + if ("without-slice-zone" in values) { + const tab = type.json[currentName]; + const sliceZoneEntry = Object.entries(tab).find(([, field]) => field.type === "Slices"); + + if (!sliceZoneEntry) { + throw new CommandError(`Tab "${currentName}" does not have a slice zone.`); + } + + const [sliceZoneId, sliceZoneField] = sliceZoneEntry; + const choices = + sliceZoneField.type === "Slices" ? (sliceZoneField.config?.choices ?? {}) : {}; + + if (Object.keys(choices).length > 0) { + throw new CommandError( + `Cannot remove slice zone from "${currentName}": disconnect all slices first.`, + ); + } + + delete tab[sliceZoneId]; + } + + if ("name" in values) { + if (values.name! in type.json) { + throw new CommandError(`Tab "${values.name}" already exists in "${typeName}".`); + } + + const newJson: CustomType["json"] = {}; + for (const [key, value] of Object.entries(type.json)) { + newJson[key === currentName ? values.name! : key] = value; + } + type.json = newJson; + } + + try { + await updateCustomType(type, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to update tab: ${message}`); + } + throw error; + } + + try { + await adapter.updateCustomType(type); + } catch { + await adapter.createCustomType(type); + } + await adapter.generateTypes(); + + console.info(`Tab updated: "${currentName}" in "${typeName}"`); +}); diff --git a/src/commands/type-remove-tab.ts b/src/commands/type-remove-tab.ts new file mode 100644 index 0000000..e0a8c3e --- /dev/null +++ b/src/commands/type-remove-tab.ts @@ -0,0 +1,62 @@ +import { getAdapter } from "../adapters"; +import { getHost, getToken } from "../auth"; +import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { UnknownRequestError } from "../lib/request"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic type remove-tab", + description: "Remove a tab from a content type.", + positionals: { + name: { description: "Name of the tab", required: true }, + }, + options: { + from: { type: "string", required: true, description: "Name of the content type" }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [name] = positionals; + const { from, repo = await getRepositoryName() } = values; + + const adapter = await getAdapter(); + const token = await getToken(); + const host = await getHost(); + const customTypes = await getCustomTypes({ repo, token, host }); + const type = customTypes.find((ct) => ct.label === from); + + if (!type) { + throw new CommandError(`Type not found: ${from}`); + } + + if (!(name in type.json)) { + throw new CommandError(`Tab "${name}" not found in "${from}".`); + } + + if (Object.keys(type.json).length <= 1) { + throw new CommandError(`Cannot remove the last tab from "${from}".`); + } + + delete type.json[name]; + + try { + await updateCustomType(type, { repo, host, token }); + } catch (error) { + if (error instanceof UnknownRequestError) { + const message = await error.text(); + throw new CommandError(`Failed to remove tab: ${message}`); + } + throw error; + } + + try { + await adapter.updateCustomType(type); + } catch { + await adapter.createCustomType(type); + } + await adapter.generateTypes(); + + console.info(`Removed tab "${name}" from "${from}"`); +}); diff --git a/src/commands/type.ts b/src/commands/type.ts index ffad94e..d7c3134 100644 --- a/src/commands/type.ts +++ b/src/commands/type.ts @@ -1,8 +1,11 @@ import { createCommandRouter } from "../lib/command"; +import typeAddTab from "./type-add-tab"; import typeCreate from "./type-create"; import typeEdit from "./type-edit"; +import typeEditTab from "./type-edit-tab"; import typeList from "./type-list"; import typeRemove from "./type-remove"; +import typeRemoveTab from "./type-remove-tab"; import typeView from "./type-view"; export default createCommandRouter({ @@ -29,5 +32,17 @@ export default createCommandRouter({ handler: typeView, description: "View a content type", }, + "add-tab": { + handler: typeAddTab, + description: "Add a tab to a content type", + }, + "edit-tab": { + handler: typeEditTab, + description: "Edit a tab of a content type", + }, + "remove-tab": { + handler: typeRemoveTab, + description: "Remove a tab from a content type", + }, }, }); diff --git a/test/type-add-tab.test.ts b/test/type-add-tab.test.ts new file mode 100644 index 0000000..71ccce0 --- /dev/null +++ b/test/type-add-tab.test.ts @@ -0,0 +1,52 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["add-tab", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type add-tab [options]"); +}); + +it("adds a tab to a type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const tabName = `Tab${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("type", [ + "add-tab", + tabName, + "--to", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Added tab "${tabName}" to "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.json).toHaveProperty(tabName); + expect(updated?.json[tabName]).toEqual({}); +}); + +it("adds a tab with a slice zone", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const tabName = `Tab${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("type", [ + "add-tab", + tabName, + "--to", + customType.label!, + "--with-slice-zone", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Added tab "${tabName}" to "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + const tab = updated?.json[tabName]; + expect(tab).toHaveProperty("slices"); + expect(tab?.slices).toMatchObject({ type: "Slices" }); +}); diff --git a/test/type-edit-tab.test.ts b/test/type-edit-tab.test.ts new file mode 100644 index 0000000..dd473b6 --- /dev/null +++ b/test/type-edit-tab.test.ts @@ -0,0 +1,80 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["edit-tab", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type edit-tab [options]"); +}); + +it("edits a tab name", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ json: { Main: {}, OldName: {} } }); + await insertCustomType(customType, { repo, token, host }); + + const newName = `Tab${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("type", [ + "edit-tab", + "OldName", + "--in", + customType.label!, + "--name", + newName, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Tab updated: "OldName" in "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.json).not.toHaveProperty("OldName"); + expect(updated?.json).toHaveProperty(newName); +}); + +it("adds a slice zone to a tab", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("type", [ + "edit-tab", + "Main", + "--in", + customType.label!, + "--with-slice-zone", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Tab updated: "Main" in "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.json.Main).toHaveProperty("slices"); + expect(updated?.json.Main.slices).toMatchObject({ type: "Slices" }); +}); + +it("removes a slice zone from a tab", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ + json: { + Main: { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { choices: {} }, + }, + }, + }, + }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("type", [ + "edit-tab", + "Main", + "--in", + customType.label!, + "--without-slice-zone", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Tab updated: "Main" in "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.json.Main).not.toHaveProperty("slices"); +}); diff --git a/test/type-remove-tab.test.ts b/test/type-remove-tab.test.ts new file mode 100644 index 0000000..0b30dc6 --- /dev/null +++ b/test/type-remove-tab.test.ts @@ -0,0 +1,27 @@ +import { buildCustomType, it } from "./it"; +import { getCustomTypes, insertCustomType } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("type", ["remove-tab", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic type remove-tab [options]"); +}); + +it("removes a tab from a type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ json: { Main: {}, Extra: {} } }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("type", [ + "remove-tab", + "Extra", + "--from", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Removed tab "Extra" from "${customType.label}"`); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(updated?.json).not.toHaveProperty("Extra"); + expect(updated?.json).toHaveProperty("Main"); +}); From ae4006a6f78b44c5c0c72e9b782c83d7f77880bd Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 11:56:53 -1000 Subject: [PATCH 14/29] feat: rename `--in` flag to `--from-slice`/`--from-type` (#110) * refactor: rename `--in` flag to `--from-slice`/`--from-type` Co-Authored-By: Claude Opus 4.6 (1M context) * fix: update tests to use renamed flags Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/slice-edit-variation.ts | 4 ++-- src/commands/type-edit-tab.ts | 4 ++-- test/slice-edit-variation.test.ts | 2 +- test/type-edit-tab.test.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index 9eba13e..b9f228a 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -14,7 +14,7 @@ const config = { name: { description: "Name of the variation", required: true }, }, options: { - in: { type: "string", required: true, description: "Name of the slice" }, + "from-slice": { type: "string", required: true, description: "Name of the slice" }, name: { type: "string", short: "n", description: "New name for the variation" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, @@ -22,7 +22,7 @@ const config = { export default createCommand(config, async ({ positionals, values }) => { const [currentName] = positionals; - const { in: sliceName, repo = await getRepositoryName() } = values; + const { "from-slice": sliceName, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); diff --git a/src/commands/type-edit-tab.ts b/src/commands/type-edit-tab.ts index 6a8e3af..103add8 100644 --- a/src/commands/type-edit-tab.ts +++ b/src/commands/type-edit-tab.ts @@ -14,7 +14,7 @@ const config = { name: { description: "Current name of the tab", required: true }, }, options: { - in: { type: "string", required: true, description: "Name of the content type" }, + "from-type": { type: "string", required: true, description: "Name of the content type" }, name: { type: "string", short: "n", description: "New name for the tab" }, "with-slice-zone": { type: "boolean", description: "Add a slice zone to the tab" }, "without-slice-zone": { type: "boolean", description: "Remove the slice zone from the tab" }, @@ -24,7 +24,7 @@ const config = { export default createCommand(config, async ({ positionals, values }) => { const [currentName] = positionals; - const { in: typeName, repo = await getRepositoryName() } = values; + const { "from-type": typeName, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); diff --git a/test/slice-edit-variation.test.ts b/test/slice-edit-variation.test.ts index cf78504..877c3c8 100644 --- a/test/slice-edit-variation.test.ts +++ b/test/slice-edit-variation.test.ts @@ -33,7 +33,7 @@ it("edits a variation name", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("slice", [ "edit-variation", variationName, - "--in", + "--from-slice", slice.name, "--name", newName, diff --git a/test/type-edit-tab.test.ts b/test/type-edit-tab.test.ts index dd473b6..cdddaf7 100644 --- a/test/type-edit-tab.test.ts +++ b/test/type-edit-tab.test.ts @@ -16,7 +16,7 @@ it("edits a tab name", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("type", [ "edit-tab", "OldName", - "--in", + "--from-type", customType.label!, "--name", newName, @@ -37,7 +37,7 @@ it("adds a slice zone to a tab", async ({ expect, prismic, repo, token, host }) const { stdout, exitCode } = await prismic("type", [ "edit-tab", "Main", - "--in", + "--from-type", customType.label!, "--with-slice-zone", ]); @@ -67,7 +67,7 @@ it("removes a slice zone from a tab", async ({ expect, prismic, repo, token, hos const { stdout, exitCode } = await prismic("type", [ "edit-tab", "Main", - "--in", + "--from-type", customType.label!, "--without-slice-zone", ]); From 75f5bc9d178b612bf670ca6d8c9ab332ad485114 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 11:57:15 -1000 Subject: [PATCH 15/29] feat: show fields inline in `view` commands and remove `field list` (#111) Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/field-list.ts | 45 ---------------------- src/commands/field.ts | 5 --- src/commands/slice-view.ts | 18 ++++++++- src/commands/type-view.ts | 18 ++++++++- test/field-list.test.ts | 78 -------------------------------------- test/slice-view.test.ts | 42 +++++++++++++++++++- test/type-view.test.ts | 26 ++++++++++++- 7 files changed, 98 insertions(+), 134 deletions(-) delete mode 100644 src/commands/field-list.ts delete mode 100644 test/field-list.test.ts diff --git a/src/commands/field-list.ts b/src/commands/field-list.ts deleted file mode 100644 index da74057..0000000 --- a/src/commands/field-list.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { getHost, getToken } from "../auth"; -import { createCommand, type CommandConfig } from "../lib/command"; -import { stringify } from "../lib/json"; -import { resolveModel, SOURCE_OPTIONS } from "../models"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic field list", - description: "List fields in a slice or custom type.", - options: { - ...SOURCE_OPTIONS, - json: { type: "boolean", description: "Output as JSON" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ values }) => { - const { repo = await getRepositoryName() } = values; - - const token = await getToken(); - const host = await getHost(); - const [rawFields] = await resolveModel(values, { repo, token, host }); - - const fields = Object.entries(rawFields).map(([id, field]) => { - return { - id, - type: field.type, - label: field.config?.label || undefined, - }; - }); - - if (values.json) { - console.info(stringify(fields)); - return; - } - - if (fields.length === 0) { - console.info("No fields found."); - return; - } - - for (const field of fields) { - const label = field.label ? ` ${field.label}` : ""; - console.info(`${field.id} ${field.type}${label}`); - } -}); diff --git a/src/commands/field.ts b/src/commands/field.ts index cf1e996..4e64602 100644 --- a/src/commands/field.ts +++ b/src/commands/field.ts @@ -1,7 +1,6 @@ import { createCommandRouter } from "../lib/command"; import fieldAdd from "./field-add"; import fieldEdit from "./field-edit"; -import fieldList from "./field-list"; import fieldRemove from "./field-remove"; export default createCommandRouter({ @@ -16,10 +15,6 @@ export default createCommandRouter({ handler: fieldEdit, description: "Edit a field", }, - list: { - handler: fieldList, - description: "List fields", - }, remove: { handler: fieldRemove, description: "Remove a field", diff --git a/src/commands/slice-view.ts b/src/commands/slice-view.ts index b0b0542..5063e1b 100644 --- a/src/commands/slice-view.ts +++ b/src/commands/slice-view.ts @@ -36,6 +36,20 @@ export default createCommand(config, async ({ positionals, values }) => { console.info(`ID: ${slice.id}`); console.info(`Name: ${slice.name}`); - const variations = slice.variations?.map((v) => v.id).join(", ") || "(none)"; - console.info(`Variations: ${variations}`); + + for (const variation of slice.variations ?? []) { + console.info(""); + console.info(`${variation.id}:`); + const entries = Object.entries(variation.primary ?? {}); + if (entries.length === 0) { + console.info(" (no fields)"); + } else { + for (const [id, field] of entries) { + const config = field.config as Record | undefined; + const label = (config?.label as string) || ""; + const placeholder = config?.placeholder ? `"${config.placeholder}"` : ""; + console.info(` ${[id, field.type, label, placeholder].filter(Boolean).join(" ")}`); + } + } + } }); diff --git a/src/commands/type-view.ts b/src/commands/type-view.ts index c0e7e56..a8a5183 100644 --- a/src/commands/type-view.ts +++ b/src/commands/type-view.ts @@ -38,6 +38,20 @@ export default createCommand(config, async ({ positionals, values }) => { console.info(`Name: ${type.label || "(no name)"}`); console.info(`Format: ${type.format}`); console.info(`Repeatable: ${type.repeatable}`); - const tabs = Object.keys(type.json).join(", ") || "(none)"; - console.info(`Tabs: ${tabs}`); + + for (const [tabName, fields] of Object.entries(type.json)) { + console.info(""); + console.info(`${tabName}:`); + const entries = Object.entries(fields); + if (entries.length === 0) { + console.info(" (no fields)"); + } else { + for (const [id, field] of entries) { + const config = field.config as Record | undefined; + const label = (config?.label as string) || ""; + const placeholder = config?.placeholder ? `"${config.placeholder}"` : ""; + console.info(` ${[id, field.type, label, placeholder].filter(Boolean).join(" ")}`); + } + } + } }); diff --git a/test/field-list.test.ts b/test/field-list.test.ts deleted file mode 100644 index 2257f29..0000000 --- a/test/field-list.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { buildSlice, it } from "./it"; -import { insertSlice } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("field", ["list", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic field list [options]"); -}); - -it("lists fields in a slice", async ({ expect, prismic, repo, token, host }) => { - const slice = buildSlice({ - variations: [ - { - id: "default", - name: "Default", - docURL: "", - version: "initial", - description: "Default", - imageUrl: "", - primary: { - title: { type: "StructuredText", config: { label: "Title" } }, - is_active: { type: "Boolean", config: { label: "Is Active" } }, - }, - }, - ], - }); - await insertSlice(slice, { repo, token, host }); - - const { stdout, exitCode } = await prismic("field", ["list", "--from-slice", slice.name]); - expect(exitCode).toBe(0); - expect(stdout).toContain("title"); - expect(stdout).toContain("StructuredText"); - expect(stdout).toContain("is_active"); - expect(stdout).toContain("Boolean"); -}); - -it("lists fields as JSON with --json", async ({ expect, prismic, repo, token, host }) => { - const slice = buildSlice({ - variations: [ - { - id: "default", - name: "Default", - docURL: "", - version: "initial", - description: "Default", - imageUrl: "", - primary: { - title: { type: "StructuredText", config: { label: "Title" } }, - }, - }, - ], - }); - await insertSlice(slice, { repo, token, host }); - - const { stdout, exitCode } = await prismic("field", [ - "list", - "--from-slice", - slice.name, - "--json", - ]); - expect(exitCode).toBe(0); - - const fields = JSON.parse(stdout); - expect(fields).toContainEqual({ - id: "title", - type: "StructuredText", - label: "Title", - }); -}); - -it("prints message when no fields exist", async ({ expect, prismic, repo, token, host }) => { - const slice = buildSlice(); - await insertSlice(slice, { repo, token, host }); - - const { stdout, exitCode } = await prismic("field", ["list", "--from-slice", slice.name]); - expect(exitCode).toBe(0); - expect(stdout).toContain("No fields found."); -}); diff --git a/test/slice-view.test.ts b/test/slice-view.test.ts index 466f8ab..c159e0c 100644 --- a/test/slice-view.test.ts +++ b/test/slice-view.test.ts @@ -15,7 +15,47 @@ it("views a slice", async ({ expect, prismic, repo, token, host }) => { expect(exitCode).toBe(0); expect(stdout).toContain(`ID: ${slice.id}`); expect(stdout).toContain(`Name: ${slice.name}`); - expect(stdout).toContain("Variations: default"); + expect(stdout).toContain("default:"); +}); + +it("shows fields per variation", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice({ + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + title: { type: "StructuredText", config: { label: "Title", placeholder: "Enter title" } }, + is_active: { type: "Boolean", config: { label: "Is Active" } }, + }, + }, + { + id: "withImage", + name: "With Image", + docURL: "", + version: "initial", + description: "With Image", + imageUrl: "", + primary: { + image: { type: "Image", config: { label: "Image" } }, + }, + }, + ], + }); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", ["view", slice.name]); + expect(exitCode).toBe(0); + expect(stdout).toContain("default:"); + expect(stdout).toContain("title StructuredText Title"); + expect(stdout).toContain('"Enter title"'); + expect(stdout).toContain("is_active Boolean Is Active"); + expect(stdout).toContain("withImage:"); + expect(stdout).toContain("image Image Image"); }); it("views a slice as JSON", async ({ expect, prismic, repo, token, host }) => { diff --git a/test/type-view.test.ts b/test/type-view.test.ts index 0c67b67..f0a843b 100644 --- a/test/type-view.test.ts +++ b/test/type-view.test.ts @@ -17,7 +17,31 @@ it("views a type", async ({ expect, prismic, repo, token, host }) => { expect(stdout).toContain(`Name: ${customType.label}`); expect(stdout).toContain("Format: custom"); expect(stdout).toContain("Repeatable: true"); - expect(stdout).toContain("Tabs: Main"); + expect(stdout).toContain("Main:"); +}); + +it("shows fields per tab", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType({ + json: { + Main: { + title: { type: "StructuredText", config: { label: "Title", placeholder: "Enter title" } }, + is_active: { type: "Boolean", config: { label: "Is Active" } }, + }, + SEO: { + meta_title: { type: "Text", config: { label: "Meta Title" } }, + }, + }, + }); + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("type", ["view", customType.label!]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Main:"); + expect(stdout).toContain("title StructuredText Title"); + expect(stdout).toContain('"Enter title"'); + expect(stdout).toContain("is_active Boolean Is Active"); + expect(stdout).toContain("SEO:"); + expect(stdout).toContain("meta_title Text Meta Title"); }); it("views a type as JSON", async ({ expect, prismic, repo, token, host }) => { From 68f620f267eb2e0beb2200accf15ccbea78b99e9 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 12:11:49 -1000 Subject: [PATCH 16/29] feat: add `field view` command (#112) * feat: add `field view` command Add a command to inspect a single field's full configuration, including label, placeholder, constraints, and type-specific settings. Resolves #93 Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: use compact buildSlice pattern in field-view tests Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/field-view.ts | 56 +++++++++++++++++++++++++ src/commands/field.ts | 5 +++ test/field-view.test.ts | 86 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/commands/field-view.ts create mode 100644 test/field-view.test.ts diff --git a/src/commands/field-view.ts b/src/commands/field-view.ts new file mode 100644 index 0000000..fde0c62 --- /dev/null +++ b/src/commands/field-view.ts @@ -0,0 +1,56 @@ +import { capitalCase } from "change-case"; + +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; +import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field view", + description: "View details of a field in a slice or custom type.", + positionals: { + id: { description: "Field ID", required: true }, + }, + options: { + ...SOURCE_OPTIONS, + json: { type: "boolean", description: "Output as JSON" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { repo = await getRepositoryName() } = values; + + const token = await getToken(); + const host = await getHost(); + const [fields] = await resolveFieldContainer(id, values, { repo, token, host }); + const [targetFields, fieldId] = resolveFieldTarget(fields, id); + + const field = targetFields[fieldId]; + if (!field) { + throw new CommandError(`Field "${id}" does not exist.`); + } + + if (values.json) { + console.info(stringify({ id: fieldId, ...field })); + return; + } + + console.info(`Type: ${field.type}`); + + if (field.config) { + for (const [key, value] of Object.entries(field.config)) { + if (value === undefined) continue; + + if (key === "fields" && typeof value === "object" && value !== null) { + const ids = Object.keys(value).join(", ") || "(none)"; + console.info(`Fields: ${ids}`); + } else if (Array.isArray(value)) { + console.info(`${capitalCase(key)}: ${value.join(", ")}`); + } else { + console.info(`${capitalCase(key)}: ${value}`); + } + } + } +}); diff --git a/src/commands/field.ts b/src/commands/field.ts index 4e64602..363a042 100644 --- a/src/commands/field.ts +++ b/src/commands/field.ts @@ -2,6 +2,7 @@ import { createCommandRouter } from "../lib/command"; import fieldAdd from "./field-add"; import fieldEdit from "./field-edit"; import fieldRemove from "./field-remove"; +import fieldView from "./field-view"; export default createCommandRouter({ name: "prismic field", @@ -19,5 +20,9 @@ export default createCommandRouter({ handler: fieldRemove, description: "Remove a field", }, + view: { + handler: fieldView, + description: "View details of a field", + }, }, }); diff --git a/test/field-view.test.ts b/test/field-view.test.ts new file mode 100644 index 0000000..964ea09 --- /dev/null +++ b/test/field-view.test.ts @@ -0,0 +1,86 @@ +import { buildCustomType, buildSlice, it } from "./it"; +import { insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["view", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field view [options]"); +}); + +it("views a field in a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.title = { + type: "StructuredText", + config: { label: "Title", placeholder: "Enter title" }, + }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "view", + "title", + "--from-slice", + slice.name, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Type: StructuredText"); + expect(stdout).toContain("Label: Title"); + expect(stdout).toContain("Placeholder: Enter title"); +}); + +it("views a field in a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + customType.json.Main.count = { + type: "Number", + config: { label: "Count", placeholder: "Enter number", min: 0, max: 100 }, + }; + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "view", + "count", + "--from-type", + customType.label!, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Type: Number"); + expect(stdout).toContain("Label: Count"); + expect(stdout).toContain("Min: 0"); + expect(stdout).toContain("Max: 100"); +}); + +it("outputs JSON with --json", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.is_active = { + type: "Boolean", + config: { label: "Is Active", default_value: true }, + }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "view", + "is_active", + "--from-slice", + slice.name, + "--json", + ]); + expect(exitCode).toBe(0); + + const field = JSON.parse(stdout); + expect(field.id).toBe("is_active"); + expect(field.type).toBe("Boolean"); + expect(field.config.label).toBe("Is Active"); +}); + +it("errors for non-existent field", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stderr, exitCode } = await prismic("field", [ + "view", + "nonexistent", + "--from-slice", + slice.name, + ]); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("nonexistent"); +}); From 8d68cd13b367bdb6446ec403a23400fa38197ab0 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 12:24:30 -1000 Subject: [PATCH 17/29] feat: consolidate link-related field types (#114) * feat: consolidate link-related field types Merge `link-to-media` into `link` with a new `--allow` option that accepts `document`, `media`, or `web`. Omitting `--allow` creates a generic link. Keep `content-relationship` separate. Update descriptions on `link` and `content-relationship` so agents can tell when to use each one. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: validate --allow option and restore field-edit subtype guards Validate that --allow is one of document, media, or web. Restore subtype guards in field-edit so content-relationship fields can't receive link-only options and vice versa. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: allow all Link options regardless of select value Remove the select-based guard in field-edit so link fields with select: "document" can still be edited with link-specific options like --allow-target-blank and --allow-text. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../field-add-content-relationship.ts | 3 +- src/commands/field-add-link-to-media.ts | 53 ------------------ src/commands/field-add-link.ts | 16 +++++- src/commands/field-add.ts | 9 +--- src/commands/field-edit.ts | 25 +++------ test/field-add-link-to-media.test.ts | 54 ------------------- test/field-add-link.test.ts | 29 +++++++++- 7 files changed, 55 insertions(+), 134 deletions(-) delete mode 100644 src/commands/field-add-link-to-media.ts delete mode 100644 test/field-add-link-to-media.test.ts diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts index 75b52bb..c93b592 100644 --- a/src/commands/field-add-content-relationship.ts +++ b/src/commands/field-add-content-relationship.ts @@ -9,7 +9,8 @@ import { getRepositoryName } from "../project"; const config = { name: "prismic field add content-relationship", - description: "Add a content relationship field to a slice or custom type.", + description: + "Add a content relationship field to a slice or custom type. Use for querying and displaying data from related documents (e.g. an author or category). For navigational links, use link instead.", positionals: { id: { description: "Field ID", required: true }, }, diff --git a/src/commands/field-add-link-to-media.ts b/src/commands/field-add-link-to-media.ts deleted file mode 100644 index 1d62691..0000000 --- a/src/commands/field-add-link-to-media.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Link } from "@prismicio/types-internal/lib/customtypes"; - -import { capitalCase } from "change-case"; - -import { getHost, getToken } from "../auth"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; -import { getRepositoryName } from "../project"; - -const config = { - name: "prismic field add link-to-media", - description: "Add a link to media field to a slice or custom type.", - positionals: { - id: { description: "Field ID", required: true }, - }, - options: { - ...TARGET_OPTIONS, - label: { type: "string", description: "Field label" }, - "allow-text": { type: "boolean", description: "Allow custom link text" }, - variant: { type: "string", multiple: true, description: "Allowed variant (can be repeated)" }, - }, -} satisfies CommandConfig; - -export default createCommand(config, async ({ positionals, values }) => { - const [id] = positionals; - const { - label, - "allow-text": allowText, - variant: variants, - repo = await getRepositoryName(), - } = values; - - const token = await getToken(); - const host = await getHost(); - const [fields, saveModel] = await resolveModel(values, { repo, token, host }); - const [targetFields, fieldId] = resolveFieldTarget(fields, id); - - const field: Link = { - type: "Link", - config: { - label: label ?? capitalCase(fieldId), - select: "media", - allowText, - variants, - }, - }; - - if (fieldId in targetFields) throw new CommandError(`Field "${id}" already exists.`); - targetFields[fieldId] = field; - await saveModel(); - - console.info(`Field added: ${id}`); -}); diff --git a/src/commands/field-add-link.ts b/src/commands/field-add-link.ts index 9bd07eb..b28bb26 100644 --- a/src/commands/field-add-link.ts +++ b/src/commands/field-add-link.ts @@ -9,13 +9,18 @@ import { getRepositoryName } from "../project"; const config = { name: "prismic field add link", - description: "Add a link field to a slice or custom type.", + description: + "Add a link field to a slice or custom type. Use for navigational links to URLs, documents, or media. For data-level relations between documents, use content-relationship instead.", positionals: { id: { description: "Field ID", required: true }, }, options: { ...TARGET_OPTIONS, label: { type: "string", description: "Field label" }, + allow: { + type: "string", + description: "Restrict to a link type: document, media, or web", + }, "allow-target-blank": { type: "boolean", description: "Allow opening in new tab" }, "allow-text": { type: "boolean", description: "Allow custom link text" }, repeatable: { type: "boolean", description: "Allow multiple links" }, @@ -25,8 +30,11 @@ const config = { export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; + const ALLOWED_LINK_TYPES = ["document", "media", "web"] as const; + const { label, + allow, "allow-target-blank": allowTargetBlank, "allow-text": allowText, repeatable: repeat, @@ -34,6 +42,11 @@ export default createCommand(config, async ({ positionals, values }) => { repo = await getRepositoryName(), } = values; + if (allow && !ALLOWED_LINK_TYPES.includes(allow as (typeof ALLOWED_LINK_TYPES)[number])) { + throw new CommandError(`--allow must be one of: ${ALLOWED_LINK_TYPES.join(", ")}`); + } + const select = allow as (typeof ALLOWED_LINK_TYPES)[number] | undefined; + const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); @@ -43,6 +56,7 @@ export default createCommand(config, async ({ positionals, values }) => { type: "Link", config: { label: label ?? capitalCase(fieldId), + select, allowTargetBlank, allowText, repeat, diff --git a/src/commands/field-add.ts b/src/commands/field-add.ts index fef56bd..46f88b2 100644 --- a/src/commands/field-add.ts +++ b/src/commands/field-add.ts @@ -9,7 +9,6 @@ import fieldAddGroup from "./field-add-group"; import fieldAddImage from "./field-add-image"; import fieldAddIntegration from "./field-add-integration"; import fieldAddLink from "./field-add-link"; -import fieldAddLinkToMedia from "./field-add-link-to-media"; import fieldAddNumber from "./field-add-number"; import fieldAddRichText from "./field-add-rich-text"; import fieldAddSelect from "./field-add-select"; @@ -32,7 +31,7 @@ export default createCommandRouter({ }, "content-relationship": { handler: fieldAddContentRelationship, - description: "Add a content relationship field", + description: "Add a content relationship field for querying linked documents (e.g. author, category)", }, date: { handler: fieldAddDate, @@ -60,11 +59,7 @@ export default createCommandRouter({ }, link: { handler: fieldAddLink, - description: "Add a link field", - }, - "link-to-media": { - handler: fieldAddLinkToMedia, - description: "Add a link to media field", + description: "Add a link field for URLs, documents, or media (navigational)", }, number: { handler: fieldAddNumber, diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index 5540d5b..fa184a3 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -47,7 +47,7 @@ const config = { }, "allow-text": { type: "boolean", - description: "Allow custom link text (link/link-to-media)", + description: "Allow custom link text (link)", }, repeatable: { type: "boolean", description: "Allow multiple links (link)" }, variant: { @@ -147,23 +147,14 @@ export default createCommand(config, async ({ positionals, values }) => { break; } case "Link": { - if (field.config.select === "document") { - // Content relationship - if ("tag" in values) field.config.tags = values.tag; - if ("custom-type" in values) field.config.customtypes = values["custom-type"]; - } else if (field.config.select === "media") { - // Link to media - if ("allow-text" in values) field.config.allowText = values["allow-text"]; - if ("variant" in values) field.config.variants = values.variant; - } else { - // Generic link - if ("allow-target-blank" in values) { - field.config.allowTargetBlank = values["allow-target-blank"]; - } - if ("allow-text" in values) field.config.allowText = values["allow-text"]; - if ("repeatable" in values) field.config.repeat = values.repeatable; - if ("variant" in values) field.config.variants = values.variant; + if ("allow-target-blank" in values) { + field.config.allowTargetBlank = values["allow-target-blank"]; } + if ("allow-text" in values) field.config.allowText = values["allow-text"]; + if ("repeatable" in values) field.config.repeat = values.repeatable; + if ("variant" in values) field.config.variants = values.variant; + if ("tag" in values) field.config.tags = values.tag; + if ("custom-type" in values) field.config.customtypes = values["custom-type"]; break; } } diff --git a/test/field-add-link-to-media.test.ts b/test/field-add-link-to-media.test.ts deleted file mode 100644 index 0a169c4..0000000 --- a/test/field-add-link-to-media.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { buildCustomType, buildSlice, it } from "./it"; -import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; - -it("supports --help", async ({ expect, prismic }) => { - const { stdout, exitCode } = await prismic("field", ["add", "link-to-media", "--help"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("prismic field add link-to-media [options]"); -}); - -it("adds a link to media field to a slice", async ({ expect, prismic, repo, token, host }) => { - const slice = buildSlice(); - await insertSlice(slice, { repo, token, host }); - - const { stdout, exitCode } = await prismic("field", [ - "add", - "link-to-media", - "my_media", - "--to-slice", - slice.name, - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Field added: my_media"); - - const slices = await getSlices({ repo, token, host }); - const updated = slices.find((s) => s.id === slice.id); - const field = updated!.variations[0].primary!.my_media; - expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); -}); - -it("adds a link to media field to a custom type", async ({ - expect, - prismic, - repo, - token, - host, -}) => { - const customType = buildCustomType(); - await insertCustomType(customType, { repo, token, host }); - - const { stdout, exitCode } = await prismic("field", [ - "add", - "link-to-media", - "my_media", - "--to-type", - customType.label!, - ]); - expect(exitCode).toBe(0); - expect(stdout).toContain("Field added: my_media"); - - const customTypes = await getCustomTypes({ repo, token, host }); - const updated = customTypes.find((ct) => ct.id === customType.id); - const field = updated!.json.Main.my_media; - expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); -}); diff --git a/test/field-add-link.test.ts b/test/field-add-link.test.ts index 06c1bc0..02c0b04 100644 --- a/test/field-add-link.test.ts +++ b/test/field-add-link.test.ts @@ -1,5 +1,10 @@ import { buildCustomType, buildSlice, it } from "./it"; -import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; +import { + getCustomTypes, + getSlices, + insertCustomType, + insertSlice, +} from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("field", ["add", "link", "--help"]); @@ -46,3 +51,25 @@ it("adds a link field to a custom type", async ({ expect, prismic, repo, token, const field = updated!.json.Main.my_link; expect(field).toMatchObject({ type: "Link" }); }); + +it("adds a media link field with --allow media", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "link", + "my_media", + "--to-slice", + slice.name, + "--allow", + "media", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_media"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const field = updated!.variations[0].primary!.my_media; + expect(field).toMatchObject({ type: "Link", config: { select: "media" } }); +}); From e23c6f623a02ff743d6cbfdf2f6565d235e0a691 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 12:55:44 -1000 Subject: [PATCH 18/29] feat: improve `content-relationship` help text (#115) * feat: improve `content-relationship` help text Co-Authored-By: Claude Opus 4.6 (1M context) * chore: drop FIELD CONSTRAINTS section from content-relationship help Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/field-add-content-relationship.ts | 18 ++++++++++++++---- src/commands/field-add.ts | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts index c93b592..93e71b3 100644 --- a/src/commands/field-add-content-relationship.ts +++ b/src/commands/field-add-content-relationship.ts @@ -9,19 +9,29 @@ import { getRepositoryName } from "../project"; const config = { name: "prismic field add content-relationship", - description: - "Add a content relationship field to a slice or custom type. Use for querying and displaying data from related documents (e.g. an author or category). For navigational links, use link instead.", + description: ` + Add a content relationship field to a slice or custom type. + + Content relationships fetch and display data from related documents + (e.g. an author's name, a category's label). They are not navigational + links -- use the link field type for URLs, documents, or media that + the user clicks to visit. + + Use --custom-type and --tag to restrict which documents can be + selected. These filters define exactly which documents are queryable + through this field. If neither is specified, all documents are allowed. + `, positionals: { id: { description: "Field ID", required: true }, }, options: { ...TARGET_OPTIONS, label: { type: "string", description: "Field label" }, - tag: { type: "string", multiple: true, description: "Allowed tag (can be repeated)" }, + tag: { type: "string", multiple: true, description: "Restrict to documents with this tag (can be repeated)" }, "custom-type": { type: "string", multiple: true, - description: "Allowed custom type (can be repeated)", + description: "Restrict to documents of this type (can be repeated)", }, }, } satisfies CommandConfig; diff --git a/src/commands/field-add.ts b/src/commands/field-add.ts index 46f88b2..eb5bc43 100644 --- a/src/commands/field-add.ts +++ b/src/commands/field-add.ts @@ -31,7 +31,7 @@ export default createCommandRouter({ }, "content-relationship": { handler: fieldAddContentRelationship, - description: "Add a content relationship field for querying linked documents (e.g. author, category)", + description: "Add a content relationship field for fetching data from related documents (not for navigation -- use link)", }, date: { handler: fieldAddDate, From 90227ab5f7f2bb190e20efb7d6862ca51cf56a89 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 13:28:40 -1000 Subject: [PATCH 19/29] feat: add a consistent table formatter for tabular output (#116) * feat: add a consistent table formatter for tabular output Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add header support to formatTable and add headers to list commands Co-Authored-By: Claude Opus 4.6 (1M context) * fix: avoid blank line in `preview list` when only simulator URL exists Co-Authored-By: Claude Opus 4.6 (1M context) * fix: rename preview list header from LABEL to NAME Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/docs-list.ts | 12 +++++------- src/commands/locale-list.ts | 8 +++++--- src/commands/preview-list.ts | 11 ++++++++--- src/commands/repo-list.ts | 9 +++++---- src/commands/slice-list.ts | 6 +++--- src/commands/slice-view.ts | 8 +++++--- src/commands/token-list.ts | 15 ++++++++------- src/commands/type-list.ts | 8 +++++--- src/commands/type-view.ts | 8 +++++--- src/commands/webhook-list.ts | 8 +++++--- src/lib/command.ts | 27 ++++++++------------------- src/lib/string.ts | 23 +++++++++++++++++++++++ test/slice-list.test.ts | 2 +- test/slice-view.test.ts | 7 +++---- test/type-list.test.ts | 4 ++-- test/type-view.test.ts | 7 +++---- 16 files changed, 94 insertions(+), 69 deletions(-) diff --git a/src/commands/docs-list.ts b/src/commands/docs-list.ts index d968ffa..9e3b54b 100644 --- a/src/commands/docs-list.ts +++ b/src/commands/docs-list.ts @@ -2,6 +2,7 @@ import { getDocsIndex, getDocsPageIndex } from "../clients/docs"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { NotFoundRequestError, UnknownRequestError } from "../lib/request"; +import { formatTable } from "../lib/string"; const config = { name: "prismic docs list", @@ -52,9 +53,8 @@ export default createCommand(config, async ({ positionals, values }) => { return; } - for (const anchor of entry.anchors) { - console.info(`${path}#${anchor.slug}: ${anchor.excerpt}`); - } + const rows = entry.anchors.map((anchor) => [`${path}#${anchor.slug}`, anchor.excerpt]); + console.info(formatTable(rows, { headers: ["PATH", "EXCERPT"] })); } else { let pages; try { @@ -79,9 +79,7 @@ export default createCommand(config, async ({ positionals, values }) => { return; } - for (const page of pages) { - const description = page.description ? ` — ${page.description}` : ""; - console.info(`${page.path}: ${page.title}${description}`); - } + const rows = pages.map((page) => [page.path, page.title, page.description ?? ""]); + console.info(formatTable(rows, { headers: ["PATH", "TITLE", "DESCRIPTION"] })); } }); diff --git a/src/commands/locale-list.ts b/src/commands/locale-list.ts index 64b63c8..c235cef 100644 --- a/src/commands/locale-list.ts +++ b/src/commands/locale-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getLocales } from "../clients/locale"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -36,8 +37,9 @@ export default createCommand(config, async ({ values }) => { return; } - for (const locale of locales) { + const rows = locales.map((locale) => { const masterLabel = locale.isMaster ? " (master)" : ""; - console.info(`${locale.id} ${locale.label}${masterLabel}`); - } + return [locale.id, `${locale.label}${masterLabel}`]; + }); + console.info(formatTable(rows, { headers: ["ID", "LABEL"] })); }); diff --git a/src/commands/preview-list.ts b/src/commands/preview-list.ts index 7744489..bc6da3f 100644 --- a/src/commands/preview-list.ts +++ b/src/commands/preview-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getPreviews, getSimulatorUrl } from "../clients/core"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -44,11 +45,15 @@ export default createCommand(config, async ({ values }) => { return; } - for (const preview of previews) { - console.info(`${preview.url} ${preview.label}`); + if (previews.length > 0) { + const rows = previews.map((preview) => [preview.url, preview.label]); + console.info(formatTable(rows, { headers: ["URL", "NAME"] })); } if (simulatorUrl) { - console.info(`\nSimulator: ${simulatorUrl}`); + if (previews.length > 0) { + console.info(""); + } + console.info(`Simulator: ${simulatorUrl}`); } }); diff --git a/src/commands/repo-list.ts b/src/commands/repo-list.ts index db67818..cd402f5 100644 --- a/src/commands/repo-list.ts +++ b/src/commands/repo-list.ts @@ -3,6 +3,7 @@ import { getProfile } from "../clients/user"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { UnknownRequestError } from "../lib/request"; +import { formatTable } from "../lib/string"; const config = { name: "prismic repo list", @@ -50,9 +51,9 @@ export default createCommand(config, async ({ values }) => { return; } - for (const repo of repos) { + const rows = repos.map((repo) => { const name = repo.name || "(no name)"; - const role = repo.role ? ` ${repo.role}` : ""; - console.info(`${repo.domain} ${name}${role}`); - } + return [repo.domain, name, repo.role ?? ""]; + }); + console.info(formatTable(rows, { headers: ["DOMAIN", "NAME", "ROLE"] })); }); diff --git a/src/commands/slice-list.ts b/src/commands/slice-list.ts index 55f4736..17e75d6 100644 --- a/src/commands/slice-list.ts +++ b/src/commands/slice-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getSlices } from "../clients/custom-types"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -30,7 +31,6 @@ export default createCommand(config, async ({ values }) => { return; } - for (const slice of slices) { - console.info(`${slice.name} (id: ${slice.id})`); - } + const rows = slices.map((slice) => [slice.name, slice.id]); + console.info(formatTable(rows, { headers: ["NAME", "ID"] })); }); diff --git a/src/commands/slice-view.ts b/src/commands/slice-view.ts index 5063e1b..7b7ff4f 100644 --- a/src/commands/slice-view.ts +++ b/src/commands/slice-view.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getSlices } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -44,12 +45,13 @@ export default createCommand(config, async ({ positionals, values }) => { if (entries.length === 0) { console.info(" (no fields)"); } else { - for (const [id, field] of entries) { + const rows = entries.map(([id, field]) => { const config = field.config as Record | undefined; const label = (config?.label as string) || ""; const placeholder = config?.placeholder ? `"${config.placeholder}"` : ""; - console.info(` ${[id, field.type, label, placeholder].filter(Boolean).join(" ")}`); - } + return [` ${id}`, field.type, label, placeholder]; + }); + console.info(formatTable(rows)); } } }); diff --git a/src/commands/token-list.ts b/src/commands/token-list.ts index 102dd11..41386c2 100644 --- a/src/commands/token-list.ts +++ b/src/commands/token-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getOAuthApps, getWriteTokens } from "../clients/wroom"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -46,9 +47,8 @@ export default createCommand(config, async ({ values }) => { if (accessTokens.length > 0) { console.info("ACCESS TOKENS"); - for (const accessToken of accessTokens) { - console.info(` ${accessToken.name} ${accessToken.scope} ${accessToken.token} ${accessToken.createdAt}`); - } + const rows = accessTokens.map((t) => [` ${t.name}`, t.scope, t.token, t.createdAt]); + console.info(formatTable(rows)); } else { console.info("ACCESS TOKENS (none)"); } @@ -57,10 +57,11 @@ export default createCommand(config, async ({ values }) => { if (writeTokens.length > 0) { console.info("WRITE TOKENS"); - for (const writeToken of writeTokens) { - const date = new Date(writeToken.timestamp * 1000).toISOString().split("T")[0]; - console.info(` ${writeToken.app_name} ${writeToken.token} ${date}`); - } + const rows = writeTokens.map((t) => { + const date = new Date(t.timestamp * 1000).toISOString().split("T")[0]; + return [` ${t.app_name}`, t.token, date]; + }); + console.info(formatTable(rows)); } else { console.info("WRITE TOKENS (none)"); } diff --git a/src/commands/type-list.ts b/src/commands/type-list.ts index 59b5137..a64bf8d 100644 --- a/src/commands/type-list.ts +++ b/src/commands/type-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getCustomTypes } from "../clients/custom-types"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -31,8 +32,9 @@ export default createCommand(config, async ({ values }) => { return; } - for (const type of types) { + const rows = types.map((type) => { const label = type.label || "(no name)"; - console.info(`${label} (id: ${type.id}, format: ${type.format})`); - } + return [label, type.id, type.format ?? ""]; + }); + console.info(formatTable(rows, { headers: ["NAME", "ID", "FORMAT"] })); }); diff --git a/src/commands/type-view.ts b/src/commands/type-view.ts index a8a5183..fbdc1ef 100644 --- a/src/commands/type-view.ts +++ b/src/commands/type-view.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getCustomTypes } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -46,12 +47,13 @@ export default createCommand(config, async ({ positionals, values }) => { if (entries.length === 0) { console.info(" (no fields)"); } else { - for (const [id, field] of entries) { + const rows = entries.map(([id, field]) => { const config = field.config as Record | undefined; const label = (config?.label as string) || ""; const placeholder = config?.placeholder ? `"${config.placeholder}"` : ""; - console.info(` ${[id, field.type, label, placeholder].filter(Boolean).join(" ")}`); - } + return [` ${id}`, field.type, label, placeholder]; + }); + console.info(formatTable(rows)); } } }); diff --git a/src/commands/webhook-list.ts b/src/commands/webhook-list.ts index 7f6266e..2557d7a 100644 --- a/src/commands/webhook-list.ts +++ b/src/commands/webhook-list.ts @@ -2,6 +2,7 @@ import { getHost, getToken } from "../auth"; import { getWebhooks } from "../clients/wroom"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; +import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; const config = { @@ -35,9 +36,10 @@ export default createCommand(config, async ({ values }) => { return; } - for (const webhook of webhooks) { + const rows = webhooks.map((webhook) => { const status = webhook.config.active ? "enabled" : "disabled"; const name = webhook.config.name ? ` (${webhook.config.name})` : ""; - console.info(`${webhook.config.url}${name} [${status}]`); - } + return [`${webhook.config.url}${name}`, `[${status}]`]; + }); + console.info(formatTable(rows, { headers: ["URL", "STATUS"] })); }); diff --git a/src/lib/command.ts b/src/lib/command.ts index 6c9de2c..666c494 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -2,7 +2,7 @@ import type { ParseArgsOptionDescriptor } from "node:util"; import { parseArgs } from "node:util"; -import { dedent } from "./string"; +import { dedent, formatTable } from "./string"; export type CommandConfig = { name: string; @@ -89,16 +89,13 @@ function buildCommandHelp(config: CommandConfig): string { if (positionalNames.length > 0) { lines.push(""); lines.push("ARGUMENTS"); - const maxNameLength = Math.max( - ...positionalNames.map((positionalName) => `<${positionalName}>`.length), - ); + const rows: string[][] = []; for (const positionalName in positionals) { - const formattedName = `<${positionalName}>`; - const paddedName = formattedName.padEnd(maxNameLength); const positional = positionals[positionalName]; const description = positional.description + (positional.required ? " (required)" : ""); - lines.push(` ${paddedName} ${description}`); + rows.push([` <${positionalName}>`, description]); } + lines.push(formatTable(rows)); } lines.push(""); @@ -116,11 +113,8 @@ function buildCommandHelp(config: CommandConfig): string { } } optionEntries.push({ left: "-h, --help", description: "Show help for command" }); - const maxOptionLength = Math.max(...optionEntries.map((optionEntry) => optionEntry.left.length)); - for (const optionEntry of optionEntries) { - const paddedLeft = optionEntry.left.padEnd(maxOptionLength); - lines.push(` ${paddedLeft} ${optionEntry.description}`); - } + const optionRows = optionEntries.map((entry) => [` ${entry.left}`, entry.description]); + lines.push(formatTable(optionRows)); if (sections) { for (const sectionName in sections) { @@ -190,13 +184,8 @@ function buildRouterHelp(config: CreateCommandRouterConfig): string { lines.push(""); lines.push("COMMANDS"); - const commandNames = Object.keys(commands); - const maxNameLength = Math.max(...commandNames.map((commandName) => commandName.length)); - for (const commandName of commandNames) { - const paddedName = commandName.padEnd(maxNameLength); - const description = commands[commandName].description; - lines.push(` ${paddedName} ${description}`); - } + const commandRows = Object.entries(commands).map(([name, cmd]) => [` ${name}`, cmd.description]); + lines.push(formatTable(commandRows)); lines.push(""); lines.push("OPTIONS"); diff --git a/src/lib/string.ts b/src/lib/string.ts index d030e24..bd8890d 100644 --- a/src/lib/string.ts +++ b/src/lib/string.ts @@ -1,3 +1,26 @@ import baseDedent from "dedent"; export const dedent = baseDedent.withOptions({ alignValues: true }); + +export function formatTable( + rows: string[][], + config?: { headers?: string[]; separator?: string }, +): string { + const separator = config?.separator ?? " "; + const allRows = config?.headers ? [config.headers, ...rows] : rows; + const columnWidths: number[] = []; + for (const row of allRows) { + for (let i = 0; i < row.length; i++) { + columnWidths[i] = Math.max(columnWidths[i] ?? 0, row[i].length); + } + } + return allRows + .map((row) => { + const line = row + .map((cell, i) => (i < row.length - 1 ? cell.padEnd(columnWidths[i]) : cell)) + .join(separator) + .trimEnd(); + return line; + }) + .join("\n"); +} diff --git a/test/slice-list.test.ts b/test/slice-list.test.ts index b06dda2..ba5899b 100644 --- a/test/slice-list.test.ts +++ b/test/slice-list.test.ts @@ -13,7 +13,7 @@ it("lists slices", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("slice", ["list"]); expect(exitCode).toBe(0); - expect(stdout).toContain(`${slice.name} (id: ${slice.id})`); + expect(stdout).toMatch(new RegExp(`${slice.name}\\s+${slice.id}`)); }); it("lists slices as JSON", async ({ expect, prismic, repo, token, host }) => { diff --git a/test/slice-view.test.ts b/test/slice-view.test.ts index c159e0c..fd27e26 100644 --- a/test/slice-view.test.ts +++ b/test/slice-view.test.ts @@ -51,11 +51,10 @@ it("shows fields per variation", async ({ expect, prismic, repo, token, host }) const { stdout, exitCode } = await prismic("slice", ["view", slice.name]); expect(exitCode).toBe(0); expect(stdout).toContain("default:"); - expect(stdout).toContain("title StructuredText Title"); - expect(stdout).toContain('"Enter title"'); - expect(stdout).toContain("is_active Boolean Is Active"); + expect(stdout).toMatch(/title\s+StructuredText\s+Title\s+"Enter title"/); + expect(stdout).toMatch(/is_active\s+Boolean\s+Is Active/); expect(stdout).toContain("withImage:"); - expect(stdout).toContain("image Image Image"); + expect(stdout).toMatch(/image\s+Image\s+Image/); }); it("views a slice as JSON", async ({ expect, prismic, repo, token, host }) => { diff --git a/test/type-list.test.ts b/test/type-list.test.ts index 543300c..f6271dc 100644 --- a/test/type-list.test.ts +++ b/test/type-list.test.ts @@ -15,8 +15,8 @@ it("lists all types", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("type", ["list"]); expect(exitCode).toBe(0); - expect(stdout).toContain(`${customType.label} (id: ${customType.id}, format: custom)`); - expect(stdout).toContain(`${pageType.label} (id: ${pageType.id}, format: page)`); + expect(stdout).toMatch(new RegExp(`${customType.label}\\s+${customType.id}\\s+custom`)); + expect(stdout).toMatch(new RegExp(`${pageType.label}\\s+${pageType.id}\\s+page`)); }); it("lists types as JSON", async ({ expect, prismic, repo, token, host }) => { diff --git a/test/type-view.test.ts b/test/type-view.test.ts index f0a843b..60ba122 100644 --- a/test/type-view.test.ts +++ b/test/type-view.test.ts @@ -37,11 +37,10 @@ it("shows fields per tab", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("type", ["view", customType.label!]); expect(exitCode).toBe(0); expect(stdout).toContain("Main:"); - expect(stdout).toContain("title StructuredText Title"); - expect(stdout).toContain('"Enter title"'); - expect(stdout).toContain("is_active Boolean Is Active"); + expect(stdout).toMatch(/title\s+StructuredText\s+Title\s+"Enter title"/); + expect(stdout).toMatch(/is_active\s+Boolean\s+Is Active/); expect(stdout).toContain("SEO:"); - expect(stdout).toContain("meta_title Text Meta Title"); + expect(stdout).toMatch(/meta_title\s+Text\s+Meta Title/); }); it("views a type as JSON", async ({ expect, prismic, repo, token, host }) => { From 774a58113753426ea1f5fd6a62fa8315be7839c1 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 10 Apr 2026 13:44:31 -1000 Subject: [PATCH 20/29] feat: replace name-based model specifiers with IDs (#117) * feat: replace name-based model specifiers with IDs Slice commands now resolve by `id` instead of `name`, content type commands resolve by `id` instead of `label`, and variation commands resolve by `id` instead of `name`. Closes #105 Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: rename `currentId` to `id` in slice-edit-variation The "current" prefix was a holdover from name-based specifiers where names could change. IDs are immutable, so the prefix is unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/slice-add-variation.ts | 4 +-- src/commands/slice-connect.ts | 14 +++++------ src/commands/slice-disconnect.ts | 14 +++++------ src/commands/slice-edit-variation.ts | 18 ++++++------- src/commands/slice-edit.ts | 8 +++--- src/commands/slice-remove-variation.ts | 14 +++++------ src/commands/slice-remove.ts | 10 ++++---- src/commands/slice-view.ts | 8 +++--- src/commands/type-add-tab.ts | 4 +-- src/commands/type-edit-tab.ts | 14 +++++------ src/commands/type-edit.ts | 8 +++--- src/commands/type-remove-tab.ts | 4 +-- src/commands/type-remove.ts | 10 ++++---- src/commands/type-view.ts | 8 +++--- src/models.ts | 28 ++++++++++----------- test/field-add-boolean.test.ts | 4 +-- test/field-add-color.test.ts | 4 +-- test/field-add-content-relationship.test.ts | 4 +-- test/field-add-date.test.ts | 4 +-- test/field-add-embed.test.ts | 4 +-- test/field-add-geopoint.test.ts | 4 +-- test/field-add-group.test.ts | 10 ++++---- test/field-add-image.test.ts | 4 +-- test/field-add-integration.test.ts | 4 +-- test/field-add-link.test.ts | 6 ++--- test/field-add-number.test.ts | 4 +-- test/field-add-rich-text.test.ts | 4 +-- test/field-add-select.test.ts | 4 +-- test/field-add-table.test.ts | 4 +-- test/field-add-text.test.ts | 6 ++--- test/field-add-timestamp.test.ts | 4 +-- test/field-add-uid.test.ts | 4 +-- test/field-edit.test.ts | 12 ++++----- test/field-remove.test.ts | 6 ++--- test/field-view.test.ts | 8 +++--- test/slice-add-variation.test.ts | 4 +-- test/slice-connect.test.ts | 8 +++--- test/slice-disconnect.test.ts | 8 +++--- test/slice-edit-variation.test.ts | 8 +++--- test/slice-edit.test.ts | 4 +-- test/slice-remove-variation.test.ts | 8 +++--- test/slice-remove.test.ts | 6 ++--- test/slice-view.test.ts | 8 +++--- test/type-add-tab.test.ts | 8 +++--- test/type-edit-tab.test.ts | 12 ++++----- test/type-edit.test.ts | 8 +++--- test/type-remove-tab.test.ts | 4 +-- test/type-remove.test.ts | 6 ++--- test/type-view.test.ts | 8 +++--- 49 files changed, 185 insertions(+), 185 deletions(-) diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index c1daaf9..dca5572 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -16,7 +16,7 @@ const config = { name: { description: "Name of the variation", required: true }, }, options: { - to: { type: "string", required: true, description: "Name of the slice" }, + to: { type: "string", required: true, description: "ID of the slice" }, id: { type: "string", description: "Custom ID for the variation" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, @@ -30,7 +30,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.name === to); + const slice = slices.find((s) => s.id === to); if (!slice) { throw new CommandError(`Slice not found: ${to}`); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index 9f27176..7450d26 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -11,13 +11,13 @@ const config = { name: "prismic slice connect", description: "Connect a slice to a type's slice zone.", positionals: { - name: { description: "Name of the slice", required: true }, + id: { description: "ID of the slice", required: true }, }, options: { to: { type: "string", required: true, - description: "Name of the content type", + description: "ID of the content type", }, "slice-zone": { type: "string", @@ -28,7 +28,7 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { to, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; const adapter = await getAdapter(); @@ -37,13 +37,13 @@ export default createCommand(config, async ({ positionals, values }) => { const apiConfig = { repo, token, host }; const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.name === name); + const slice = slices.find((s) => s.id === id); if (!slice) { - throw new CommandError(`Slice not found: ${name}`); + throw new CommandError(`Slice not found: ${id}`); } const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.label === to); + const customType = customTypes.find((ct) => ct.id === to); if (!customType) { throw new CommandError(`Type not found: ${to}`); } @@ -86,5 +86,5 @@ export default createCommand(config, async ({ positionals, values }) => { } await adapter.generateTypes(); - console.info(`Connected slice "${name}" to "${to}"`); + console.info(`Connected slice "${id}" to "${to}"`); }); diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index 5bde826..f9cbf01 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -11,13 +11,13 @@ const config = { name: "prismic slice disconnect", description: "Disconnect a slice from a type's slice zone.", positionals: { - name: { description: "Name of the slice", required: true }, + id: { description: "ID of the slice", required: true }, }, options: { from: { type: "string", required: true, - description: "Name of the content type", + description: "ID of the content type", }, "slice-zone": { type: "string", @@ -28,7 +28,7 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { from, "slice-zone": sliceZone = "slices", repo = await getRepositoryName() } = values; const adapter = await getAdapter(); @@ -37,13 +37,13 @@ export default createCommand(config, async ({ positionals, values }) => { const apiConfig = { repo, token, host }; const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.name === name); + const slice = slices.find((s) => s.id === id); if (!slice) { - throw new CommandError(`Slice not found: ${name}`); + throw new CommandError(`Slice not found: ${id}`); } const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.label === from); + const customType = customTypes.find((ct) => ct.id === from); if (!customType) { throw new CommandError(`Type not found: ${from}`); } @@ -83,5 +83,5 @@ export default createCommand(config, async ({ positionals, values }) => { } await adapter.generateTypes(); - console.info(`Disconnected slice "${name}" from "${from}"`); + console.info(`Disconnected slice "${id}" from "${from}"`); }); diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index b9f228a..aafbbf2 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -11,33 +11,33 @@ const config = { name: "prismic slice edit-variation", description: "Edit a variation of a slice.", positionals: { - name: { description: "Name of the variation", required: true }, + id: { description: "ID of the variation", required: true }, }, options: { - "from-slice": { type: "string", required: true, description: "Name of the slice" }, + "from-slice": { type: "string", required: true, description: "ID of the slice" }, name: { type: "string", short: "n", description: "New name for the variation" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [currentName] = positionals; - const { "from-slice": sliceName, repo = await getRepositoryName() } = values; + const [id] = positionals; + const { "from-slice": sliceId, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.name === sliceName); + const slice = slices.find((s) => s.id === sliceId); if (!slice) { - throw new CommandError(`Slice not found: ${sliceName}`); + throw new CommandError(`Slice not found: ${sliceId}`); } - const variation = slice.variations.find((v) => v.name === currentName); + const variation = slice.variations.find((v) => v.id === id); if (!variation) { - throw new CommandError(`Variation "${currentName}" not found in slice "${sliceName}".`); + throw new CommandError(`Variation "${id}" not found in slice "${sliceId}".`); } if ("name" in values) variation.name = values.name!; @@ -61,5 +61,5 @@ export default createCommand(config, async ({ positionals, values }) => { } await adapter.generateTypes(); - console.info(`Variation updated: "${variation.name}" in slice "${sliceName}"`); + console.info(`Variation updated: "${id}" in slice "${sliceId}"`); }); diff --git a/src/commands/slice-edit.ts b/src/commands/slice-edit.ts index f1b5c9a..003e263 100644 --- a/src/commands/slice-edit.ts +++ b/src/commands/slice-edit.ts @@ -11,7 +11,7 @@ const config = { name: "prismic slice edit", description: "Edit a slice.", positionals: { - name: { description: "Name of the slice", required: true }, + id: { description: "ID of the slice", required: true }, }, options: { name: { type: "string", short: "n", description: "New name for the slice" }, @@ -20,17 +20,17 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [currentName] = positionals; + const [id] = positionals; const { repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.name === currentName); + const slice = slices.find((s) => s.id === id); if (!slice) { - throw new CommandError(`Slice not found: ${currentName}`); + throw new CommandError(`Slice not found: ${id}`); } const updatedSlice: SharedSlice = { ...slice }; diff --git a/src/commands/slice-remove-variation.ts b/src/commands/slice-remove-variation.ts index 8b9a913..53e64c0 100644 --- a/src/commands/slice-remove-variation.ts +++ b/src/commands/slice-remove-variation.ts @@ -11,32 +11,32 @@ const config = { name: "prismic slice remove-variation", description: "Remove a variation from a slice.", positionals: { - name: { description: "Name of the variation", required: true }, + id: { description: "ID of the variation", required: true }, }, options: { - from: { type: "string", required: true, description: "Name of the slice" }, + from: { type: "string", required: true, description: "ID of the slice" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { from, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.name === from); + const slice = slices.find((s) => s.id === from); if (!slice) { throw new CommandError(`Slice not found: ${from}`); } - const variation = slice.variations.find((v) => v.name === name); + const variation = slice.variations.find((v) => v.id === id); if (!variation) { - throw new CommandError(`Variation "${name}" not found in slice "${from}".`); + throw new CommandError(`Variation "${id}" not found in slice "${from}".`); } const updatedSlice: SharedSlice = { @@ -61,5 +61,5 @@ export default createCommand(config, async ({ positionals, values }) => { } await adapter.generateTypes(); - console.info(`Removed variation "${name}" from slice "${from}"`); + console.info(`Removed variation "${id}" from slice "${from}"`); }); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts index 1ae76f7..4831e25 100644 --- a/src/commands/slice-remove.ts +++ b/src/commands/slice-remove.ts @@ -9,7 +9,7 @@ const config = { name: "prismic slice remove", description: "Remove a slice.", positionals: { - name: { description: "Name of the slice", required: true }, + id: { description: "ID of the slice", required: true }, }, options: { repo: { type: "string", short: "r", description: "Repository domain" }, @@ -17,17 +17,17 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.name === name); + const slice = slices.find((s) => s.id === id); if (!slice) { - throw new CommandError(`Slice not found: ${name}`); + throw new CommandError(`Slice not found: ${id}`); } try { @@ -45,5 +45,5 @@ export default createCommand(config, async ({ positionals, values }) => { } catch {} await adapter.generateTypes(); - console.info(`Slice removed: "${name}" (id: ${slice.id})`); + console.info(`Slice removed: ${id}`); }); diff --git a/src/commands/slice-view.ts b/src/commands/slice-view.ts index 7b7ff4f..ec53508 100644 --- a/src/commands/slice-view.ts +++ b/src/commands/slice-view.ts @@ -9,7 +9,7 @@ const config = { name: "prismic slice view", description: "View details of a slice.", positionals: { - name: { description: "Name of the slice", required: true }, + id: { description: "ID of the slice", required: true }, }, options: { json: { type: "boolean", description: "Output as JSON" }, @@ -18,16 +18,16 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { json, repo = await getRepositoryName() } = values; const token = await getToken(); const host = await getHost(); const slices = await getSlices({ repo, token, host }); - const slice = slices.find((slice) => slice.name === name); + const slice = slices.find((slice) => slice.id === id); if (!slice) { - throw new CommandError(`Slice not found: ${name}`); + throw new CommandError(`Slice not found: ${id}`); } if (json) { diff --git a/src/commands/type-add-tab.ts b/src/commands/type-add-tab.ts index 7744172..4cf02f4 100644 --- a/src/commands/type-add-tab.ts +++ b/src/commands/type-add-tab.ts @@ -12,7 +12,7 @@ const config = { name: { description: "Name of the tab", required: true }, }, options: { - to: { type: "string", required: true, description: "Name of the content type" }, + to: { type: "string", required: true, description: "ID of the content type" }, "with-slice-zone": { type: "boolean", description: "Add a slice zone to the tab" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, @@ -26,7 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === to); + const type = customTypes.find((ct) => ct.id === to); if (!type) { throw new CommandError(`Type not found: ${to}`); diff --git a/src/commands/type-edit-tab.ts b/src/commands/type-edit-tab.ts index 103add8..b8a1fc1 100644 --- a/src/commands/type-edit-tab.ts +++ b/src/commands/type-edit-tab.ts @@ -14,7 +14,7 @@ const config = { name: { description: "Current name of the tab", required: true }, }, options: { - "from-type": { type: "string", required: true, description: "Name of the content type" }, + "from-type": { type: "string", required: true, description: "ID of the content type" }, name: { type: "string", short: "n", description: "New name for the tab" }, "with-slice-zone": { type: "boolean", description: "Add a slice zone to the tab" }, "without-slice-zone": { type: "boolean", description: "Remove the slice zone from the tab" }, @@ -24,20 +24,20 @@ const config = { export default createCommand(config, async ({ positionals, values }) => { const [currentName] = positionals; - const { "from-type": typeName, repo = await getRepositoryName() } = values; + const { "from-type": typeId, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === typeName); + const type = customTypes.find((ct) => ct.id === typeId); if (!type) { - throw new CommandError(`Type not found: ${typeName}`); + throw new CommandError(`Type not found: ${typeId}`); } if (!(currentName in type.json)) { - throw new CommandError(`Tab "${currentName}" not found in "${typeName}".`); + throw new CommandError(`Tab "${currentName}" not found in "${typeId}".`); } if ("with-slice-zone" in values && "without-slice-zone" in values) { @@ -82,7 +82,7 @@ export default createCommand(config, async ({ positionals, values }) => { if ("name" in values) { if (values.name! in type.json) { - throw new CommandError(`Tab "${values.name}" already exists in "${typeName}".`); + throw new CommandError(`Tab "${values.name}" already exists in "${typeId}".`); } const newJson: CustomType["json"] = {}; @@ -109,5 +109,5 @@ export default createCommand(config, async ({ positionals, values }) => { } await adapter.generateTypes(); - console.info(`Tab updated: "${currentName}" in "${typeName}"`); + console.info(`Tab updated: "${currentName}" in "${typeId}"`); }); diff --git a/src/commands/type-edit.ts b/src/commands/type-edit.ts index bffabdc..e6817cc 100644 --- a/src/commands/type-edit.ts +++ b/src/commands/type-edit.ts @@ -9,7 +9,7 @@ const config = { name: "prismic type edit", description: "Edit a content type.", positionals: { - name: { description: "Name of the content type", required: true }, + id: { description: "ID of the content type", required: true }, }, options: { name: { type: "string", short: "n", description: "New name for the type" }, @@ -19,7 +19,7 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [currentName] = positionals; + const [id] = positionals; const { repo = await getRepositoryName() } = values; if ("format" in values && values.format !== "custom" && values.format !== "page") { @@ -30,10 +30,10 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === currentName); + const type = customTypes.find((ct) => ct.id === id); if (!type) { - throw new CommandError(`Type not found: ${currentName}`); + throw new CommandError(`Type not found: ${id}`); } if ("name" in values) type.label = values.name; diff --git a/src/commands/type-remove-tab.ts b/src/commands/type-remove-tab.ts index e0a8c3e..53bd1a8 100644 --- a/src/commands/type-remove-tab.ts +++ b/src/commands/type-remove-tab.ts @@ -12,7 +12,7 @@ const config = { name: { description: "Name of the tab", required: true }, }, options: { - from: { type: "string", required: true, description: "Name of the content type" }, + from: { type: "string", required: true, description: "ID of the content type" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; @@ -25,7 +25,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === from); + const type = customTypes.find((ct) => ct.id === from); if (!type) { throw new CommandError(`Type not found: ${from}`); diff --git a/src/commands/type-remove.ts b/src/commands/type-remove.ts index ec4e54c..810c5dd 100644 --- a/src/commands/type-remove.ts +++ b/src/commands/type-remove.ts @@ -9,7 +9,7 @@ const config = { name: "prismic type remove", description: "Remove a content type.", positionals: { - name: { description: "Name of the content type", required: true }, + id: { description: "ID of the content type", required: true }, }, options: { repo: { type: "string", short: "r", description: "Repository domain" }, @@ -17,17 +17,17 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === name); + const type = customTypes.find((ct) => ct.id === id); if (!type) { - throw new CommandError(`Type not found: ${name}`); + throw new CommandError(`Type not found: ${id}`); } try { @@ -45,5 +45,5 @@ export default createCommand(config, async ({ positionals, values }) => { } catch {} await adapter.generateTypes(); - console.info(`Type removed: "${name}" (id: ${type.id})`); + console.info(`Type removed: ${id}`); }); diff --git a/src/commands/type-view.ts b/src/commands/type-view.ts index fbdc1ef..2772849 100644 --- a/src/commands/type-view.ts +++ b/src/commands/type-view.ts @@ -9,7 +9,7 @@ const config = { name: "prismic type view", description: "View details of a content type.", positionals: { - name: { description: "Name of the content type", required: true }, + id: { description: "ID of the content type", required: true }, }, options: { json: { type: "boolean", description: "Output as JSON" }, @@ -18,16 +18,16 @@ const config = { } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { - const [name] = positionals; + const [id] = positionals; const { json, repo = await getRepositoryName() } = values; const token = await getToken(); const host = await getHost(); const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.label === name); + const type = customTypes.find((ct) => ct.id === id); if (!type) { - throw new CommandError(`Type not found: ${name}`); + throw new CommandError(`Type not found: ${id}`); } if (json) { diff --git a/src/models.ts b/src/models.ts index 48a9127..3b6e763 100644 --- a/src/models.ts +++ b/src/models.ts @@ -14,16 +14,16 @@ type ApiConfig = { repo: string; token: string | undefined; host: string }; type Target = [fields: Fields, save: () => Promise, modelKind: ModelKind]; export const TARGET_OPTIONS = { - "to-slice": { type: "string", description: "Name of the target slice" }, - "to-type": { type: "string", description: "Name of the target content type" }, + "to-slice": { type: "string", description: "ID of the target slice" }, + "to-type": { type: "string", description: "ID of the target content type" }, variation: { type: "string", description: 'Slice variation ID (default: "default")' }, tab: { type: "string", description: 'Content type tab name (default: "Main")' }, repo: { type: "string", short: "r", description: "Repository domain" }, } satisfies CommandConfig["options"]; export const SOURCE_OPTIONS = { - "from-slice": { type: "string", description: "Name of the source slice" }, - "from-type": { type: "string", description: "Name of the source content type" }, + "from-slice": { type: "string", description: "ID of the source slice" }, + "from-type": { type: "string", description: "ID of the source content type" }, variation: TARGET_OPTIONS.variation, tab: TARGET_OPTIONS.tab, repo: TARGET_OPTIONS.repo, @@ -55,7 +55,7 @@ export async function resolveFieldContainer( if (fromSlice) { const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.name === fromSlice); + const slice = slices.find((s) => s.id === fromSlice); if (!slice) { throw new CommandError(`Slice not found: ${fromSlice}`); } @@ -82,7 +82,7 @@ export async function resolveFieldContainer( } const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.label === fromType); + const customType = customTypes.find((ct) => ct.id === fromType); if (!customType) { throw new CommandError(`Type not found: ${fromType}`); } @@ -122,10 +122,10 @@ export async function resolveModel( apiConfig: ApiConfig, ): Promise { const adapter = await getAdapter(); - const sliceName = values["to-slice"] ?? values["from-slice"]; - const typeName = values["to-type"] ?? values["from-type"]; + const sliceId = values["to-slice"] ?? values["from-slice"]; + const typeId = values["to-type"] ?? values["from-type"]; - const providedCount = [sliceName, typeName].filter(Boolean).length; + const providedCount = [sliceId, typeId].filter(Boolean).length; if (providedCount === 0) { throw new CommandError("Specify a target with --to-slice or --to-type."); } @@ -133,16 +133,16 @@ export async function resolveModel( throw new CommandError("Only one of --to-slice or --to-type can be specified."); } - if (sliceName) { + if (sliceId) { if ("tab" in values) { throw new CommandError("--tab is only valid for content types."); } const variation = values.variation ?? "default"; const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.name === sliceName); + const slice = slices.find((s) => s.id === sliceId); if (!slice) { - throw new CommandError(`Slice not found: ${sliceName}`); + throw new CommandError(`Slice not found: ${sliceId}`); } const newModel = structuredClone(slice); @@ -182,9 +182,9 @@ export async function resolveModel( const tab = values.tab ?? "Main"; const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.label === typeName); + const customType = customTypes.find((ct) => ct.id === typeId); if (!customType) { - throw new CommandError(`Type not found: ${typeName}`); + throw new CommandError(`Type not found: ${typeId}`); } const newModel = structuredClone(customType); diff --git a/test/field-add-boolean.test.ts b/test/field-add-boolean.test.ts index ebbef70..6e082fc 100644 --- a/test/field-add-boolean.test.ts +++ b/test/field-add-boolean.test.ts @@ -16,7 +16,7 @@ it("adds a boolean field to a slice", async ({ expect, prismic, repo, token, hos "boolean", "my_field", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_field"); @@ -36,7 +36,7 @@ it("adds a boolean field to a custom type", async ({ expect, prismic, repo, toke "boolean", "is_active", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: is_active"); diff --git a/test/field-add-color.test.ts b/test/field-add-color.test.ts index cb72815..6d7d6e8 100644 --- a/test/field-add-color.test.ts +++ b/test/field-add-color.test.ts @@ -16,7 +16,7 @@ it("adds a color field to a slice", async ({ expect, prismic, repo, token, host "color", "my_color", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_color"); @@ -36,7 +36,7 @@ it("adds a color field to a custom type", async ({ expect, prismic, repo, token, "color", "my_color", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_color"); diff --git a/test/field-add-content-relationship.test.ts b/test/field-add-content-relationship.test.ts index 5879279..88d0ff8 100644 --- a/test/field-add-content-relationship.test.ts +++ b/test/field-add-content-relationship.test.ts @@ -22,7 +22,7 @@ it("adds a content relationship field to a slice", async ({ "content-relationship", "my_link", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_link"); @@ -48,7 +48,7 @@ it("adds a content relationship field to a custom type", async ({ "content-relationship", "my_link", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_link"); diff --git a/test/field-add-date.test.ts b/test/field-add-date.test.ts index f0deda9..4d9e7ae 100644 --- a/test/field-add-date.test.ts +++ b/test/field-add-date.test.ts @@ -16,7 +16,7 @@ it("adds a date field to a slice", async ({ expect, prismic, repo, token, host } "date", "my_date", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_date"); @@ -36,7 +36,7 @@ it("adds a date field to a custom type", async ({ expect, prismic, repo, token, "date", "my_date", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_date"); diff --git a/test/field-add-embed.test.ts b/test/field-add-embed.test.ts index 3aacedf..7849e3e 100644 --- a/test/field-add-embed.test.ts +++ b/test/field-add-embed.test.ts @@ -16,7 +16,7 @@ it("adds an embed field to a slice", async ({ expect, prismic, repo, token, host "embed", "my_embed", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_embed"); @@ -36,7 +36,7 @@ it("adds an embed field to a custom type", async ({ expect, prismic, repo, token "embed", "my_embed", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_embed"); diff --git a/test/field-add-geopoint.test.ts b/test/field-add-geopoint.test.ts index 2d13dce..b7112ca 100644 --- a/test/field-add-geopoint.test.ts +++ b/test/field-add-geopoint.test.ts @@ -16,7 +16,7 @@ it("adds a geopoint field to a slice", async ({ expect, prismic, repo, token, ho "geopoint", "my_location", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_location"); @@ -36,7 +36,7 @@ it("adds a geopoint field to a custom type", async ({ expect, prismic, repo, tok "geopoint", "my_location", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_location"); diff --git a/test/field-add-group.test.ts b/test/field-add-group.test.ts index 59d45e8..f31dd7b 100644 --- a/test/field-add-group.test.ts +++ b/test/field-add-group.test.ts @@ -18,7 +18,7 @@ it("adds a group field to a slice", async ({ expect, prismic, repo, token, host "group", "my_group", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_group"); @@ -38,7 +38,7 @@ it("adds a group field to a custom type", async ({ expect, prismic, repo, token, "group", "my_group", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_group"); @@ -65,7 +65,7 @@ it("adds a field inside a group using dot syntax", async ({ "text", "my_group.subtitle", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_group.subtitle"); @@ -93,7 +93,7 @@ it("errors when dot syntax targets a non-existent field", async ({ "text", "nonexistent.subtitle", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(1); expect(stderr).toContain('Field "nonexistent" does not exist.'); @@ -115,7 +115,7 @@ it("errors when dot syntax targets a non-group field", async ({ "text", "my_text.subtitle", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(1); expect(stderr).toContain('Field "my_text" is not a group field.'); diff --git a/test/field-add-image.test.ts b/test/field-add-image.test.ts index 9632c6e..5c4a51c 100644 --- a/test/field-add-image.test.ts +++ b/test/field-add-image.test.ts @@ -16,7 +16,7 @@ it("adds an image field to a slice", async ({ expect, prismic, repo, token, host "image", "my_image", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_image"); @@ -36,7 +36,7 @@ it("adds an image field to a custom type", async ({ expect, prismic, repo, token "image", "my_image", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_image"); diff --git a/test/field-add-integration.test.ts b/test/field-add-integration.test.ts index 78a7920..bcadd2e 100644 --- a/test/field-add-integration.test.ts +++ b/test/field-add-integration.test.ts @@ -16,7 +16,7 @@ it("adds an integration field to a slice", async ({ expect, prismic, repo, token "integration", "my_integration", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_integration"); @@ -36,7 +36,7 @@ it("adds an integration field to a custom type", async ({ expect, prismic, repo, "integration", "my_integration", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_integration"); diff --git a/test/field-add-link.test.ts b/test/field-add-link.test.ts index 02c0b04..ca77515 100644 --- a/test/field-add-link.test.ts +++ b/test/field-add-link.test.ts @@ -21,7 +21,7 @@ it("adds a link field to a slice", async ({ expect, prismic, repo, token, host } "link", "my_link", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_link"); @@ -41,7 +41,7 @@ it("adds a link field to a custom type", async ({ expect, prismic, repo, token, "link", "my_link", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_link"); @@ -61,7 +61,7 @@ it("adds a media link field with --allow media", async ({ expect, prismic, repo, "link", "my_media", "--to-slice", - slice.name, + slice.id, "--allow", "media", ]); diff --git a/test/field-add-number.test.ts b/test/field-add-number.test.ts index 69825d6..defe545 100644 --- a/test/field-add-number.test.ts +++ b/test/field-add-number.test.ts @@ -16,7 +16,7 @@ it("adds a number field to a slice", async ({ expect, prismic, repo, token, host "number", "my_number", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_number"); @@ -36,7 +36,7 @@ it("adds a number field to a custom type", async ({ expect, prismic, repo, token "number", "my_number", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_number"); diff --git a/test/field-add-rich-text.test.ts b/test/field-add-rich-text.test.ts index 8e35ca9..0bbe347 100644 --- a/test/field-add-rich-text.test.ts +++ b/test/field-add-rich-text.test.ts @@ -16,7 +16,7 @@ it("adds a rich text field to a slice", async ({ expect, prismic, repo, token, h "rich-text", "my_content", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_content"); @@ -36,7 +36,7 @@ it("adds a rich text field to a custom type", async ({ expect, prismic, repo, to "rich-text", "my_content", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_content"); diff --git a/test/field-add-select.test.ts b/test/field-add-select.test.ts index bcc5ee5..eb8cb76 100644 --- a/test/field-add-select.test.ts +++ b/test/field-add-select.test.ts @@ -16,7 +16,7 @@ it("adds a select field to a slice", async ({ expect, prismic, repo, token, host "select", "my_select", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_select"); @@ -36,7 +36,7 @@ it("adds a select field to a custom type", async ({ expect, prismic, repo, token "select", "my_select", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_select"); diff --git a/test/field-add-table.test.ts b/test/field-add-table.test.ts index 4c78da1..1c61529 100644 --- a/test/field-add-table.test.ts +++ b/test/field-add-table.test.ts @@ -16,7 +16,7 @@ it("adds a table field to a slice", async ({ expect, prismic, repo, token, host "table", "my_table", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_table"); @@ -36,7 +36,7 @@ it("adds a table field to a custom type", async ({ expect, prismic, repo, token, "table", "my_table", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_table"); diff --git a/test/field-add-text.test.ts b/test/field-add-text.test.ts index e5ebea2..c5983dd 100644 --- a/test/field-add-text.test.ts +++ b/test/field-add-text.test.ts @@ -16,7 +16,7 @@ it("adds a text field to a slice", async ({ expect, prismic, repo, token, host } "text", "subtitle", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: subtitle"); @@ -36,7 +36,7 @@ it("adds a text field to a custom type", async ({ expect, prismic, repo, token, "text", "subtitle", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: subtitle"); @@ -56,7 +56,7 @@ it("adds a text field to a page type", async ({ expect, prismic, repo, token, ho "text", "subtitle", "--to-type", - pageType.label!, + pageType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: subtitle"); diff --git a/test/field-add-timestamp.test.ts b/test/field-add-timestamp.test.ts index c641b3e..306021a 100644 --- a/test/field-add-timestamp.test.ts +++ b/test/field-add-timestamp.test.ts @@ -16,7 +16,7 @@ it("adds a timestamp field to a slice", async ({ expect, prismic, repo, token, h "timestamp", "my_timestamp", "--to-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_timestamp"); @@ -36,7 +36,7 @@ it("adds a timestamp field to a custom type", async ({ expect, prismic, repo, to "timestamp", "my_timestamp", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: my_timestamp"); diff --git a/test/field-add-uid.test.ts b/test/field-add-uid.test.ts index d87d8e5..e15ae06 100644 --- a/test/field-add-uid.test.ts +++ b/test/field-add-uid.test.ts @@ -15,7 +15,7 @@ it("adds a uid field to a custom type", async ({ expect, prismic, repo, token, h "add", "uid", "--to-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: uid"); @@ -34,7 +34,7 @@ it("adds a uid field to a page type", async ({ expect, prismic, repo, token, hos "add", "uid", "--to-type", - pageType.label!, + pageType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field added: uid"); diff --git a/test/field-edit.test.ts b/test/field-edit.test.ts index 8b4c835..8cd0379 100644 --- a/test/field-edit.test.ts +++ b/test/field-edit.test.ts @@ -16,7 +16,7 @@ it("edits a field label on a slice", async ({ expect, prismic, repo, token, host "edit", "my_field", "--from-slice", - slice.name, + slice.id, "--label", "New Label", ]); @@ -46,7 +46,7 @@ it("edits a field label on a custom type", async ({ expect, prismic, repo, token "edit", "title", "--from-type", - customType.label!, + customType.id, "--label", "Page Title", ]); @@ -70,7 +70,7 @@ it("edits boolean field options", async ({ expect, prismic, repo, token, host }) "edit", "is_active", "--from-slice", - slice.name, + slice.id, "--default-value", "true", "--true-label", @@ -103,7 +103,7 @@ it("edits number field options", async ({ expect, prismic, repo, token, host }) "edit", "quantity", "--from-slice", - slice.name, + slice.id, "--min", "0", "--max", @@ -133,7 +133,7 @@ it("edits select field options", async ({ expect, prismic, repo, token, host }) "edit", "color", "--from-slice", - slice.name, + slice.id, "--default-value", "green", "--option", @@ -170,7 +170,7 @@ it("edits link field options", async ({ expect, prismic, repo, token, host }) => "edit", "cta_link", "--from-slice", - slice.name, + slice.id, "--allow-target-blank", ]); expect(exitCode).toBe(0); diff --git a/test/field-remove.test.ts b/test/field-remove.test.ts index bf4cca9..84ae078 100644 --- a/test/field-remove.test.ts +++ b/test/field-remove.test.ts @@ -31,7 +31,7 @@ it("removes a field from a slice", async ({ expect, prismic, repo, token, host } "remove", "my_field", "--from-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field removed: my_field"); @@ -55,7 +55,7 @@ it("removes a field from a custom type", async ({ expect, prismic, repo, token, "remove", "title", "--from-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field removed: title"); @@ -95,7 +95,7 @@ it("removes a nested field using dot notation", async ({ expect, prismic, repo, "remove", "my_group.subtitle", "--from-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Field removed: my_group.subtitle"); diff --git a/test/field-view.test.ts b/test/field-view.test.ts index 964ea09..d05c474 100644 --- a/test/field-view.test.ts +++ b/test/field-view.test.ts @@ -19,7 +19,7 @@ it("views a field in a slice", async ({ expect, prismic, repo, token, host }) => "view", "title", "--from-slice", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Type: StructuredText"); @@ -39,7 +39,7 @@ it("views a field in a custom type", async ({ expect, prismic, repo, token, host "view", "count", "--from-type", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain("Type: Number"); @@ -60,7 +60,7 @@ it("outputs JSON with --json", async ({ expect, prismic, repo, token, host }) => "view", "is_active", "--from-slice", - slice.name, + slice.id, "--json", ]); expect(exitCode).toBe(0); @@ -79,7 +79,7 @@ it("errors for non-existent field", async ({ expect, prismic, repo, token, host "view", "nonexistent", "--from-slice", - slice.name, + slice.id, ]); expect(exitCode).not.toBe(0); expect(stderr).toContain("nonexistent"); diff --git a/test/slice-add-variation.test.ts b/test/slice-add-variation.test.ts index bfcbfbf..f32add7 100644 --- a/test/slice-add-variation.test.ts +++ b/test/slice-add-variation.test.ts @@ -17,11 +17,11 @@ it("adds a variation to a slice", async ({ expect, prismic, repo, token, host }) "add-variation", variationName, "--to", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); expect(stdout).toContain(`Added variation "${variationName}"`); - expect(stdout).toContain(`to slice "${slice.name}"`); + expect(stdout).toContain(`to slice "${slice.id}"`); const slices = await getSlices({ repo, token, host }); const updated = slices.find((s) => s.id === slice.id); diff --git a/test/slice-connect.test.ts b/test/slice-connect.test.ts index 791c30e..c19f436 100644 --- a/test/slice-connect.test.ts +++ b/test/slice-connect.test.ts @@ -6,7 +6,7 @@ import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["connect", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice connect [options]"); + expect(stdout).toContain("prismic slice connect [options]"); }); it("connects a slice to a type", async ({ expect, prismic, repo, token, host }) => { @@ -29,12 +29,12 @@ it("connects a slice to a type", async ({ expect, prismic, repo, token, host }) const { stdout, exitCode } = await prismic("slice", [ "connect", - slice.name, + slice.id, "--to", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Connected slice "${slice.name}" to "${customType.label}"`); + expect(stdout).toContain(`Connected slice "${slice.id}" to "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/slice-disconnect.test.ts b/test/slice-disconnect.test.ts index 49cb054..ecc56a8 100644 --- a/test/slice-disconnect.test.ts +++ b/test/slice-disconnect.test.ts @@ -6,7 +6,7 @@ import { getCustomTypes, insertCustomType, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["disconnect", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice disconnect [options]"); + expect(stdout).toContain("prismic slice disconnect [options]"); }); it("disconnects a slice from a type", async ({ expect, prismic, repo, token, host }) => { @@ -29,12 +29,12 @@ it("disconnects a slice from a type", async ({ expect, prismic, repo, token, hos const { stdout, exitCode } = await prismic("slice", [ "disconnect", - slice.name, + slice.id, "--from", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Disconnected slice "${slice.name}" from "${customType.label}"`); + expect(stdout).toContain(`Disconnected slice "${slice.id}" from "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/slice-edit-variation.test.ts b/test/slice-edit-variation.test.ts index 877c3c8..dfe0f41 100644 --- a/test/slice-edit-variation.test.ts +++ b/test/slice-edit-variation.test.ts @@ -4,7 +4,7 @@ import { getSlices, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["edit-variation", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice edit-variation [options]"); + expect(stdout).toContain("prismic slice edit-variation [options]"); }); it("edits a variation name", async ({ expect, prismic, repo, token, host }) => { @@ -32,14 +32,14 @@ it("edits a variation name", async ({ expect, prismic, repo, token, host }) => { const { stdout, exitCode } = await prismic("slice", [ "edit-variation", - variationName, + variationId, "--from-slice", - slice.name, + slice.id, "--name", newName, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Variation updated: "${newName}" in slice "${slice.name}"`); + expect(stdout).toContain(`Variation updated: "${variationId}" in slice "${slice.id}"`); const slices = await getSlices({ repo, token, host }); const updated = slices.find((s) => s.id === slice.id); diff --git a/test/slice-edit.test.ts b/test/slice-edit.test.ts index 2f76eb9..8df5c72 100644 --- a/test/slice-edit.test.ts +++ b/test/slice-edit.test.ts @@ -4,7 +4,7 @@ import { getSlices, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["edit", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice edit [options]"); + expect(stdout).toContain("prismic slice edit [options]"); }); it("edits a slice name", async ({ expect, prismic, repo, token, host }) => { @@ -13,7 +13,7 @@ it("edits a slice name", async ({ expect, prismic, repo, token, host }) => { const newName = `SliceS${crypto.randomUUID().split("-")[0]}`; - const { stdout, exitCode } = await prismic("slice", ["edit", slice.name, "--name", newName]); + const { stdout, exitCode } = await prismic("slice", ["edit", slice.id, "--name", newName]); expect(exitCode).toBe(0); expect(stdout).toContain(`Slice updated: "${newName}"`); diff --git a/test/slice-remove-variation.test.ts b/test/slice-remove-variation.test.ts index 1adcf71..76757cc 100644 --- a/test/slice-remove-variation.test.ts +++ b/test/slice-remove-variation.test.ts @@ -4,7 +4,7 @@ import { getSlices, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["remove-variation", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice remove-variation [options]"); + expect(stdout).toContain("prismic slice remove-variation [options]"); }); it("removes a variation from a slice", async ({ expect, prismic, repo, token, host }) => { @@ -30,12 +30,12 @@ it("removes a variation from a slice", async ({ expect, prismic, repo, token, ho const { stdout, exitCode } = await prismic("slice", [ "remove-variation", - variationName, + variationId, "--from", - slice.name, + slice.id, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Removed variation "${variationName}" from slice "${slice.name}"`); + expect(stdout).toContain(`Removed variation "${variationId}" from slice "${slice.id}"`); const slices = await getSlices({ repo, token, host }); const updated = slices.find((s) => s.id === slice.id); diff --git a/test/slice-remove.test.ts b/test/slice-remove.test.ts index c89a638..eb9ae8c 100644 --- a/test/slice-remove.test.ts +++ b/test/slice-remove.test.ts @@ -4,16 +4,16 @@ import { getSlices, insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["remove", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice remove [options]"); + expect(stdout).toContain("prismic slice remove [options]"); }); it("removes a slice", async ({ expect, prismic, repo, token, host }) => { const slice = buildSlice(); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("slice", ["remove", slice.name]); + const { stdout, exitCode } = await prismic("slice", ["remove", slice.id]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Slice removed: "${slice.name}" (id: ${slice.id})`); + expect(stdout).toContain(`Slice removed: ${slice.id}`); const slices = await getSlices({ repo, token, host }); const removed = slices.find((s) => s.id === slice.id); diff --git a/test/slice-view.test.ts b/test/slice-view.test.ts index fd27e26..2560fb0 100644 --- a/test/slice-view.test.ts +++ b/test/slice-view.test.ts @@ -4,14 +4,14 @@ import { insertSlice } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("slice", ["view", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic slice view [options]"); + expect(stdout).toContain("prismic slice view [options]"); }); it("views a slice", async ({ expect, prismic, repo, token, host }) => { const slice = buildSlice(); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("slice", ["view", slice.name]); + const { stdout, exitCode } = await prismic("slice", ["view", slice.id]); expect(exitCode).toBe(0); expect(stdout).toContain(`ID: ${slice.id}`); expect(stdout).toContain(`Name: ${slice.name}`); @@ -48,7 +48,7 @@ it("shows fields per variation", async ({ expect, prismic, repo, token, host }) }); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("slice", ["view", slice.name]); + const { stdout, exitCode } = await prismic("slice", ["view", slice.id]); expect(exitCode).toBe(0); expect(stdout).toContain("default:"); expect(stdout).toMatch(/title\s+StructuredText\s+Title\s+"Enter title"/); @@ -61,7 +61,7 @@ it("views a slice as JSON", async ({ expect, prismic, repo, token, host }) => { const slice = buildSlice(); await insertSlice(slice, { repo, token, host }); - const { stdout, exitCode } = await prismic("slice", ["view", slice.name, "--json"]); + const { stdout, exitCode } = await prismic("slice", ["view", slice.id, "--json"]); expect(exitCode).toBe(0); const parsed = JSON.parse(stdout); expect(parsed).toMatchObject({ id: slice.id, name: slice.name }); diff --git a/test/type-add-tab.test.ts b/test/type-add-tab.test.ts index 71ccce0..200036f 100644 --- a/test/type-add-tab.test.ts +++ b/test/type-add-tab.test.ts @@ -17,10 +17,10 @@ it("adds a tab to a type", async ({ expect, prismic, repo, token, host }) => { "add-tab", tabName, "--to", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Added tab "${tabName}" to "${customType.label}"`); + expect(stdout).toContain(`Added tab "${tabName}" to "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); @@ -38,11 +38,11 @@ it("adds a tab with a slice zone", async ({ expect, prismic, repo, token, host } "add-tab", tabName, "--to", - customType.label!, + customType.id, "--with-slice-zone", ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Added tab "${tabName}" to "${customType.label}"`); + expect(stdout).toContain(`Added tab "${tabName}" to "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/type-edit-tab.test.ts b/test/type-edit-tab.test.ts index cdddaf7..3752ca4 100644 --- a/test/type-edit-tab.test.ts +++ b/test/type-edit-tab.test.ts @@ -17,12 +17,12 @@ it("edits a tab name", async ({ expect, prismic, repo, token, host }) => { "edit-tab", "OldName", "--from-type", - customType.label!, + customType.id, "--name", newName, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Tab updated: "OldName" in "${customType.label}"`); + expect(stdout).toContain(`Tab updated: "OldName" in "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); @@ -38,11 +38,11 @@ it("adds a slice zone to a tab", async ({ expect, prismic, repo, token, host }) "edit-tab", "Main", "--from-type", - customType.label!, + customType.id, "--with-slice-zone", ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Tab updated: "Main" in "${customType.label}"`); + expect(stdout).toContain(`Tab updated: "Main" in "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); @@ -68,11 +68,11 @@ it("removes a slice zone from a tab", async ({ expect, prismic, repo, token, hos "edit-tab", "Main", "--from-type", - customType.label!, + customType.id, "--without-slice-zone", ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Tab updated: "Main" in "${customType.label}"`); + expect(stdout).toContain(`Tab updated: "Main" in "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/type-edit.test.ts b/test/type-edit.test.ts index 0df5449..23af10f 100644 --- a/test/type-edit.test.ts +++ b/test/type-edit.test.ts @@ -4,7 +4,7 @@ import { getCustomTypes, insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("type", ["edit", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic type edit [options]"); + expect(stdout).toContain("prismic type edit [options]"); }); it("edits a type name", async ({ expect, prismic, repo, token, host }) => { @@ -13,10 +13,10 @@ it("edits a type name", async ({ expect, prismic, repo, token, host }) => { const newName = `TypeT${crypto.randomUUID().split("-")[0]}`; - const { stdout, stderr, exitCode } = await prismic("type", ["edit", customType.label!, "--name", newName]); + const { stdout, stderr, exitCode } = await prismic("type", ["edit", customType.id, "--name", newName]); expect(stderr).toBe(""); expect(exitCode).toBe(0); - expect(stdout).toContain(`Type updated: "${newName}"`); + expect(stdout).toContain(`Type updated: "${newName}" (id: ${customType.id})`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); @@ -27,7 +27,7 @@ it("edits a type format", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stderr, exitCode } = await prismic("type", ["edit", customType.label!, "--format", "page"]); + const { stderr, exitCode } = await prismic("type", ["edit", customType.id, "--format", "page"]); expect(stderr).toBe(""); expect(exitCode).toBe(0); diff --git a/test/type-remove-tab.test.ts b/test/type-remove-tab.test.ts index 0b30dc6..d881a7a 100644 --- a/test/type-remove-tab.test.ts +++ b/test/type-remove-tab.test.ts @@ -15,10 +15,10 @@ it("removes a tab from a type", async ({ expect, prismic, repo, token, host }) = "remove-tab", "Extra", "--from", - customType.label!, + customType.id, ]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Removed tab "Extra" from "${customType.label}"`); + expect(stdout).toContain(`Removed tab "Extra" from "${customType.id}"`); const customTypes = await getCustomTypes({ repo, token, host }); const updated = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/type-remove.test.ts b/test/type-remove.test.ts index 1d51363..3388e58 100644 --- a/test/type-remove.test.ts +++ b/test/type-remove.test.ts @@ -4,16 +4,16 @@ import { getCustomTypes, insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("type", ["remove", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic type remove [options]"); + expect(stdout).toContain("prismic type remove [options]"); }); it("removes a type", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("type", ["remove", customType.label!]); + const { stdout, exitCode } = await prismic("type", ["remove", customType.id]); expect(exitCode).toBe(0); - expect(stdout).toContain(`Type removed: "${customType.label}" (id: ${customType.id})`); + expect(stdout).toContain(`Type removed: ${customType.id}`); const customTypes = await getCustomTypes({ repo, token, host }); const removed = customTypes.find((ct) => ct.id === customType.id); diff --git a/test/type-view.test.ts b/test/type-view.test.ts index 60ba122..e90e6c8 100644 --- a/test/type-view.test.ts +++ b/test/type-view.test.ts @@ -4,14 +4,14 @@ import { insertCustomType } from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("type", ["view", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("prismic type view [options]"); + expect(stdout).toContain("prismic type view [options]"); }); it("views a type", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("type", ["view", customType.label!]); + const { stdout, exitCode } = await prismic("type", ["view", customType.id]); expect(exitCode).toBe(0); expect(stdout).toContain(`ID: ${customType.id}`); expect(stdout).toContain(`Name: ${customType.label}`); @@ -34,7 +34,7 @@ it("shows fields per tab", async ({ expect, prismic, repo, token, host }) => { }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("type", ["view", customType.label!]); + const { stdout, exitCode } = await prismic("type", ["view", customType.id]); expect(exitCode).toBe(0); expect(stdout).toContain("Main:"); expect(stdout).toMatch(/title\s+StructuredText\s+Title\s+"Enter title"/); @@ -47,7 +47,7 @@ it("views a type as JSON", async ({ expect, prismic, repo, token, host }) => { const customType = buildCustomType({ format: "custom" }); await insertCustomType(customType, { repo, token, host }); - const { stdout, exitCode } = await prismic("type", ["view", customType.label!, "--json"]); + const { stdout, exitCode } = await prismic("type", ["view", customType.id, "--json"]); expect(exitCode).toBe(0); const parsed = JSON.parse(stdout); expect(parsed).toMatchObject({ id: customType.id, label: customType.label, format: "custom" }); From af8d9d8c8e090c509059a04a3b4e5ec29be5e993 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 13 Apr 2026 18:39:24 -1000 Subject: [PATCH 21/29] refactor: add `getCustomType` and `getSlice` client functions (#124) * refactor: add `getCustomType` and `getSlice` client functions Replace the `getCustomTypes().find()` and `getSlices().find()` patterns with dedicated single-resource fetch functions that set contextual error messages on `NotFoundRequestError`. Add a central handler in the root router to print those messages. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: give `NotFoundRequestError` a user-friendly default message Overrides the raw "fetch failed: " message with "Not found." so unhandled 404s from any endpoint still produce a clean message when caught by the root error handler. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: include URL in default NotFoundRequestError message Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/clients/custom-types.ts | 40 +++++++++++++++++++++++++- src/commands/slice-add-variation.ts | 9 ++---- src/commands/slice-connect.ts | 15 ++-------- src/commands/slice-disconnect.ts | 15 ++-------- src/commands/slice-edit-variation.ts | 9 ++---- src/commands/slice-edit.ts | 9 ++---- src/commands/slice-remove-variation.ts | 9 ++---- src/commands/slice-remove.ts | 9 ++---- src/commands/slice-view.ts | 11 ++----- src/commands/type-add-tab.ts | 19 +++++------- src/commands/type-edit-tab.ts | 27 +++++++---------- src/commands/type-edit.ts | 21 ++++++-------- src/commands/type-remove-tab.ts | 21 ++++++-------- src/commands/type-remove.ts | 13 +++------ src/index.ts | 11 ++++++- src/lib/request.ts | 5 ++++ src/models.ts | 26 ++++------------- 17 files changed, 116 insertions(+), 153 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index fc5640b..b02ae66 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -1,6 +1,6 @@ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; -import { request } from "../lib/request"; +import { NotFoundRequestError, request } from "../lib/request"; export async function getCustomTypes(config: { repo: string; @@ -16,6 +16,25 @@ export async function getCustomTypes(config: { return response; } +export async function getCustomType( + id: string, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`customtypes/${encodeURIComponent(id)}`, customTypesServiceUrl); + try { + return await request(url, { + headers: { repository: repo, Authorization: `Bearer ${token}` }, + }); + } catch (error) { + if (error instanceof NotFoundRequestError) { + error.message = `Type not found: ${id}`; + } + throw error; + } +} + export async function insertCustomType( model: CustomType, config: { repo: string; token: string | undefined; host: string }, @@ -71,6 +90,25 @@ export async function getSlices(config: { return response; } +export async function getSlice( + id: string, + config: { repo: string; token: string | undefined; host: string }, +): Promise { + const { repo, token, host } = config; + const customTypesServiceUrl = getCustomTypesServiceUrl(host); + const url = new URL(`slices/${encodeURIComponent(id)}`, customTypesServiceUrl); + try { + return await request(url, { + headers: { repository: repo, Authorization: `Bearer ${token}` }, + }); + } catch (error) { + if (error instanceof NotFoundRequestError) { + error.message = `Slice not found: ${id}`; + } + throw error; + } +} + export async function insertSlice( model: SharedSlice, config: { repo: string; token: string | undefined; host: string }, diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index dca5572..200ac1b 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -4,7 +4,7 @@ import { camelCase } from "change-case"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlices, updateSlice } from "../clients/custom-types"; +import { getSlice, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -29,12 +29,7 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.id === to); - - if (!slice) { - throw new CommandError(`Slice not found: ${to}`); - } + const slice = await getSlice(to, { repo, token, host }); if (slice.variations.some((v) => v.id === id)) { throw new CommandError(`Variation "${id}" already exists in slice "${to}".`); diff --git a/src/commands/slice-connect.ts b/src/commands/slice-connect.ts index 7450d26..1d8b40e 100644 --- a/src/commands/slice-connect.ts +++ b/src/commands/slice-connect.ts @@ -2,7 +2,7 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; +import { getCustomType, getSlice, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -36,17 +36,8 @@ export default createCommand(config, async ({ positionals, values }) => { const host = await getHost(); const apiConfig = { repo, token, host }; - const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.id === id); - if (!slice) { - throw new CommandError(`Slice not found: ${id}`); - } - - const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.id === to); - if (!customType) { - throw new CommandError(`Type not found: ${to}`); - } + const slice = await getSlice(id, apiConfig); + const customType = await getCustomType(to, apiConfig); const allFields: Record = Object.assign( {}, diff --git a/src/commands/slice-disconnect.ts b/src/commands/slice-disconnect.ts index f9cbf01..87e9d2b 100644 --- a/src/commands/slice-disconnect.ts +++ b/src/commands/slice-disconnect.ts @@ -2,7 +2,7 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, getSlices, updateCustomType } from "../clients/custom-types"; +import { getCustomType, getSlice, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -36,17 +36,8 @@ export default createCommand(config, async ({ positionals, values }) => { const host = await getHost(); const apiConfig = { repo, token, host }; - const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.id === id); - if (!slice) { - throw new CommandError(`Slice not found: ${id}`); - } - - const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.id === from); - if (!customType) { - throw new CommandError(`Type not found: ${from}`); - } + const slice = await getSlice(id, apiConfig); + const customType = await getCustomType(from, apiConfig); const allFields: Record = Object.assign( {}, diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index aafbbf2..ff51101 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -2,7 +2,7 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlices, updateSlice } from "../clients/custom-types"; +import { getSlice, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -27,12 +27,7 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.id === sliceId); - - if (!slice) { - throw new CommandError(`Slice not found: ${sliceId}`); - } + const slice = await getSlice(sliceId, { repo, token, host }); const variation = slice.variations.find((v) => v.id === id); diff --git a/src/commands/slice-edit.ts b/src/commands/slice-edit.ts index 003e263..701b5d2 100644 --- a/src/commands/slice-edit.ts +++ b/src/commands/slice-edit.ts @@ -2,7 +2,7 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlices, updateSlice } from "../clients/custom-types"; +import { getSlice, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -26,12 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.id === id); - - if (!slice) { - throw new CommandError(`Slice not found: ${id}`); - } + const slice = await getSlice(id, { repo, token, host }); const updatedSlice: SharedSlice = { ...slice }; diff --git a/src/commands/slice-remove-variation.ts b/src/commands/slice-remove-variation.ts index 53e64c0..956c523 100644 --- a/src/commands/slice-remove-variation.ts +++ b/src/commands/slice-remove-variation.ts @@ -2,7 +2,7 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlices, updateSlice } from "../clients/custom-types"; +import { getSlice, updateSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -26,12 +26,7 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.id === from); - - if (!slice) { - throw new CommandError(`Slice not found: ${from}`); - } + const slice = await getSlice(from, { repo, token, host }); const variation = slice.variations.find((v) => v.id === id); diff --git a/src/commands/slice-remove.ts b/src/commands/slice-remove.ts index 4831e25..8a49da7 100644 --- a/src/commands/slice-remove.ts +++ b/src/commands/slice-remove.ts @@ -1,6 +1,6 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlices, removeSlice } from "../clients/custom-types"; +import { getSlice, removeSlice } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -23,12 +23,7 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((s) => s.id === id); - - if (!slice) { - throw new CommandError(`Slice not found: ${id}`); - } + const slice = await getSlice(id, { repo, token, host }); try { await removeSlice(slice.id, { repo, host, token }); diff --git a/src/commands/slice-view.ts b/src/commands/slice-view.ts index ec53508..3ff9a1a 100644 --- a/src/commands/slice-view.ts +++ b/src/commands/slice-view.ts @@ -1,6 +1,6 @@ import { getHost, getToken } from "../auth"; -import { getSlices } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { getSlice } from "../clients/custom-types"; +import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; @@ -23,12 +23,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); - const slices = await getSlices({ repo, token, host }); - const slice = slices.find((slice) => slice.id === id); - - if (!slice) { - throw new CommandError(`Slice not found: ${id}`); - } + const slice = await getSlice(id, { repo, token, host }); if (json) { console.info(stringify(slice)); diff --git a/src/commands/type-add-tab.ts b/src/commands/type-add-tab.ts index 4cf02f4..6df79e4 100644 --- a/src/commands/type-add-tab.ts +++ b/src/commands/type-add-tab.ts @@ -1,6 +1,6 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { getCustomType, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -25,18 +25,13 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === to); + const customType = await getCustomType(to, { repo, token, host }); - if (!type) { - throw new CommandError(`Type not found: ${to}`); - } - - if (name in type.json) { + if (name in customType.json) { throw new CommandError(`Tab "${name}" already exists in "${to}".`); } - type.json[name] = withSliceZone + customType.json[name] = withSliceZone ? { slices: { type: "Slices", @@ -47,7 +42,7 @@ export default createCommand(config, async ({ positionals, values }) => { : {}; try { - await updateCustomType(type, { repo, host, token }); + await updateCustomType(customType, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -57,9 +52,9 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.updateCustomType(type); + await adapter.updateCustomType(customType); } catch { - await adapter.createCustomType(type); + await adapter.createCustomType(customType); } await adapter.generateTypes(); diff --git a/src/commands/type-edit-tab.ts b/src/commands/type-edit-tab.ts index b8a1fc1..cb4d0a9 100644 --- a/src/commands/type-edit-tab.ts +++ b/src/commands/type-edit-tab.ts @@ -2,7 +2,7 @@ import type { CustomType } from "@prismicio/types-internal/lib/customtypes"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { getCustomType, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -29,14 +29,9 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === typeId); + const customType = await getCustomType(typeId, { repo, token, host }); - if (!type) { - throw new CommandError(`Type not found: ${typeId}`); - } - - if (!(currentName in type.json)) { + if (!(currentName in customType.json)) { throw new CommandError(`Tab "${currentName}" not found in "${typeId}".`); } @@ -45,7 +40,7 @@ export default createCommand(config, async ({ positionals, values }) => { } if ("with-slice-zone" in values) { - const tab = type.json[currentName]; + const tab = customType.json[currentName]; const hasSliceZone = Object.values(tab).some((field) => field.type === "Slices"); if (hasSliceZone) { @@ -60,7 +55,7 @@ export default createCommand(config, async ({ positionals, values }) => { } if ("without-slice-zone" in values) { - const tab = type.json[currentName]; + const tab = customType.json[currentName]; const sliceZoneEntry = Object.entries(tab).find(([, field]) => field.type === "Slices"); if (!sliceZoneEntry) { @@ -81,19 +76,19 @@ export default createCommand(config, async ({ positionals, values }) => { } if ("name" in values) { - if (values.name! in type.json) { + if (values.name! in customType.json) { throw new CommandError(`Tab "${values.name}" already exists in "${typeId}".`); } const newJson: CustomType["json"] = {}; - for (const [key, value] of Object.entries(type.json)) { + for (const [key, value] of Object.entries(customType.json)) { newJson[key === currentName ? values.name! : key] = value; } - type.json = newJson; + customType.json = newJson; } try { - await updateCustomType(type, { repo, host, token }); + await updateCustomType(customType, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -103,9 +98,9 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.updateCustomType(type); + await adapter.updateCustomType(customType); } catch { - await adapter.createCustomType(type); + await adapter.createCustomType(customType); } await adapter.generateTypes(); diff --git a/src/commands/type-edit.ts b/src/commands/type-edit.ts index e6817cc..7dcf6b6 100644 --- a/src/commands/type-edit.ts +++ b/src/commands/type-edit.ts @@ -1,6 +1,6 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { getCustomType, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -29,18 +29,13 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === id); + const customType = await getCustomType(id, { repo, token, host }); - if (!type) { - throw new CommandError(`Type not found: ${id}`); - } - - if ("name" in values) type.label = values.name; - if ("format" in values) type.format = values.format as "custom" | "page"; + if ("name" in values) customType.label = values.name; + if ("format" in values) customType.format = values.format as "custom" | "page"; try { - await updateCustomType(type, { repo, host, token }); + await updateCustomType(customType, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -50,11 +45,11 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.updateCustomType(type); + await adapter.updateCustomType(customType); } catch { - await adapter.createCustomType(type); + await adapter.createCustomType(customType); } await adapter.generateTypes(); - console.info(`Type updated: "${type.label}" (id: ${type.id})`); + console.info(`Type updated: "${customType.label}" (id: ${customType.id})`); }); diff --git a/src/commands/type-remove-tab.ts b/src/commands/type-remove-tab.ts index 53bd1a8..250bd68 100644 --- a/src/commands/type-remove-tab.ts +++ b/src/commands/type-remove-tab.ts @@ -1,6 +1,6 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, updateCustomType } from "../clients/custom-types"; +import { getCustomType, updateCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -24,25 +24,20 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === from); + const customType = await getCustomType(from, { repo, token, host }); - if (!type) { - throw new CommandError(`Type not found: ${from}`); - } - - if (!(name in type.json)) { + if (!(name in customType.json)) { throw new CommandError(`Tab "${name}" not found in "${from}".`); } - if (Object.keys(type.json).length <= 1) { + if (Object.keys(customType.json).length <= 1) { throw new CommandError(`Cannot remove the last tab from "${from}".`); } - delete type.json[name]; + delete customType.json[name]; try { - await updateCustomType(type, { repo, host, token }); + await updateCustomType(customType, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -52,9 +47,9 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.updateCustomType(type); + await adapter.updateCustomType(customType); } catch { - await adapter.createCustomType(type); + await adapter.createCustomType(customType); } await adapter.generateTypes(); diff --git a/src/commands/type-remove.ts b/src/commands/type-remove.ts index 810c5dd..cc548fe 100644 --- a/src/commands/type-remove.ts +++ b/src/commands/type-remove.ts @@ -1,6 +1,6 @@ import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getCustomTypes, removeCustomType } from "../clients/custom-types"; +import { getCustomType, removeCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -23,15 +23,10 @@ export default createCommand(config, async ({ positionals, values }) => { const adapter = await getAdapter(); const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === id); - - if (!type) { - throw new CommandError(`Type not found: ${id}`); - } + const customType = await getCustomType(id, { repo, token, host }); try { - await removeCustomType(type.id, { repo, host, token }); + await removeCustomType(customType.id, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -41,7 +36,7 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.deleteCustomType(type.id); + await adapter.deleteCustomType(customType.id); } catch {} await adapter.generateTypes(); diff --git a/src/index.ts b/src/index.ts index a20961b..174f8cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,11 @@ import webhook from "./commands/webhook"; import whoami from "./commands/whoami"; import { InvalidPrismicConfig, MissingPrismicConfig } from "./config"; import { CommandError, createCommandRouter } from "./lib/command"; -import { ForbiddenRequestError, UnauthorizedRequestError } from "./lib/request"; +import { + ForbiddenRequestError, + NotFoundRequestError, + UnauthorizedRequestError, +} from "./lib/request"; import { initSegment, segmentIdentify, @@ -198,6 +202,11 @@ async function main(): Promise { return; } + if (error instanceof NotFoundRequestError) { + console.error(error.message); + return; + } + if (error instanceof InvalidPrismicConfig) { console.error(`${error.message} Run \`prismic init\` to re-create a config.`); return; diff --git a/src/lib/request.ts b/src/lib/request.ts index 04ba67a..50f0a98 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -91,6 +91,11 @@ export class UnknownRequestError extends RequestError { } export class NotFoundRequestError extends RequestError { name = "NotFoundRequestError"; + + constructor(response: Response) { + super(response); + this.message = `Not found: ${response.url}`; + } } export class ForbiddenRequestError extends RequestError { name = "ForbiddenRequestError"; diff --git a/src/models.ts b/src/models.ts index 3b6e763..009fc6b 100644 --- a/src/models.ts +++ b/src/models.ts @@ -3,7 +3,7 @@ import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; import type { CommandConfig } from "./lib/command"; import { getAdapter } from "./adapters"; -import { getCustomTypes, getSlices, updateCustomType, updateSlice } from "./clients/custom-types"; +import { getCustomType, getSlice, updateCustomType, updateSlice } from "./clients/custom-types"; import { CommandError } from "./lib/command"; import { UnknownRequestError } from "./lib/request"; @@ -54,11 +54,7 @@ export async function resolveFieldContainer( } if (fromSlice) { - const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.id === fromSlice); - if (!slice) { - throw new CommandError(`Slice not found: ${fromSlice}`); - } + const slice = await getSlice(fromSlice, apiConfig); const variation = slice.variations.find((v) => v.id === variationId); if (!variation) { const variationIds = slice.variations?.map((v) => v.id).join(", ") || "(none)"; @@ -81,11 +77,7 @@ export async function resolveFieldContainer( ]; } - const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.id === fromType); - if (!customType) { - throw new CommandError(`Type not found: ${fromType}`); - } + const customType = await getCustomType(fromType!, apiConfig); let tab: Record | undefined; for (const tabName in customType.json) { if (id in customType.json[tabName]) tab = customType.json[tabName]; @@ -139,11 +131,7 @@ export async function resolveModel( } const variation = values.variation ?? "default"; - const slices = await getSlices(apiConfig); - const slice = slices.find((s) => s.id === sliceId); - if (!slice) { - throw new CommandError(`Slice not found: ${sliceId}`); - } + const slice = await getSlice(sliceId, apiConfig); const newModel = structuredClone(slice); const newVariation = newModel.variations?.find((v) => v.id === variation); @@ -181,11 +169,7 @@ export async function resolveModel( } const tab = values.tab ?? "Main"; - const customTypes = await getCustomTypes(apiConfig); - const customType = customTypes.find((ct) => ct.id === typeId); - if (!customType) { - throw new CommandError(`Type not found: ${typeId}`); - } + const customType = await getCustomType(typeId!, apiConfig); const newModel = structuredClone(customType); const newTab = newModel.json[tab]; From ddd055526374aaf12587cedd5429009e7ff959ab Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 14 Apr 2026 08:30:26 -1000 Subject: [PATCH 22/29] feat: add `--field` option to `field add content-relationship` and `field edit` (#123) * feat: add `--field` option to `field add content-relationship` and `field edit` Add a repeatable `--field` flag for specifying which fields to fetch from related documents. Fields are validated against the production custom type model via the Custom Types API. Supports dot notation for nested selection: - `--field title` for top-level fields - `--field group.name` for group sub-fields - `--field cr.name` for CR target type fields - `--field group.cr.group.leaf` for full depth Closes #103 Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: use `getCustomType` instead of `getCustomTypes` + find Replace 7 call sites that fetch all custom types just to find one by ID with the single-type `GET /customtypes/{id}` endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: simplify field selection resolution with single recursive function Replace 7 functions (157 lines) with a single recursive `resolveFields` function and named `ResolvedField` type, reducing to 3 functions (107 lines). Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove unnatural phrasing from JSDoc comment Co-Authored-By: Claude Opus 4.6 (1M context) * fix: simplify test custom type construction to use default fields Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../field-add-content-relationship.ts | 29 ++++- src/commands/field-edit.ts | 26 ++++- src/models.ts | 109 +++++++++++++++++- test/field-add-content-relationship.test.ts | 40 +++++++ test/field-edit.test.ts | 41 +++++++ 5 files changed, 239 insertions(+), 6 deletions(-) diff --git a/src/commands/field-add-content-relationship.ts b/src/commands/field-add-content-relationship.ts index 93e71b3..63427d8 100644 --- a/src/commands/field-add-content-relationship.ts +++ b/src/commands/field-add-content-relationship.ts @@ -3,8 +3,9 @@ import type { Link } from "@prismicio/types-internal/lib/customtypes"; import { capitalCase } from "change-case"; import { getHost, getToken } from "../auth"; +import { getCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; +import { resolveFieldSelection, resolveFieldTarget, resolveModel, TARGET_OPTIONS } from "../models"; import { getRepositoryName } from "../project"; const config = { @@ -33,25 +34,47 @@ const config = { multiple: true, description: "Restrict to documents of this type (can be repeated)", }, + field: { + type: "string", + multiple: true, + description: "Fetch this field from the related document (can be repeated, requires one --custom-type)", + }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; - const { label, tag: tags, "custom-type": customtypes, repo = await getRepositoryName() } = values; + const { + label, + tag: tags, + "custom-type": customtypes, + field: fieldSelection, + repo = await getRepositoryName(), + } = values; + + if (fieldSelection && (!customtypes || customtypes.length !== 1)) { + throw new CommandError("--field requires exactly one --custom-type."); + } const token = await getToken(); const host = await getHost(); const [fields, saveModel] = await resolveModel(values, { repo, token, host }); const [targetFields, fieldId] = resolveFieldTarget(fields, id); + let resolvedCustomTypes: NonNullable["customtypes"] = customtypes; + if (fieldSelection && customtypes) { + const targetType = await getCustomType(customtypes[0], { repo, token, host }); + const resolvedFields = await resolveFieldSelection(fieldSelection, targetType, { repo, token, host }); + resolvedCustomTypes = [{ id: customtypes[0], fields: resolvedFields }] as typeof resolvedCustomTypes; + } + const field: Link = { type: "Link", config: { label: label ?? capitalCase(fieldId), select: "document", tags, - customtypes, + customtypes: resolvedCustomTypes, }, }; diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index fa184a3..e37e57c 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -1,6 +1,7 @@ import { getHost, getToken } from "../auth"; +import { getCustomType } from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; -import { resolveFieldContainer, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; +import { resolveFieldContainer, resolveFieldSelection, resolveFieldTarget, SOURCE_OPTIONS } from "../models"; import { getRepositoryName } from "../project"; const config = { @@ -66,6 +67,11 @@ const config = { multiple: true, description: "Allowed custom type (content-relationship, can be repeated)", }, + field: { + type: "string", + multiple: true, + description: "Fetch this field from the related document (content-relationship, can be repeated)", + }, // Rich Text allow: { type: "string", @@ -154,7 +160,23 @@ export default createCommand(config, async ({ positionals, values }) => { if ("repeatable" in values) field.config.repeat = values.repeatable; if ("variant" in values) field.config.variants = values.variant; if ("tag" in values) field.config.tags = values.tag; - if ("custom-type" in values) field.config.customtypes = values["custom-type"]; + if ("field" in values) { + const cts = "custom-type" in values ? values["custom-type"] : field.config.customtypes; + if (!cts || cts.length === 0) { + throw new CommandError( + "--field requires the field to be restricted to a custom type. Use --custom-type to specify one.", + ); + } + if (cts.length > 1) { + throw new CommandError("--field requires the field to be restricted to a single custom type."); + } + const ctId = typeof cts[0] === "string" ? cts[0] : cts[0].id; + const targetType = await getCustomType(ctId, { repo, token, host }); + const resolvedFields = await resolveFieldSelection(values.field!, targetType, { repo, token, host }); + field.config.customtypes = [{ id: ctId, fields: resolvedFields }] as typeof field.config.customtypes; + } else if ("custom-type" in values) { + field.config.customtypes = values["custom-type"]; + } break; } } diff --git a/src/models.ts b/src/models.ts index 009fc6b..4755ced 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,4 +1,4 @@ -import type { DynamicWidget } from "@prismicio/types-internal/lib/customtypes"; +import type { CustomType, DynamicWidget, Link } from "@prismicio/types-internal/lib/customtypes"; import type { CommandConfig } from "./lib/command"; @@ -235,3 +235,110 @@ export function resolveFieldTarget( return [currentFields, fieldId]; } + +// Mirrors the Prismic API's nested field selection format for content relationships. +// A field is either a leaf (string), a group (with sub-fields), or a CR (with a target type). +type ResolvedField = + | string + | { id: string; fields: ResolvedField[] } + | { id: string; customtypes: { id: string; fields: ResolvedField[] }[] }; + +const UNFETCHABLE_FIELD_TYPES = ["Slices", "UID", "Choice"]; + +/** + * Resolves user-provided dot-separated field paths (e.g. ["title", "group.cr.name"]) + * against a custom type, producing the nested structure the Prismic API expects. + */ +export async function resolveFieldSelection( + fieldSelection: string[], + targetType: CustomType, + apiConfig: ApiConfig, +): Promise { + // Merge all tabs into one flat field map. + const fields: Record = Object.assign({}, ...Object.values(targetType.json)); + + return resolveFields(fieldSelection, fields, targetType.id, apiConfig, 1); +} + +/** + * Splits paths by their first segment, validates leaves, and recurses into + * groups and content relationships. + * + * @param crDepth - How many more CR boundaries we can cross. The API supports + * at most: type → CR → group → leaf, so the entry point passes 1. + */ +async function resolveFields( + paths: string[], + fields: Record, + context: string, + apiConfig: ApiConfig, + crDepth: number, +): Promise { + const result: ResolvedField[] = []; + const grouped = new Map(); + + // Split each path into leaves (no dot) vs. prefixed groups (has dot). + for (const path of paths) { + const dot = path.indexOf("."); + if (dot === -1) { + validateLeafField(path, fields, context); + result.push(path); + } else { + const key = path.slice(0, dot); + const rest = path.slice(dot + 1); + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(rest); + } + } + + // Recurse into each prefixed group. + for (const [id, subPaths] of grouped) { + const field = fields[id]; + if (!field) { + throw new CommandError(`Field "${id}" not found on type "${context}".`); + } + + if (field.type === "Group") { + const groupFields = field.config?.fields ?? {}; + const resolved = await resolveFields(subPaths, groupFields, context, apiConfig, crDepth); + result.push({ id, fields: resolved }); + + } else if (field.type === "Link" && field.config?.select === "document") { + if (crDepth <= 0) { + throw new CommandError("Cannot nest deeper than --field group.cr.group.leaf."); + } + + // CR must target exactly one custom type so we know which schema to resolve against. + const cts = (field as Link).config?.customtypes; + if (!cts || cts.length !== 1) { + throw new CommandError( + `Field "${id}" must be restricted to a single custom type to select fields from it.`, + ); + } + const ctId = typeof cts[0] === "string" ? cts[0] : cts[0].id; + + // Cross the CR boundary: fetch the target type and resolve sub-paths against it. + const nestedType = await getCustomType(ctId, apiConfig); + const nestedFields: Record = Object.assign({}, ...Object.values(nestedType.json)); + const resolved = await resolveFields(subPaths, nestedFields, ctId, apiConfig, crDepth - 1); + result.push({ id, customtypes: [{ id: ctId, fields: resolved }] }); + + } else { + throw new CommandError(`Field "${id}" is not a group or content relationship field.`); + } + } + + return result; +} + +function validateLeafField(id: string, fields: Record, context: string): void { + if (!(id in fields)) { + throw new CommandError(`Field "${id}" not found on type "${context}".`); + } + if (UNFETCHABLE_FIELD_TYPES.includes(fields[id].type) || id === "uid") { + throw new CommandError(`Field "${id}" cannot be fetched from a content relationship.`); + } + if (fields[id].type === "Group") { + throw new CommandError(`Field "${id}" is a group. Select specific sub-fields with --field ${id}..`); + } +} diff --git a/test/field-add-content-relationship.test.ts b/test/field-add-content-relationship.test.ts index 88d0ff8..1898ad5 100644 --- a/test/field-add-content-relationship.test.ts +++ b/test/field-add-content-relationship.test.ts @@ -58,3 +58,43 @@ it("adds a content relationship field to a custom type", async ({ const field = updated!.json.Main.my_link; expect(field).toMatchObject({ type: "Link", config: { select: "document" } }); }); + +it("adds a content relationship field with --field", async ({ + expect, + prismic, + repo, + token, + host, +}) => { + const target = buildCustomType(); + target.json.Main.title = { type: "Text", config: { label: "Title" } }; + await insertCustomType(target, { repo, token, host }); + + const owner = buildCustomType(); + await insertCustomType(owner, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "add", + "content-relationship", + "my_link", + "--to-type", + owner.id, + "--custom-type", + target.id, + "--field", + "title", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field added: my_link"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === owner.id); + const field = updated!.json.Main.my_link; + expect(field).toMatchObject({ + type: "Link", + config: { + select: "document", + customtypes: [{ id: target.id, fields: ["title"] }], + }, + }); +}); diff --git a/test/field-edit.test.ts b/test/field-edit.test.ts index 8cd0379..a41f237 100644 --- a/test/field-edit.test.ts +++ b/test/field-edit.test.ts @@ -158,6 +158,47 @@ it("edits select field options", async ({ expect, prismic, repo, token, host }) }); }); +it("edits content relationship field with --field", async ({ + expect, + prismic, + repo, + token, + host, +}) => { + const target = buildCustomType({ + json: { Main: { title: { type: "Text", config: { label: "Title" } } } }, + }); + await insertCustomType(target, { repo, token, host }); + + const owner = buildCustomType(); + owner.json.Main.my_link = { + type: "Link", + config: { label: "My Link", select: "document", customtypes: [target.id] }, + }; + await insertCustomType(owner, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "edit", + "my_link", + "--from-type", + owner.id, + "--field", + "title", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field updated: my_link"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === owner.id); + expect(updated!.json.Main.my_link).toMatchObject({ + type: "Link", + config: { + select: "document", + customtypes: [{ id: target.id, fields: ["title"] }], + }, + }); +}); + it("edits link field options", async ({ expect, prismic, repo, token, host }) => { const slice = buildSlice(); slice.variations[0].primary!.cta_link = { From 1903f4b395cc22cb908fa0323025d203fadc1a6b Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 14 Apr 2026 09:55:09 -1000 Subject: [PATCH 23/29] feat: handle type removal when documents exist (#127) Show a clear error message when attempting to remove a type that has associated documents, guiding users to delete the documents first. Closes #99 Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/type-remove.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands/type-remove.ts b/src/commands/type-remove.ts index cc548fe..84257b4 100644 --- a/src/commands/type-remove.ts +++ b/src/commands/type-remove.ts @@ -30,6 +30,11 @@ export default createCommand(config, async ({ positionals, values }) => { } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); + if (message.includes("associated documents")) { + throw new CommandError( + `Type "${id}" has documents. Delete its documents before removing the type.`, + ); + } throw new CommandError(`Failed to remove type: ${message}`); } throw error; From d94447ae924aa37b7a88c14d482fbc65a92e9a93 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 14 Apr 2026 13:10:54 -1000 Subject: [PATCH 24/29] feat: add field reorder command (#129) * feat: add field reorder command Adds `prismic field reorder` to move a field before or after another field. Supports cross-tab moves in custom types (destination tab is determined by the anchor field) and dot-notation for group fields. Resolves #100 Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: simplify test model setup in field-reorder tests Use buildSlice()/buildCustomType() with property assignment instead of verbose override objects. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/field-reorder.ts | 89 +++++++++++++++++++++++ src/commands/field.ts | 5 ++ src/models.ts | 87 ++++++++++++++++++++++ test/field-reorder.test.ts | 132 ++++++++++++++++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 src/commands/field-reorder.ts create mode 100644 test/field-reorder.test.ts diff --git a/src/commands/field-reorder.ts b/src/commands/field-reorder.ts new file mode 100644 index 0000000..926e419 --- /dev/null +++ b/src/commands/field-reorder.ts @@ -0,0 +1,89 @@ +import { getHost, getToken } from "../auth"; +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { resolveFieldPair, resolveFieldTarget } from "../models"; +import { getRepositoryName } from "../project"; + +const config = { + name: "prismic field reorder", + description: "Reorder a field in a slice or custom type.", + positionals: { + id: { description: "Field ID to move", required: true }, + }, + options: { + before: { type: "string", description: "Place field before this field ID" }, + after: { type: "string", description: "Place field after this field ID" }, + "from-slice": { type: "string", description: "ID of the source slice" }, + "from-type": { type: "string", description: "ID of the source content type" }, + variation: { type: "string", description: 'Slice variation ID (default: "default")' }, + repo: { type: "string", short: "r", description: "Repository domain" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [id] = positionals; + const { before, after, repo = await getRepositoryName() } = values; + + if (!before && !after) { + throw new CommandError("Specify --before or --after."); + } + if (before && after) { + throw new CommandError("Only one of --before or --after can be specified."); + } + + const anchor = (before ?? after)!; + const position = before ? "before" : "after"; + + if (id === anchor) { + throw new CommandError(`Cannot reorder "${id}" relative to itself.`); + } + + const idParent = id.lastIndexOf("."); + const anchorParent = anchor.lastIndexOf("."); + if ( + (idParent === -1 ? "" : id.slice(0, idParent)) !== + (anchorParent === -1 ? "" : anchor.slice(0, anchorParent)) + ) { + throw new CommandError( + `Cannot reorder "${id}" relative to "${anchor}": fields must be in the same container.`, + ); + } + + const token = await getToken(); + const host = await getHost(); + const [sourceFields, anchorFields, saveModel] = await resolveFieldPair(id, anchor, values, { + repo, + token, + host, + }); + + const [sourceLeaf, sourceFieldId] = resolveFieldTarget(sourceFields, id); + const [anchorLeaf, anchorFieldId] = resolveFieldTarget(anchorFields, anchor); + + if (!(sourceFieldId in sourceLeaf)) { + throw new CommandError(`Field "${id}" does not exist.`); + } + if (!(anchorFieldId in anchorLeaf)) { + throw new CommandError(`Field "${anchor}" does not exist.`); + } + + const fieldValue = sourceLeaf[sourceFieldId]; + delete sourceLeaf[sourceFieldId]; + + const entries = Object.entries(anchorLeaf); + for (const key of Object.keys(anchorLeaf)) { + delete anchorLeaf[key]; + } + for (const [key, value] of entries) { + if (position === "before" && key === anchorFieldId) { + anchorLeaf[sourceFieldId] = fieldValue; + } + anchorLeaf[key] = value; + if (position === "after" && key === anchorFieldId) { + anchorLeaf[sourceFieldId] = fieldValue; + } + } + + await saveModel(); + + console.info(`Field reordered: ${id}`); +}); diff --git a/src/commands/field.ts b/src/commands/field.ts index 363a042..91a7538 100644 --- a/src/commands/field.ts +++ b/src/commands/field.ts @@ -2,6 +2,7 @@ import { createCommandRouter } from "../lib/command"; import fieldAdd from "./field-add"; import fieldEdit from "./field-edit"; import fieldRemove from "./field-remove"; +import fieldReorder from "./field-reorder"; import fieldView from "./field-view"; export default createCommandRouter({ @@ -20,6 +21,10 @@ export default createCommandRouter({ handler: fieldRemove, description: "Remove a field", }, + reorder: { + handler: fieldReorder, + description: "Reorder a field", + }, view: { handler: fieldView, description: "View details of a field", diff --git a/src/models.ts b/src/models.ts index 4755ced..8c43d0d 100644 --- a/src/models.ts +++ b/src/models.ts @@ -102,6 +102,93 @@ export async function resolveFieldContainer( ]; } +export async function resolveFieldPair( + sourceId: string, + anchorId: string, + values: { + "from-slice"?: string; + "from-type"?: string; + variation?: string; + }, + apiConfig: ApiConfig, +): Promise<[sourceFields: Fields, anchorFields: Fields, save: () => Promise, modelKind: ModelKind]> { + const adapter = await getAdapter(); + const { + "from-slice": fromSlice, + "from-type": fromType, + variation: variationId = "default", + } = values; + + const providedCount = [fromSlice, fromType].filter(Boolean).length; + if (providedCount === 0) { + throw new CommandError("Specify a target with --from-slice or --from-type."); + } + if (providedCount > 1) { + throw new CommandError("Only one of --from-slice or --from-type can be specified."); + } + + if (fromSlice) { + const slice = await getSlice(fromSlice, apiConfig); + const variation = slice.variations.find((v) => v.id === variationId); + if (!variation) { + const variationIds = slice.variations?.map((v) => v.id).join(", ") || "(none)"; + throw new CommandError(`Variation "${variationId}" not found. Available: ${variationIds}`); + } + variation.primary ??= {}; + return [ + variation.primary, + variation.primary, + async () => { + await updateSlice(slice, apiConfig); + try { + await adapter.updateSlice(slice); + } catch { + await adapter.createSlice(slice); + } + await adapter.generateTypes(); + }, + "slice", + ]; + } + + const customType = await getCustomType(fromType!, apiConfig); + + const sourceRoot = sourceId.includes(".") ? sourceId.split(".")[0] : sourceId; + const anchorRoot = anchorId.includes(".") ? anchorId.split(".")[0] : anchorId; + + let sourceTab: Record | undefined; + let anchorTab: Record | undefined; + for (const tabName in customType.json) { + const tab = customType.json[tabName]; + if (sourceRoot in tab) sourceTab = tab; + if (anchorRoot in tab) anchorTab = tab; + } + + const allFieldIds = Object.keys(Object.assign({}, ...Object.values(customType.json))); + const available = allFieldIds.join(", ") || "(none)"; + if (!sourceTab) { + throw new CommandError(`Field "${sourceId}" not found. Available: ${available}`); + } + if (!anchorTab) { + throw new CommandError(`Field "${anchorId}" not found. Available: ${available}`); + } + + return [ + sourceTab, + anchorTab, + async () => { + await updateCustomType(customType, apiConfig); + try { + await adapter.updateCustomType(customType); + } catch { + await adapter.createCustomType(customType); + } + await adapter.generateTypes(); + }, + "customType", + ]; +} + export async function resolveModel( values: { "to-slice"?: string; diff --git a/test/field-reorder.test.ts b/test/field-reorder.test.ts new file mode 100644 index 0000000..d8204b8 --- /dev/null +++ b/test/field-reorder.test.ts @@ -0,0 +1,132 @@ +import type { Group } from "@prismicio/types-internal/lib/customtypes"; + +import { buildCustomType, buildSlice, it } from "./it"; +import { getCustomTypes, getSlices, insertCustomType, insertSlice } from "./prismic"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("field", ["reorder", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic field reorder [options]"); +}); + +it("reorders a field in a slice", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.field_a = { type: "Boolean", config: { label: "A" } }; + slice.variations[0].primary!.field_b = { type: "Boolean", config: { label: "B" } }; + slice.variations[0].primary!.field_c = { type: "Boolean", config: { label: "C" } }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "reorder", + "field_a", + "--after", + "field_c", + "--from-slice", + slice.id, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field reordered: field_a"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + expect(Object.keys(updated!.variations[0].primary!)).toEqual(["field_b", "field_c", "field_a"]); +}); + +it("reorders a field in a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + customType.json.Main.title = { type: "StructuredText", config: { label: "Title" } }; + customType.json.Main.body = { type: "StructuredText", config: { label: "Body" } }; + customType.json.Main.image = { type: "Image", config: { label: "Image" } }; + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "reorder", + "image", + "--before", + "body", + "--from-type", + customType.id, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field reordered: image"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(Object.keys(updated!.json.Main)).toEqual(["title", "image", "body"]); +}); + +it("moves a field across tabs in a custom type", async ({ expect, prismic, repo, token, host }) => { + const customType = buildCustomType(); + customType.json.Main.title = { type: "StructuredText", config: { label: "Title" } }; + customType.json.Main.body = { type: "StructuredText", config: { label: "Body" } }; + customType.json.SEO = { + meta_title: { type: "Text", config: { label: "Meta Title" } }, + meta_desc: { type: "Text", config: { label: "Meta Description" } }, + }; + await insertCustomType(customType, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "reorder", + "body", + "--after", + "meta_title", + "--from-type", + customType.id, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field reordered: body"); + + const customTypes = await getCustomTypes({ repo, token, host }); + const updated = customTypes.find((ct) => ct.id === customType.id); + expect(Object.keys(updated!.json.Main)).toEqual(["title"]); + expect(Object.keys(updated!.json.SEO)).toEqual(["meta_title", "body", "meta_desc"]); +}); + +it("reorders a nested field in a group", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.my_group = { + type: "Group", + config: { + label: "My Group", + fields: { + sub_a: { type: "Boolean", config: { label: "A" } }, + sub_b: { type: "Boolean", config: { label: "B" } }, + sub_c: { type: "Boolean", config: { label: "C" } }, + }, + }, + }; + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("field", [ + "reorder", + "my_group.sub_c", + "--before", + "my_group.sub_a", + "--from-slice", + slice.id, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Field reordered: my_group.sub_c"); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const group = updated!.variations[0].primary!.my_group as Group; + expect(Object.keys(group.config!.fields!)).toEqual(["sub_c", "sub_a", "sub_b"]); +}); + +it("errors when the field does not exist", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + slice.variations[0].primary!.field_a = { type: "Boolean", config: { label: "A" } }; + await insertSlice(slice, { repo, token, host }); + + const { stderr, exitCode } = await prismic("field", [ + "reorder", + "missing", + "--after", + "field_a", + "--from-slice", + slice.id, + ]); + expect(exitCode).toBe(1); + expect(stderr).toContain('"missing"'); +}); From 40f71f04076fffd868184cc6dad0581b91d93827 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Tue, 14 Apr 2026 13:32:49 -1000 Subject: [PATCH 25/29] feat: add `--screenshot` option to `slice add-variation` and `slice edit-variation` (#128) * feat: add `--screenshot` option to `slice add-variation` and `slice edit-variation` Upload a screenshot image (local file or URL) to Prismic's S3 via the ACL provider, storing the resulting imgix URL in the variation's `imageUrl` field. Closes #95 Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address PR review feedback for screenshot upload - Use GET with auth headers (Authorization, Repository) for ACL provider - Match response schema from slice-machine: { values: { url, fields }, imgixEndpoint } - Support PRISMIC_HOST via getAclProviderUrl() helper - Use request() instead of fetch() for S3 upload - Fix buffer pooling issue by slicing to owned ArrayBuffer - Build supported extensions list from MIME_TYPES - Add local file path test Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: use response.blob() and URL for path construction Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: accept Blob in uploadScreenshot and validate file type Separate file resolution (readURLFile) from upload logic so uploadScreenshot only deals with Blobs. Add UnsupportedFileTypeError for MIME type validation, fix getExtension edge case, and remove dead imports. Co-Authored-By: Claude Opus 4.6 (1M context) * test: add local screenshot file test for slice edit-variation Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address PR review feedback for screenshot upload Rename MIME_TYPE_EXTENSIONS to SUPPORTED_IMAGE_MIME_TYPES to clarify intent. Use regex check for HTTP URLs instead of URL.canParse to avoid misidentifying Windows absolute paths as URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/clients/custom-types.ts | 79 ++++++++++++++++++++++++++++ src/commands/slice-add-variation.ts | 36 +++++++++++-- src/commands/slice-edit-variation.ts | 41 ++++++++++++--- src/lib/file.ts | 33 +++++++++++- src/lib/url.ts | 6 +++ test/slice-add-variation.test.ts | 68 ++++++++++++++++++++++++ test/slice-edit-variation.test.ts | 62 ++++++++++++++++++++++ 7 files changed, 313 insertions(+), 12 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index 367f01a..6dacb74 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -1,6 +1,10 @@ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; +import { createHash } from "node:crypto"; +import * as z from "zod/mini"; + import { NotFoundRequestError, request } from "../lib/request"; +import { appendTrailingSlash } from "../lib/url"; export async function getCustomTypes(config: { repo: string; @@ -164,6 +168,81 @@ export async function removeSlice( }); } +const AclCreateResponseSchema = z.object({ + values: z.object({ + url: z.string(), + fields: z.record(z.string(), z.string()), + }), + imgixEndpoint: z.string(), +}); + +const SUPPORTED_IMAGE_MIME_TYPES: Record = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/gif": ".gif", + "image/webp": ".webp", +}; + +export async function uploadScreenshot( + blob: Blob, + config: { + sliceId: string; + variationId: string; + repo: string; + token: string | undefined; + host: string; + }, +): Promise { + const { sliceId, variationId, repo, token, host } = config; + + const type = blob.type; + if (!(type in SUPPORTED_IMAGE_MIME_TYPES)) { + throw new UnsupportedFileTypeError(type); + } + + const aclUrl = new URL("create", getAclProviderUrl(host)); + const acl = await request(aclUrl, { + headers: { Repository: repo, Authorization: `Bearer ${token}` }, + schema: AclCreateResponseSchema, + }); + + const extension = SUPPORTED_IMAGE_MIME_TYPES[type]; + const digest = createHash("md5") + .update(new Uint8Array(await blob.arrayBuffer())) + .digest("hex"); + const key = `${repo}/shared-slices/${sliceId}/${variationId}/${digest}${extension}`; + + const formData = new FormData(); + for (const [field, value] of Object.entries(acl.values.fields)) { + formData.append(field, value); + } + formData.append("key", key); + formData.append("Content-Type", type); + formData.append("file", blob); + + await request(acl.values.url, { method: "POST", body: formData }); + + const url = new URL(key, appendTrailingSlash(acl.imgixEndpoint)); + url.searchParams.set("auto", "compress,format"); + + return url; +} + +export class UnsupportedFileTypeError extends Error { + name = "UnsupportedFileTypeError"; + + constructor(mimeType: string) { + const supportedTypes = Object.keys(SUPPORTED_IMAGE_MIME_TYPES); + super( + `Unsupported file type: ${mimeType || "unknown"}. Supported: ${supportedTypes.join(", ")}`, + ); + } +} + function getCustomTypesServiceUrl(host: string): URL { return new URL(`https://customtypes.${host}/`); } + +function getAclProviderUrl(host: string): URL { + return new URL(`https://acl-provider.${host}/`); +} diff --git a/src/commands/slice-add-variation.ts b/src/commands/slice-add-variation.ts index 200ac1b..e166aaa 100644 --- a/src/commands/slice-add-variation.ts +++ b/src/commands/slice-add-variation.ts @@ -1,11 +1,18 @@ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { camelCase } from "change-case"; +import { pathToFileURL } from "node:url"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlice, updateSlice } from "../clients/custom-types"; +import { + getSlice, + UnsupportedFileTypeError, + updateSlice, + uploadScreenshot, +} from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { readURLFile } from "../lib/file"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -18,13 +25,14 @@ const config = { options: { to: { type: "string", required: true, description: "ID of the slice" }, id: { type: "string", description: "Custom ID for the variation" }, + screenshot: { type: "string", short: "s", description: "Screenshot image file path or URL" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { const [name] = positionals; - const { to, id = camelCase(name), repo = await getRepositoryName() } = values; + const { to, id = camelCase(name), screenshot, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); @@ -35,6 +43,28 @@ export default createCommand(config, async ({ positionals, values }) => { throw new CommandError(`Variation "${id}" already exists in slice "${to}".`); } + let imageUrl = ""; + if (screenshot) { + const url = /^https?:\/\//i.test(screenshot) ? new URL(screenshot) : pathToFileURL(screenshot); + const blob = await readURLFile(url); + let screenshotUrl; + try { + screenshotUrl = await uploadScreenshot(blob, { + sliceId: slice.id, + variationId: id, + repo, + token, + host, + }); + } catch (error) { + if (error instanceof UnsupportedFileTypeError) { + throw new CommandError(error.message); + } + throw error; + } + imageUrl = screenshotUrl.toString(); + } + const updatedSlice: SharedSlice = { ...slice, variations: [ @@ -44,7 +74,7 @@ export default createCommand(config, async ({ positionals, values }) => { name, description: name, docURL: "", - imageUrl: "", + imageUrl, version: "", primary: {}, }, diff --git a/src/commands/slice-edit-variation.ts b/src/commands/slice-edit-variation.ts index ff51101..00d1862 100644 --- a/src/commands/slice-edit-variation.ts +++ b/src/commands/slice-edit-variation.ts @@ -1,9 +1,15 @@ -import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; +import { pathToFileURL } from "node:url"; import { getAdapter } from "../adapters"; import { getHost, getToken } from "../auth"; -import { getSlice, updateSlice } from "../clients/custom-types"; +import { + getSlice, + UnsupportedFileTypeError, + updateSlice, + uploadScreenshot, +} from "../clients/custom-types"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { readURLFile } from "../lib/file"; import { UnknownRequestError } from "../lib/request"; import { getRepositoryName } from "../project"; @@ -16,13 +22,14 @@ const config = { options: { "from-slice": { type: "string", required: true, description: "ID of the slice" }, name: { type: "string", short: "n", description: "New name for the variation" }, + screenshot: { type: "string", short: "s", description: "Screenshot image file path or URL" }, repo: { type: "string", short: "r", description: "Repository domain" }, }, } satisfies CommandConfig; export default createCommand(config, async ({ positionals, values }) => { const [id] = positionals; - const { "from-slice": sliceId, repo = await getRepositoryName() } = values; + const { "from-slice": sliceId, screenshot, repo = await getRepositoryName() } = values; const adapter = await getAdapter(); const token = await getToken(); @@ -30,17 +37,35 @@ export default createCommand(config, async ({ positionals, values }) => { const slice = await getSlice(sliceId, { repo, token, host }); const variation = slice.variations.find((v) => v.id === id); - if (!variation) { throw new CommandError(`Variation "${id}" not found in slice "${sliceId}".`); } if ("name" in values) variation.name = values.name!; - const updatedSlice: SharedSlice = { ...slice }; + if (screenshot) { + const url = /^https?:\/\//i.test(screenshot) ? new URL(screenshot) : pathToFileURL(screenshot); + const blob = await readURLFile(url); + let screenshotUrl; + try { + screenshotUrl = await uploadScreenshot(blob, { + sliceId: slice.id, + variationId: variation.id, + repo, + token, + host, + }); + } catch (error) { + if (error instanceof UnsupportedFileTypeError) { + throw new CommandError(error.message); + } + throw error; + } + variation.imageUrl = screenshotUrl.toString(); + } try { - await updateSlice(updatedSlice, { repo, host, token }); + await updateSlice(slice, { repo, host, token }); } catch (error) { if (error instanceof UnknownRequestError) { const message = await error.text(); @@ -50,9 +75,9 @@ export default createCommand(config, async ({ positionals, values }) => { } try { - await adapter.updateSlice(updatedSlice); + await adapter.updateSlice(slice); } catch { - await adapter.createSlice(updatedSlice); + await adapter.createSlice(slice); } await adapter.generateTypes(); diff --git a/src/lib/file.ts b/src/lib/file.ts index 4a93644..bf64473 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -2,7 +2,7 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import * as z from "zod/mini"; -import { appendTrailingSlash } from "./url"; +import { appendTrailingSlash, getExtension } from "./url"; export async function findUpward( name: string, @@ -68,3 +68,34 @@ export async function readJsonFile( if (schema) return z.parse(schema, json); return json; } + +const MIME_TYPES: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", +}; + +export async function readURLFile(url: URL): Promise { + if (url.protocol === "http:" || url.protocol === "https:") { + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `Failed to download file from "${url.toString()}" (HTTP ${response.status}).`, + ); + } + return await response.blob(); + } + + if (url.protocol === "file:") { + const buffer = await readFile(url); + const extension = getExtension(url); + const type = extension + ? MIME_TYPES[extension] || "application/octet-stream" + : "application/octet-stream"; + return new Blob([buffer], { type }); + } + + throw new Error(`Unsupported file protocol: ${url.protocol}`); +} diff --git a/src/lib/url.ts b/src/lib/url.ts index 32b8d33..2013cdb 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -10,3 +10,9 @@ export function appendTrailingSlash(url: string | URL): URL { export function relativePathname(a: URL, b: URL): string { return relative(fileURLToPath(a), fileURLToPath(b)); } + +export function getExtension(url: URL): string | undefined { + const dotIndex = url.pathname.lastIndexOf("."); + if (dotIndex === -1) return undefined; + return url.pathname.slice(dotIndex + 1).toLowerCase() || undefined; +} diff --git a/test/slice-add-variation.test.ts b/test/slice-add-variation.test.ts index f32add7..03af00b 100644 --- a/test/slice-add-variation.test.ts +++ b/test/slice-add-variation.test.ts @@ -1,3 +1,6 @@ +import { writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + import { buildSlice, it } from "./it"; import { getSlices, insertSlice } from "./prismic"; @@ -28,3 +31,68 @@ it("adds a variation to a slice", async ({ expect, prismic, repo, token, host }) const variation = updated?.variations.find((v) => v.name === variationName); expect(variation).toBeDefined(); }); + +it("adds a variation with a screenshot URL", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const variationName = `Variation${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", [ + "add-variation", + variationName, + "--to", + slice.id, + "--screenshot", + "https://images.prismic.io/slice-machine/621a5ec4-0387-4bc5-9860-2dd46cbc07cd_default_ss.png", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Added variation "${variationName}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.name === variationName); + expect(variation).toBeDefined(); + expect(variation?.imageUrl).toContain("https://"); + expect(variation?.imageUrl).toContain(".png"); +}); + +it("adds a variation with a local screenshot file", async ({ + expect, + prismic, + project, + repo, + token, + host, +}) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const screenshotUrl = + "https://images.prismic.io/slice-machine/621a5ec4-0387-4bc5-9860-2dd46cbc07cd_default_ss.png"; + const response = await fetch(screenshotUrl); + const data = new Uint8Array(await response.arrayBuffer()); + const screenshotFileUrl = new URL("screenshot.png", project); + await writeFile(screenshotFileUrl, data); + const screenshotPath = fileURLToPath(screenshotFileUrl); + + const variationName = `Variation${crypto.randomUUID().split("-")[0]}`; + + const { stdout, exitCode } = await prismic("slice", [ + "add-variation", + variationName, + "--to", + slice.id, + "--screenshot", + screenshotPath, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Added variation "${variationName}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.name === variationName); + expect(variation).toBeDefined(); + expect(variation?.imageUrl).toContain("https://"); + expect(variation?.imageUrl).toContain(".png"); +}); diff --git a/test/slice-edit-variation.test.ts b/test/slice-edit-variation.test.ts index dfe0f41..7129a2d 100644 --- a/test/slice-edit-variation.test.ts +++ b/test/slice-edit-variation.test.ts @@ -1,3 +1,6 @@ +import { writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + import { buildSlice, it } from "./it"; import { getSlices, insertSlice } from "./prismic"; @@ -46,3 +49,62 @@ it("edits a variation name", async ({ expect, prismic, repo, token, host }) => { const variation = updated?.variations.find((v) => v.id === variationId); expect(variation?.name).toBe(newName); }); + +it("sets a screenshot on a variation", async ({ expect, prismic, repo, token, host }) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const { stdout, exitCode } = await prismic("slice", [ + "edit-variation", + "default", + "--from-slice", + slice.id, + "--screenshot", + "https://images.prismic.io/slice-machine/621a5ec4-0387-4bc5-9860-2dd46cbc07cd_default_ss.png", + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Variation updated: "default" in slice "${slice.id}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.id === "default"); + expect(variation?.imageUrl).toContain("https://"); + expect(variation?.imageUrl).toContain(".png"); +}); + +it("sets a local screenshot file on a variation", async ({ + expect, + prismic, + project, + repo, + token, + host, +}) => { + const slice = buildSlice(); + await insertSlice(slice, { repo, token, host }); + + const screenshotUrl = + "https://images.prismic.io/slice-machine/621a5ec4-0387-4bc5-9860-2dd46cbc07cd_default_ss.png"; + const response = await fetch(screenshotUrl); + const data = new Uint8Array(await response.arrayBuffer()); + const screenshotFileUrl = new URL("screenshot.png", project); + await writeFile(screenshotFileUrl, data); + const screenshotPath = fileURLToPath(screenshotFileUrl); + + const { stdout, exitCode } = await prismic("slice", [ + "edit-variation", + "default", + "--from-slice", + slice.id, + "--screenshot", + screenshotPath, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Variation updated: "default" in slice "${slice.id}"`); + + const slices = await getSlices({ repo, token, host }); + const updated = slices.find((s) => s.id === slice.id); + const variation = updated?.variations.find((v) => v.id === "default"); + expect(variation?.imageUrl).toContain("https://"); + expect(variation?.imageUrl).toContain(".png"); +}); From a493a7f743151f9ef9f12372a2aec4ad933ad9ea Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Wed, 15 Apr 2026 01:09:11 +0000 Subject: [PATCH 26/29] fix: misc fixes for remote modeling branch - Remove unnecessary type parameters from request calls - Fix field-edit to reference field.config instead of config - Fix sync watch to generate types once after both syncs - Use getCustomType in type-view instead of filtering list - Fix error message to show variationId instead of variation object Co-Authored-By: Claude Opus 4.6 (1M context) --- src/clients/custom-types.ts | 12 ++++++------ src/commands/field-edit.ts | 4 ++-- src/commands/sync.ts | 6 ++++-- src/commands/type-view.ts | 11 +++-------- src/models.ts | 2 +- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/clients/custom-types.ts b/src/clients/custom-types.ts index 6dacb74..f26436a 100644 --- a/src/clients/custom-types.ts +++ b/src/clients/custom-types.ts @@ -53,7 +53,7 @@ export async function insertCustomType( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL("customtypes/insert", customTypesServiceUrl); - await request(url, { + await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, @@ -67,7 +67,7 @@ export async function updateCustomType( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL("customtypes/update", customTypesServiceUrl); - await request(url, { + await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, @@ -81,7 +81,7 @@ export async function removeCustomType( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL(`customtypes/${encodeURIComponent(id)}`, customTypesServiceUrl); - await request(url, { + await request(url, { method: "DELETE", headers: { repository: repo, Authorization: `Bearer ${token}` }, }); @@ -134,7 +134,7 @@ export async function insertSlice( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL("slices/insert", customTypesServiceUrl); - await request(url, { + await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, @@ -148,7 +148,7 @@ export async function updateSlice( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL("slices/update", customTypesServiceUrl); - await request(url, { + await request(url, { method: "POST", headers: { repository: repo, Authorization: `Bearer ${token}` }, body: model, @@ -162,7 +162,7 @@ export async function removeSlice( const { repo, token, host } = config; const customTypesServiceUrl = getCustomTypesServiceUrl(host); const url = new URL(`slices/${encodeURIComponent(id)}`, customTypesServiceUrl); - await request(url, { + await request(url, { method: "DELETE", headers: { repository: repo, Authorization: `Bearer ${token}` }, }); diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index e37e57c..373dbea 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -144,8 +144,8 @@ export default createCommand(config, async ({ positionals, values }) => { field.config.single = allowList; } else if ("allow" in values) { // Update whichever mode is currently set - if ("single" in config) { - config.single = values.allow; + if ("single" in field.config) { + field.config.single = values.allow; } else { field.config.multi = values.allow; } diff --git a/src/commands/sync.ts b/src/commands/sync.ts index adabf9d..74266ec 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -103,16 +103,18 @@ async function watchForChanges(repo: string, adapter: Adapter) { const changed = []; if (slicesChanged) { - await adapter.syncSlices({ repo, token, host }); + await adapter.syncSlices({ repo, token, host, generateTypes: false }); lastRemoteSlicesHash = remoteSlicesHash; changed.push("slices"); } if (customTypesChanged) { - await adapter.syncCustomTypes({ repo, token, host }); + await adapter.syncCustomTypes({ repo, token, host, generateTypes: false }); lastRemoteCustomTypesHash = remoteCustomTypesHash; changed.push("custom types"); } + await adapter.generateTypes(); + const timestamp = new Date().toLocaleTimeString(); console.info(`[${timestamp}] Changes detected in ${changed.join(" and ")}`); } diff --git a/src/commands/type-view.ts b/src/commands/type-view.ts index 2772849..f9787d1 100644 --- a/src/commands/type-view.ts +++ b/src/commands/type-view.ts @@ -1,6 +1,6 @@ import { getHost, getToken } from "../auth"; -import { getCustomTypes } from "../clients/custom-types"; -import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { getCustomType } from "../clients/custom-types"; +import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { formatTable } from "../lib/string"; import { getRepositoryName } from "../project"; @@ -23,12 +23,7 @@ export default createCommand(config, async ({ positionals, values }) => { const token = await getToken(); const host = await getHost(); - const customTypes = await getCustomTypes({ repo, token, host }); - const type = customTypes.find((ct) => ct.id === id); - - if (!type) { - throw new CommandError(`Type not found: ${id}`); - } + const type = await getCustomType(id, { repo, token, host }); if (json) { console.info(stringify(type)); diff --git a/src/models.ts b/src/models.ts index 8c43d0d..1b70b50 100644 --- a/src/models.ts +++ b/src/models.ts @@ -58,7 +58,7 @@ export async function resolveFieldContainer( const variation = slice.variations.find((v) => v.id === variationId); if (!variation) { const variationIds = slice.variations?.map((v) => v.id).join(", ") || "(none)"; - throw new CommandError(`Variation "${variation}" not found. Available: ${variationIds}`); + throw new CommandError(`Variation "${variationId}" not found. Available: ${variationIds}`); } variation.primary ??= {}; resolveFieldTarget(variation.primary, id); From 400e166076105a727af45e1e83d8f0b36add78b6 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Wed, 15 Apr 2026 01:21:07 +0000 Subject: [PATCH 27/29] fix: resolve nested field lookup and missing existence check - Use root field ID for custom type tab lookup in resolveFieldContainer so dot-separated IDs (e.g. "my_group.subtitle") find the correct tab - Add field existence check in field-edit to throw a clean CommandError instead of a TypeError when the field doesn't exist Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/field-edit.ts | 3 +++ src/models.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index 373dbea..138b6a2 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -93,6 +93,9 @@ export default createCommand(config, async ({ positionals, values }) => { const [targetFields, fieldId] = resolveFieldTarget(fields, id); const field = targetFields[fieldId]; + if (!field) { + throw new CommandError(`Field "${id}" does not exist.`); + } field.config ??= {}; if ("label" in values) field.config.label = values.label; diff --git a/src/models.ts b/src/models.ts index 1b70b50..f888944 100644 --- a/src/models.ts +++ b/src/models.ts @@ -78,9 +78,10 @@ export async function resolveFieldContainer( } const customType = await getCustomType(fromType!, apiConfig); + const root = id.includes(".") ? id.split(".")[0] : id; let tab: Record | undefined; for (const tabName in customType.json) { - if (id in customType.json[tabName]) tab = customType.json[tabName]; + if (root in customType.json[tabName]) tab = customType.json[tabName]; } if (!tab) { const fieldIds = Object.keys(Object.assign({}, ...Object.values(customType.json))) || "(none)"; From 0fbc9c3cc95c07d297ce0b78718bba12f9088663 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Wed, 15 Apr 2026 01:31:55 +0000 Subject: [PATCH 28/29] fix: join field IDs before fallback check in error message Co-Authored-By: Claude Opus 4.6 (1M context) --- src/models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models.ts b/src/models.ts index f888944..06e7230 100644 --- a/src/models.ts +++ b/src/models.ts @@ -84,7 +84,7 @@ export async function resolveFieldContainer( if (root in customType.json[tabName]) tab = customType.json[tabName]; } if (!tab) { - const fieldIds = Object.keys(Object.assign({}, ...Object.values(customType.json))) || "(none)"; + const fieldIds = Object.keys(Object.assign({}, ...Object.values(customType.json))).join(", ") || "(none)"; throw new CommandError(`Field "${id}" not found. Available: ${fieldIds}`); } resolveFieldTarget(tab, id); From 1e28f9df8e5c0de5a20814e582276586f06b1e78 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Wed, 15 Apr 2026 01:59:05 +0000 Subject: [PATCH 29/29] fix: allow adding placeholder to fields that lack one and remove unused --tab from SOURCE_OPTIONS Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/field-edit.ts | 4 ++-- src/models.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/field-edit.ts b/src/commands/field-edit.ts index 138b6a2..278fd2e 100644 --- a/src/commands/field-edit.ts +++ b/src/commands/field-edit.ts @@ -99,8 +99,8 @@ export default createCommand(config, async ({ positionals, values }) => { field.config ??= {}; if ("label" in values) field.config.label = values.label; - if ("placeholder" in values && "placeholder" in field.config) - field.config.placeholder = values.placeholder; + if ("placeholder" in values) + (field.config as Record).placeholder = values.placeholder; switch (field.type) { case "Boolean": { diff --git a/src/models.ts b/src/models.ts index 06e7230..3369dc6 100644 --- a/src/models.ts +++ b/src/models.ts @@ -25,7 +25,6 @@ export const SOURCE_OPTIONS = { "from-slice": { type: "string", description: "ID of the source slice" }, "from-type": { type: "string", description: "ID of the source content type" }, variation: TARGET_OPTIONS.variation, - tab: TARGET_OPTIONS.tab, repo: TARGET_OPTIONS.repo, } satisfies CommandConfig["options"];