From 89f1b8154aca0a64b18a6727f23f47d68ead03ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:25:29 +0100 Subject: [PATCH 1/4] feat(dev): rebuild the release preview on model changes and print its link A website previewing the `prismic dev` release only shows model changes once the release preview is rebuilt, and nothing rebuilt it. After each sync that pushes, pulls or deletes a model, the command now calls `POST /core/releases/{id}/refresh`. That covers local edits and Type Builder edits, which reach the command as pulls. A failed rebuild stays pending and is retried on the next sync. At start it prints a `Preview:` link for the release, built from the repository's first website preview, or how to add one. Co-Authored-By: Claude Opus 5.5 --- src/commands/dev.ts | 64 ++++++++++++++++++++++++++++----- src/lib/prismic/clients/core.ts | 32 +++++++++++++++++ test/dev.test.ts | 1 + 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index f23a2a3b..ea62434b 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -13,7 +13,13 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig, CommandError, exclusiveOptions } from "../lib/command"; import { readJsonFile, watchFiles, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; -import { createRelease, deleteRelease } from "../lib/prismic/clients/core"; +import { + createRelease, + deleteRelease, + getPreviews, + getReleasePreviewURL, + refreshRelease, +} from "../lib/prismic/clients/core"; import type { CustomTypesConfig } from "../lib/prismic/clients/custom-types"; import { canonicalizeCustomType, @@ -41,8 +47,12 @@ const config = { Type Builder changes are saved to local files, and local file changes appear in the Type Builder. + The "Preview:" line opens your website with these models, and the + preview updates as they change. + The command runs until you stop it. Agents should run it in the - background and give the user the URL from its "Type Builder:" line. + background and give the user the URLs from its "Type Builder:" and + "Preview:" lines. @experimental - This command may change or be removed in any release and does not follow semver. `, @@ -86,13 +96,32 @@ export default createCommand(config, async ({ values }) => { session = await createSession({ repo, token, host }); } const release = { repo, token, host, releaseId: session.releaseId }; - const sync = (push = true) => - syncModels(adapter, release, session, sessionPath, { + // The release preview only shows model changes once rebuilt. A failed rebuild stays pending and is retried on the next sync. + let needsRefresh = true; + const refresh = async () => { + if (!needsRefresh) return; + await refreshRelease(release.releaseId, release); + needsRefresh = false; + }; + const sync = async (push = true) => { + const models = await syncModels(adapter, release, session, sessionPath, { push, - onPull: (ids) => log(`↓ Pulled ${names(ids)}`), - onPush: (ids) => log(`↑ Pushed ${names(ids)}`), - onDelete: (ids) => log(`− Deleted ${names(ids)}`), + onPull: (ids) => { + needsRefresh = true; + log(`↓ Pulled ${names(ids)}`); + }, + onPush: (ids) => { + needsRefresh = true; + log(`↑ Pushed ${names(ids)}`); + }, + onDelete: (ids) => { + needsRefresh = true; + log(`− Deleted ${names(ids)}`); + }, }); + if (push) await refresh(); + return models; + }; const end = async (): Promise => { try { @@ -116,6 +145,7 @@ export default createCommand(config, async ({ values }) => { const models = continueSession ? await sync() : await seedRelease(adapter, release, session, sessionPath); + await refresh().catch(() => {}); const types = models.customTypes.length; const slices = models.slices.length; console.info( @@ -125,8 +155,11 @@ export default createCommand(config, async ({ values }) => { const url = new URL("builder/types", `https://${repo}.${host}/`); url.searchParams.set("r", release.releaseId); console.info(`Type Builder: ${url}`); + await printPreviewURL(release); if (noBrowser) { - console.info("Open the URL above to start editing. Changes sync both ways while this runs."); + console.info( + "Open the Type Builder URL above to start editing. Changes sync both ways while this runs.", + ); } else { openBrowser(url); console.info("Opened in your browser. Changes sync both ways while this runs."); @@ -184,6 +217,21 @@ async function createSession({ repo, token, host }: CustomTypesConfig): Promise< return { repo, releaseId, pid: process.pid, synced: {} }; } +async function printPreviewURL({ releaseId, ...config }: Release): Promise { + try { + const [preview] = await getPreviews(config); + if (!preview) { + console.info( + "Preview: run `prismic preview add ` to preview your website with these models.", + ); + return; + } + console.info(`Preview: ${await getReleasePreviewURL(releaseId, preview.id, config)}`); + } catch (error) { + console.error(`Could not create a preview link: ${await getErrorMessage(error)}`); + } +} + async function endSession({ releaseId, ...config }: Release, sessionPath: URL): Promise { await deleteRelease(releaseId, config); await rm(sessionPath, { force: true }); diff --git a/src/lib/prismic/clients/core.ts b/src/lib/prismic/clients/core.ts index 8d271f38..598f6d96 100644 --- a/src/lib/prismic/clients/core.ts +++ b/src/lib/prismic/clients/core.ts @@ -146,6 +146,38 @@ export async function deleteRelease(id: string, config: CoreConfig): Promise { + const { repo, host } = config; + const url = new URL( + `core/releases/${encodeURIComponent(id)}/refresh`, + getCoreServiceUrl(repo, host), + ); + await coreServiceRequest(url, config, { + method: "POST", + notFoundMessage: `Release not found: ${id}`, + unknownErrorMessage: "Failed to refresh the release preview", + }); +} + +export async function getReleasePreviewURL( + releaseId: string, + previewId: string, + config: CoreConfig, +): Promise { + const { repo, host } = config; + const url = new URL( + `core/previews/session/releases/${encodeURIComponent(releaseId)}`, + getCoreServiceUrl(repo, host), + ); + url.searchParams.set("previewId", previewId); + const response = await coreServiceRequest(url, config, { + schema: z.looseObject({ preview_url: z.string() }), + notFoundMessage: `Release not found: ${releaseId}`, + unknownErrorMessage: "Failed to create a preview link", + }); + return response.preview_url; +} + function coreServiceRequest( url: URL, config: CoreConfig, diff --git a/test/dev.test.ts b/test/dev.test.ts index fe412c85..c074a181 100644 --- a/test/dev.test.ts +++ b/test/dev.test.ts @@ -187,6 +187,7 @@ describe("with an isolated repository", () => { const { proc, output, releaseId } = await startSession(prismic, expect); expect(output()).toContain(`https://${repo}.${host}/builder/types?r=`); + await expect.poll(output, { timeout: 30_000 }).toContain("Preview:"); const releaseTypeIds = (await getCustomTypes({ repo, token, host, releaseId })).map( (m) => m.id, From 994b655c672b1657f1b24bc991933f683e9afd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:45:58 +0100 Subject: [PATCH 2/4] fix(dev): keep a continued session when the preview rebuild fails The rebuild ran inside `sync`, so with `--continue` a failed refresh at start ended the session and deleted its release. The rebuild now runs next to each loop sync and once at start, where a failure stays pending and is retried. Without a website preview, the hint no longer starts with `Preview:`, so agents only hand over real preview URLs. A new test checks that a pushed model change moves the release preview to a new ref. It passes on hosts with prismicio/wroom#3279 and fails on prismic.io until that ships. Co-Authored-By: Claude Opus 5.5 --- src/commands/dev.ts | 36 +++++++++++++++--------------------- test/dev.test.ts | 36 ++++++++++++++++++++++++++++++++++-- test/prismic.ts | 19 +++++++++++++++++++ 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index ea62434b..372725e9 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -103,25 +103,17 @@ export default createCommand(config, async ({ values }) => { await refreshRelease(release.releaseId, release); needsRefresh = false; }; - const sync = async (push = true) => { - const models = await syncModels(adapter, release, session, sessionPath, { + const changed = (message: (ids: string[]) => string) => (ids: string[]) => { + needsRefresh = true; + log(message(ids)); + }; + const sync = (push = true) => + syncModels(adapter, release, session, sessionPath, { push, - onPull: (ids) => { - needsRefresh = true; - log(`↓ Pulled ${names(ids)}`); - }, - onPush: (ids) => { - needsRefresh = true; - log(`↑ Pushed ${names(ids)}`); - }, - onDelete: (ids) => { - needsRefresh = true; - log(`− Deleted ${names(ids)}`); - }, + onPull: changed((ids) => `↓ Pulled ${names(ids)}`), + onPush: changed((ids) => `↑ Pushed ${names(ids)}`), + onDelete: changed((ids) => `− Deleted ${names(ids)}`), }); - if (push) await refresh(); - return models; - }; const end = async (): Promise => { try { @@ -171,7 +163,11 @@ export default createCommand(config, async ({ values }) => { if (message !== lastError) log(`! ${message}`, console.error); lastError = message; }; - await syncUntilStopped(sync, waitForChange, stopping.signal, { + const syncAndRefresh = async () => { + await sync(); + await refresh(); + }; + await syncUntilStopped(syncAndRefresh, waitForChange, stopping.signal, { onSync: () => { lastError = undefined; }, @@ -221,9 +217,7 @@ async function printPreviewURL({ releaseId, ...config }: Release): Promise try { const [preview] = await getPreviews(config); if (!preview) { - console.info( - "Preview: run `prismic preview add ` to preview your website with these models.", - ); + console.info("To preview your website with these models, run `prismic preview add `."); return; } console.info(`Preview: ${await getReleasePreviewURL(releaseId, preview.id, config)}`); diff --git a/test/dev.test.ts b/test/dev.test.ts index c074a181..8cfb9ea4 100644 --- a/test/dev.test.ts +++ b/test/dev.test.ts @@ -15,7 +15,14 @@ import { writeLocalCustomType, writeLocalSlice, } from "./it"; -import { getCustomTypes, getSlices, insertCustomType, updateCustomType } from "./prismic"; +import { + addPreview, + getCustomTypes, + getReleasePreviewToken, + getSlices, + insertCustomType, + updateCustomType, +} from "./prismic"; it("supports --help", async ({ expect, prismic }) => { const { stdout, stderr, exitCode } = await prismic("dev", ["--help"]); @@ -187,7 +194,7 @@ describe("with an isolated repository", () => { const { proc, output, releaseId } = await startSession(prismic, expect); expect(output()).toContain(`https://${repo}.${host}/builder/types?r=`); - await expect.poll(output, { timeout: 30_000 }).toContain("Preview:"); + await expect.poll(output, { timeout: 30_000 }).toContain("prismic preview add"); const releaseTypeIds = (await getCustomTypes({ repo, token, host, releaseId })).map( (m) => m.id, @@ -218,6 +225,31 @@ describe("with an isolated repository", () => { expect(await readdir(new URL(".config/prismic/dev/", home))).toEqual([]); }, 120_000); + it("rebuilds the release preview when a model changes", async ({ + expect, + prismic, + project, + repo, + token, + host, + }) => { + await addPreview("https://example.com/api/preview", "Test", { repo, token, host }); + const customType = buildCustomType(); + await writeLocalCustomType(project, customType); + + const { output, releaseId } = await startSession(prismic, expect); + await expect + .poll(output, { timeout: 30_000 }) + .toContain("Preview: https://example.com/api/preview?token="); + const before = await getReleasePreviewToken(releaseId, { repo, token, host }); + + await writeLocalCustomType(project, { ...customType, label: "Edited" }); + await expect.poll(output, { timeout: 30_000 }).toContain(`Pushed ${customType.id}`); + await expect + .poll(() => getReleasePreviewToken(releaseId, { repo, token, host }), { timeout: 30_000 }) + .not.toBe(before); + }, 90_000); + it("keeps syncing after a rejected change", async ({ expect, prismic, diff --git a/test/prismic.ts b/test/prismic.ts index 539776ae..7e7fba81 100644 --- a/test/prismic.ts +++ b/test/prismic.ts @@ -296,6 +296,25 @@ export async function addPreview( if (!res.ok) throw new Error(`Failed to add preview: ${res.status} ${await res.text()}`); } +export async function getReleasePreviewToken( + releaseId: string, + config: RepoConfig, +): Promise { + const host = config.host ?? DEFAULT_HOST; + const [preview] = await getPreviews(config); + const url = new URL( + `core/previews/session/releases/${releaseId}`, + `https://${config.repo}.${host}/`, + ); + url.searchParams.set("previewId", preview.id); + const res = await fetch(url, { + headers: { Cookie: `prismic-auth=${config.token}` }, + }); + if (!res.ok) throw new Error(`Failed to get release preview: ${res.status} ${await res.text()}`); + const data = await res.json(); + return new URL(data.preview_url).searchParams.get("token"); +} + export async function getLocales( config: RepoConfig, ): Promise<{ id: string; label: string; customName: string | null; isMaster: boolean }[]> { From 3ca929030fffa55269882f21e17178cc393cf0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:36:03 +0100 Subject: [PATCH 3/4] refactor(dev): stop printing a preview link The editor and toolbar create and refresh release previews, so prismic dev only keeps the release refreshed after each sync. Co-Authored-By: Claude Opus 5.5 --- src/commands/dev.ts | 32 +++----------------------------- src/lib/prismic/clients/core.ts | 19 ------------------- test/dev.test.ts | 4 ---- 3 files changed, 3 insertions(+), 52 deletions(-) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 372725e9..214e7bed 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -13,13 +13,7 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig, CommandError, exclusiveOptions } from "../lib/command"; import { readJsonFile, watchFiles, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; -import { - createRelease, - deleteRelease, - getPreviews, - getReleasePreviewURL, - refreshRelease, -} from "../lib/prismic/clients/core"; +import { createRelease, deleteRelease, refreshRelease } from "../lib/prismic/clients/core"; import type { CustomTypesConfig } from "../lib/prismic/clients/custom-types"; import { canonicalizeCustomType, @@ -47,12 +41,8 @@ const config = { Type Builder changes are saved to local files, and local file changes appear in the Type Builder. - The "Preview:" line opens your website with these models, and the - preview updates as they change. - The command runs until you stop it. Agents should run it in the - background and give the user the URLs from its "Type Builder:" and - "Preview:" lines. + background and give the user the URL from its "Type Builder:" line. @experimental - This command may change or be removed in any release and does not follow semver. `, @@ -147,11 +137,8 @@ export default createCommand(config, async ({ values }) => { const url = new URL("builder/types", `https://${repo}.${host}/`); url.searchParams.set("r", release.releaseId); console.info(`Type Builder: ${url}`); - await printPreviewURL(release); if (noBrowser) { - console.info( - "Open the Type Builder URL above to start editing. Changes sync both ways while this runs.", - ); + console.info("Open the URL above to start editing. Changes sync both ways while this runs."); } else { openBrowser(url); console.info("Opened in your browser. Changes sync both ways while this runs."); @@ -213,19 +200,6 @@ async function createSession({ repo, token, host }: CustomTypesConfig): Promise< return { repo, releaseId, pid: process.pid, synced: {} }; } -async function printPreviewURL({ releaseId, ...config }: Release): Promise { - try { - const [preview] = await getPreviews(config); - if (!preview) { - console.info("To preview your website with these models, run `prismic preview add `."); - return; - } - console.info(`Preview: ${await getReleasePreviewURL(releaseId, preview.id, config)}`); - } catch (error) { - console.error(`Could not create a preview link: ${await getErrorMessage(error)}`); - } -} - async function endSession({ releaseId, ...config }: Release, sessionPath: URL): Promise { await deleteRelease(releaseId, config); await rm(sessionPath, { force: true }); diff --git a/src/lib/prismic/clients/core.ts b/src/lib/prismic/clients/core.ts index 598f6d96..fc392d63 100644 --- a/src/lib/prismic/clients/core.ts +++ b/src/lib/prismic/clients/core.ts @@ -159,25 +159,6 @@ export async function refreshRelease(id: string, config: CoreConfig): Promise { - const { repo, host } = config; - const url = new URL( - `core/previews/session/releases/${encodeURIComponent(releaseId)}`, - getCoreServiceUrl(repo, host), - ); - url.searchParams.set("previewId", previewId); - const response = await coreServiceRequest(url, config, { - schema: z.looseObject({ preview_url: z.string() }), - notFoundMessage: `Release not found: ${releaseId}`, - unknownErrorMessage: "Failed to create a preview link", - }); - return response.preview_url; -} - function coreServiceRequest( url: URL, config: CoreConfig, diff --git a/test/dev.test.ts b/test/dev.test.ts index 8cfb9ea4..89db1925 100644 --- a/test/dev.test.ts +++ b/test/dev.test.ts @@ -194,7 +194,6 @@ describe("with an isolated repository", () => { const { proc, output, releaseId } = await startSession(prismic, expect); expect(output()).toContain(`https://${repo}.${host}/builder/types?r=`); - await expect.poll(output, { timeout: 30_000 }).toContain("prismic preview add"); const releaseTypeIds = (await getCustomTypes({ repo, token, host, releaseId })).map( (m) => m.id, @@ -238,9 +237,6 @@ describe("with an isolated repository", () => { await writeLocalCustomType(project, customType); const { output, releaseId } = await startSession(prismic, expect); - await expect - .poll(output, { timeout: 30_000 }) - .toContain("Preview: https://example.com/api/preview?token="); const before = await getReleasePreviewToken(releaseId, { repo, token, host }); await writeLocalCustomType(project, { ...customType, label: "Edited" }); From 32d934514286610c378150af9fe78a0d4f9d927c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= <7235666+jomifepe@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:38:43 +0100 Subject: [PATCH 4/4] feat(dev): print the release preview link again With types on releases, documents of a release-only type are written through the MCP, so neither the editor nor the toolbar opens their preview. prismic dev knows the release and keeps it refreshed, so it prints the link. Co-Authored-By: Claude Opus 5.5 --- src/commands/dev.ts | 32 +++++++++++++++++++++++++++++--- src/lib/prismic/clients/core.ts | 19 +++++++++++++++++++ test/dev.test.ts | 4 ++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 214e7bed..372725e9 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -13,7 +13,13 @@ import { openBrowser } from "../lib/browser"; import { createCommand, type CommandConfig, CommandError, exclusiveOptions } from "../lib/command"; import { readJsonFile, watchFiles, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; -import { createRelease, deleteRelease, refreshRelease } from "../lib/prismic/clients/core"; +import { + createRelease, + deleteRelease, + getPreviews, + getReleasePreviewURL, + refreshRelease, +} from "../lib/prismic/clients/core"; import type { CustomTypesConfig } from "../lib/prismic/clients/custom-types"; import { canonicalizeCustomType, @@ -41,8 +47,12 @@ const config = { Type Builder changes are saved to local files, and local file changes appear in the Type Builder. + The "Preview:" line opens your website with these models, and the + preview updates as they change. + The command runs until you stop it. Agents should run it in the - background and give the user the URL from its "Type Builder:" line. + background and give the user the URLs from its "Type Builder:" and + "Preview:" lines. @experimental - This command may change or be removed in any release and does not follow semver. `, @@ -137,8 +147,11 @@ export default createCommand(config, async ({ values }) => { const url = new URL("builder/types", `https://${repo}.${host}/`); url.searchParams.set("r", release.releaseId); console.info(`Type Builder: ${url}`); + await printPreviewURL(release); if (noBrowser) { - console.info("Open the URL above to start editing. Changes sync both ways while this runs."); + console.info( + "Open the Type Builder URL above to start editing. Changes sync both ways while this runs.", + ); } else { openBrowser(url); console.info("Opened in your browser. Changes sync both ways while this runs."); @@ -200,6 +213,19 @@ async function createSession({ repo, token, host }: CustomTypesConfig): Promise< return { repo, releaseId, pid: process.pid, synced: {} }; } +async function printPreviewURL({ releaseId, ...config }: Release): Promise { + try { + const [preview] = await getPreviews(config); + if (!preview) { + console.info("To preview your website with these models, run `prismic preview add `."); + return; + } + console.info(`Preview: ${await getReleasePreviewURL(releaseId, preview.id, config)}`); + } catch (error) { + console.error(`Could not create a preview link: ${await getErrorMessage(error)}`); + } +} + async function endSession({ releaseId, ...config }: Release, sessionPath: URL): Promise { await deleteRelease(releaseId, config); await rm(sessionPath, { force: true }); diff --git a/src/lib/prismic/clients/core.ts b/src/lib/prismic/clients/core.ts index fc392d63..598f6d96 100644 --- a/src/lib/prismic/clients/core.ts +++ b/src/lib/prismic/clients/core.ts @@ -159,6 +159,25 @@ export async function refreshRelease(id: string, config: CoreConfig): Promise { + const { repo, host } = config; + const url = new URL( + `core/previews/session/releases/${encodeURIComponent(releaseId)}`, + getCoreServiceUrl(repo, host), + ); + url.searchParams.set("previewId", previewId); + const response = await coreServiceRequest(url, config, { + schema: z.looseObject({ preview_url: z.string() }), + notFoundMessage: `Release not found: ${releaseId}`, + unknownErrorMessage: "Failed to create a preview link", + }); + return response.preview_url; +} + function coreServiceRequest( url: URL, config: CoreConfig, diff --git a/test/dev.test.ts b/test/dev.test.ts index 258b8f00..4f3225db 100644 --- a/test/dev.test.ts +++ b/test/dev.test.ts @@ -195,6 +195,7 @@ describe("with an isolated repository", () => { const { proc, output, releaseId } = await startSession(prismic, expect); expect(output()).toContain(`https://${repo}.${host}/builder/types?r=`); + await expect.poll(output, { timeout: 30_000 }).toContain("prismic preview add"); const releaseTypeIds = (await getCustomTypes({ repo, token, host, releaseId })).map( (m) => m.id, @@ -238,6 +239,9 @@ describe("with an isolated repository", () => { await writeLocalCustomType(project, customType); const { output, releaseId } = await startSession(prismic, expect); + await expect + .poll(output, { timeout: 30_000 }) + .toContain("Preview: https://example.com/api/preview?token="); const before = await getReleasePreviewToken(releaseId, { repo, token, host }); await writeLocalCustomType(project, { ...customType, label: "Edited" });