diff --git a/.env.test.example b/.env.test.example index a6b9a54..337dfcf 100644 --- a/.env.test.example +++ b/.env.test.example @@ -7,3 +7,7 @@ E2E_PRISMIC_EMAIL= # The password to your Prismic account. E2E_PRISMIC_PASSWORD= + +# The Anthropic API key used to run the AI evals (`node --run evals`). Billed to +# the API, not a Claude subscription. In CI this comes from a GitHub secret. +ANTHROPIC_API_KEY= diff --git a/.gitignore b/.gitignore index 31f393c..533d9b7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ coverage *.code-workspace .claude + +# Latest eval run, including filtered ones; results.json only records full runs +evals/results.local.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 628b9c8..0734f58 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,5 +1,5 @@ { "$schema": "./node_modules/oxfmt/configuration_schema.json", "experimentalSortImports": {}, - "ignorePatterns": ["CHANGELOG.md"] + "ignorePatterns": ["CHANGELOG.md", "evals/results*.json"] } diff --git a/evals/check-before-acting.eval.ts b/evals/check-before-acting.eval.ts new file mode 100644 index 0000000..e869f14 --- /dev/null +++ b/evals/check-before-acting.eval.ts @@ -0,0 +1,64 @@ +import { + buildCustomType, + readLocalCustomType, + readLocalCustomTypes, + writeLocalCustomType, +} from "../test/it"; +import { it, trials } from "./it"; + +it.for(trials)( + "consults the docs before an unfamiliar task", + async (_, { project, agent, expect }) => { + const homepage = buildCustomType({ id: "homepage", label: "Homepage" }); + await writeLocalCustomType(project, homepage); + + const result = await agent( + `Set up content previews for this project so editors can preview drafts.`, + ); + + expect(result).toHaveRun("prismic", ["docs"]); + }, +); + +it.for(trials)( + "does not try to reassign a document to another type", + async (_, { project, agent, expect }) => { + const article = buildCustomType({ id: "article", label: "Article" }); + const post = buildCustomType({ id: "blog_post", label: "Blog Post" }); + await writeLocalCustomType(project, article); + await writeLocalCustomType(project, post); + + const result = await agent( + `Change the "getting-started" document from the "article" type to "blog_post".`, + ); + + expect(result).not.toHaveRun("prismic", ["type", "remove"]); + const models = await readLocalCustomTypes(project); + expect(models.find((model) => model.id === article.id)).toEqual(article); + expect(models.find((model) => model.id === post.id)).toEqual(post); + }, +); + +it.for(trials)( + "does not destructively act on an ambiguous request", + async (_, { project, agent, expect }) => { + const article = buildCustomType({ + id: "article", + label: "Article", + json: { + Main: { + title: { type: "StructuredText", config: { label: "Title", single: "heading1" } }, + body: { type: "StructuredText", config: { label: "Body", multi: "paragraph" } }, + }, + }, + }); + await writeLocalCustomType(project, article); + + const result = await agent(`Clean up the fields on "article".`); + + expect(result).not.toHaveRun("prismic", ["field", "remove"]); + const model = await readLocalCustomType(project, article.id); + expect(model.json.Main.title).toEqual(article.json.Main.title); + expect(model.json.Main.body).toEqual(article.json.Main.body); + }, +); diff --git a/evals/configure-repositories.eval.ts b/evals/configure-repositories.eval.ts new file mode 100644 index 0000000..e738906 --- /dev/null +++ b/evals/configure-repositories.eval.ts @@ -0,0 +1,69 @@ +import { + addPreview, + getAccessTokens, + getLocales, + getPreviews, + getRepository, + getWebhooks, +} from "../test/prismic"; +import { it, trials } from "./it"; + +it.for(trials)("sets up a content preview", async (_, { agent, expect, repo, token, host }) => { + const result = await agent( + `Set up a content preview for this repo pointing at https://example.com/api/preview.`, + ); + + expect(result).toHaveRun("prismic", ["preview", "add"]); + const previews = await getPreviews({ repo, token, host }); + expect(previews.some((preview) => preview.url.includes("example.com"))).toBe(true); +}); + +it.for(trials)( + "updates previews for production after a deploy", + async (_, { agent, expect, repo, token, host }) => { + await addPreview("http://localhost:3000/api/preview", "Development", { repo, token, host }); + + const result = await agent( + `We just deployed the site to https://example.com. Set up content previews for production.`, + ); + + expect(result).toHaveRun("prismic", ["preview", "add"]); + const previews = await getPreviews({ repo, token, host }); + expect(previews.some((preview) => preview.url.includes("example.com"))).toBe(true); + expect(previews.some((preview) => preview.url.includes("localhost:3000"))).toBe(true); + const repository = await getRepository({ repo, token, host }); + expect(repository.simulatorUrl).toContain("example.com"); + }, +); + +it.for(trials)( + "creates an API token and makes the API private", + async (_, { agent, expect, repo, token, host }) => { + const result = await agent( + `Create a content API token named "ci" for this repo and make the content API private.`, + ); + + expect(result).toHaveRun("prismic", ["token", "create"]); + expect(result).toHaveRun("prismic", ["repo", "set-api-access"]); + const apps = await getAccessTokens({ repo, token, host }); + expect(apps.some((app) => app.name === "ci")).toBe(true); + }, +); + +it.for(trials)("registers a webhook", async (_, { agent, expect, repo, token, host }) => { + const result = await agent( + `Register a webhook at https://example.com/api/revalidate that fires when documents are published or unpublished.`, + ); + + expect(result).toHaveRun("prismic", ["webhook", "create"]); + const webhooks = await getWebhooks({ repo, token, host }); + expect(JSON.stringify(webhooks)).toContain("example.com/api/revalidate"); +}); + +it.for(trials)("adds a locale", async (_, { agent, expect, repo, token, host }) => { + const result = await agent(`Add French (France) as a locale for this repo.`); + + expect(result).toHaveRun("prismic", ["locale", "add"]); + const locales = await getLocales({ repo, token, host }); + expect(locales.some((locale) => locale.id === "fr-fr")).toBe(true); +}); diff --git a/evals/configure-routes.eval.ts b/evals/configure-routes.eval.ts new file mode 100644 index 0000000..1f609af --- /dev/null +++ b/evals/configure-routes.eval.ts @@ -0,0 +1,67 @@ +import { readFile } from "node:fs/promises"; + +import { buildCustomType, writeLocalCustomType } from "../test/it"; +import { it, trials } from "./it"; + +it.for(trials)( + "routes a nested URL through a content relationship", + async (_, { project, agent, expect }) => { + const category = buildCustomType({ id: "category", label: "Category" }); + const blogPost = buildCustomType({ + id: "blog_post", + label: "Blog Post", + json: { + Main: { + category: { + type: "Link", + config: { label: "Category", select: "document", customtypes: ["category"] }, + }, + }, + }, + }); + await writeLocalCustomType(project, category); + await writeLocalCustomType(project, blogPost); + + await agent( + `Blog posts should live at /blog//, where is the linked category's UID and is the post's UID.`, + ); + + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + const route = config.routes?.find((route: { type: string }) => route.type === "blog_post"); + expect(route?.path).toBe("/blog/:category/:uid"); + expect(route?.resolvers).toEqual({ category: "category" }); + }, +); + +it.for(trials)( + "routes non-default locales with an optional locale prefix", + async (_, { project, agent, expect }) => { + const page = buildCustomType({ id: "page", label: "Page" }); + await writeLocalCustomType(project, page); + + await agent( + `Pages live at /. Non-default locales should get a locale prefix, like /fr-fr/, while the default locale stays at /.`, + ); + + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + const route = config.routes?.find((route: { type: string }) => route.type === "page"); + expect(route?.path).toBe("/:lang?/:uid"); + }, +); + +it.for(trials)( + "routes the home document to the root URL", + async (_, { project, agent, expect }) => { + const page = buildCustomType({ id: "page", label: "Page" }); + await writeLocalCustomType(project, page); + + await agent(`The "home" page document should be served at /, and every other page at /.`); + + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + const routes: { type: string; path: string; uid?: string }[] = + config.routes?.filter((route: { type: string }) => route.type === "page") ?? []; + const home = routes.find((route) => route.uid === "home"); + expect(home?.path).toBe("/"); + expect(routes.some((route) => route.path === "/:uid" && route.uid === undefined)).toBe(true); + }, +); diff --git a/evals/design-sensible-models.eval.ts b/evals/design-sensible-models.eval.ts new file mode 100644 index 0000000..1e24c8c --- /dev/null +++ b/evals/design-sensible-models.eval.ts @@ -0,0 +1,158 @@ +import dedent from "dedent"; +import { copyFile } from "node:fs/promises"; + +import { + buildCustomType, + readLocalCustomType, + readLocalCustomTypes, + readLocalSlices, + writeLocalCustomType, +} from "../test/it"; +import { it, trials } from "./it"; + +it.for(trials)( + "models a testimonial slice from a text description", + async (_, { project, agent, expect }) => { + const result = await agent( + `Create a testimonial slice: a quote, author name, author role, an avatar, and a company logo.`, + ); + + expect(result).toHaveRun("prismic", ["slice", "create"]); + const slices = await readLocalSlices(project); + expect(slices.length).toBe(1); + await expect(JSON.stringify(slices[0], null, 2)).toSatisfyJudge( + dedent` + This is a Prismic shared slice modeled from: "a testimonial with a quote, author name, author role, an avatar, and a company logo". + Passes if all five are present with sensible types: rich text or key text for the quote, key text for the name and role, and image fields for the avatar and logo. Field names must clearly convey their purpose (e.g. something like "quote", not "text_1"); exact names may vary. + `, + ); + }, +); + +it.for(trials)("models a slice from a screenshot", async (_, { project, agent, expect }) => { + await copyFile( + new URL("feature-slice.png", import.meta.url), + new URL("feature-slice.png", project), + ); + + const result = await agent(`Model a Prismic slice for the section design in feature-slice.png.`); + + expect(result).toHaveRun("prismic", ["slice", "create"]); + const slices = await readLocalSlices(project); + expect(slices.length).toBe(1); + await expect(JSON.stringify(slices[0], null, 2)).toSatisfyJudge( + dedent` + This is a Prismic shared slice modeled from a screenshot of a feature section. The screenshot shows a heading, a description paragraph, two statistic cards (each a large value like "4,800+" with a short label), and a large photo. + Passes if the heading, description, photo, and statistics are all modeled with sensible types and field names that clearly convey their purpose. The statistics may be a repeatable group with value and label fields, or individual field pairs; both are acceptable. + Fails if any of the four is missing or has an implausible type. + `, + ); +}); + +it.for(trials)( + "models a page type with sensible field types", + async (_, { project, agent, expect }) => { + const author = buildCustomType({ id: "author", label: "Author" }); + await writeLocalCustomType(project, author); + + const result = await agent( + `model a Prismic blog post: a title, publish date, hero image, author, and body`, + ); + + expect(result).toHaveRun("prismic", ["type", "create"]); + const models = (await readLocalCustomTypes(project)).filter((model) => model.id !== author.id); + + await expect(JSON.stringify(models, null, 2)).toSatisfyJudge( + dedent` + These are Prismic models for a blog post with a title, publish date, hero image, author, and body. + Passes if the title and body are rich text, the publish date is a date or timestamp, the hero is an image, and the author is a content relationship (a link to another document type), not free text. Field names must clearly convey their purpose; exact names may vary. + `, + ); + }, +); + +it.for(trials)( + "models pages as page types and data as custom types", + async (_, { project, agent, expect }) => { + const result = await agent(`Model a landing page and a global navigation menu.`); + + expect(result).toHaveRun("prismic", ["type", "create"]); + const models = await readLocalCustomTypes(project); + const landingPage = models.find((model) => /landing/.test(model.id)); + const navigation = models.find((model) => /nav/.test(model.id)); + expect(landingPage?.format).toBe("page"); + expect(landingPage?.repeatable).toBe(true); + expect(navigation?.format).not.toBe("page"); + expect(navigation?.repeatable).toBe(false); + }, +); + +it.for(trials)( + "handles a vague design request reasonably", + async (_, { project, agent, expect }) => { + const homepage = buildCustomType({ id: "homepage", label: "Homepage", repeatable: false }); + await writeLocalCustomType(project, homepage); + + const result = await agent(`The homepage needs a flexible hero.`); + + expect(result).toHaveRun("prismic"); + const models = await readLocalCustomTypes(project); + const slices = await readLocalSlices(project); + await expect(JSON.stringify({ models, slices }, null, 2)).toSatisfyJudge( + dedent` + These are Prismic models after an agent was asked for "a flexible hero" on the homepage. + Passes if there is a hero slice (or hero fields on the homepage) with a small sensible set of fields, e.g. heading, description, image, and a call-to-action link. Variations are a plus but not required. + Fails if nothing was modeled, existing structure was deleted, or the result is an implausible pile of fields. + `, + ); + }, +); + +it.for(trials)( + "picks appropriate field types for a rating and a CTA", + async (_, { project, agent, expect }) => { + const product = buildCustomType({ id: "product", label: "Product" }); + await writeLocalCustomType(project, product); + + const result = await agent( + `Add a star rating (1 to 5) and a call-to-action button to the "product" type.`, + ); + + expect(result).toHaveRun("prismic", ["field", "add"]); + const model = await readLocalCustomType(project, product.id); + await expect(JSON.stringify(model, null, 2)).toSatisfyJudge( + dedent` + This is a Prismic "product" model after adding a star rating (1 to 5) and a call-to-action button. + Passes only if both hold: the rating is a number field or a select constrained to the values 1-5 (not free text), and the CTA is a single link field (ideally with display text), not separate text and URL fields. Field names must clearly convey their purpose; exact names may vary. + `, + ); + }, +); + +it.for(trials)( + "models a title as single-heading rich text and a social media handle as key text", + async (_, { project, agent, expect }) => { + const customType = buildCustomType({ id: "blog_post", label: "Blog Post" }); + await writeLocalCustomType(project, customType); + const result = await agent( + `Set up the "blog_post" type: it needs a title and the author's Bluesky handle.`, + ); + expect(result).toHaveRun("prismic", ["field", "add"]); + const model = await readLocalCustomType(project, customType.id); + + await expect(JSON.stringify(model, null, 2)).toSatisfyJudge( + dedent` + This is a Prismic "blog_post" model that needs a title. + By Prismic convention, passes only if the title is rich text (type "StructuredText") limited to a single heading block, with a field name that clearly conveys it is the title. + Fails if the title is key text, or rich text without a single-heading limit. + `, + ); + await expect(JSON.stringify(model, null, 2)).toSatisfyJudge( + dedent` + This is a Prismic "blog_post" model that needs the author's Bluesky handle. + Passes only if the handle is key text (type "Text"): a short single-line string with no formatting, with a field name that clearly conveys it is the Bluesky handle. + Fails if the handle is rich text. + `, + ); + }, +); diff --git a/evals/edit-models-precisely.eval.ts b/evals/edit-models-precisely.eval.ts new file mode 100644 index 0000000..7f9db7d --- /dev/null +++ b/evals/edit-models-precisely.eval.ts @@ -0,0 +1,204 @@ +import { + buildCustomType, + buildSlice, + readLocalCustomType, + readLocalSlice, + writeLocalCustomType, + writeLocalSlice, +} from "../test/it"; +import { it, trials } from "./it"; + +it.for(trials)("adds a field", async (_, { project, agent, expect }) => { + const article = buildCustomType({ + id: "article", + label: "Article", + json: { + Main: { + title: { type: "StructuredText", config: { label: "Title", single: "heading1" } }, + published_at: { type: "Date", config: { label: "Published At" } }, + }, + }, + }); + await writeLocalCustomType(project, article); + + const result = await agent(`Add an "excerpt" rich text field to the "article" type.`); + + expect(result).toHaveRun("prismic", ["field", "add", "rich-text", "excerpt"]); + const model = await readLocalCustomType(project, article.id); + expect(model.json.Main.excerpt.type).toBe("StructuredText"); + expect(model.json.Main.title).toEqual(article.json.Main.title); + expect(model.json.Main.published_at).toEqual(article.json.Main.published_at); +}); + +it.for(trials)( + "adds a slice variation without losing existing ones", + async (_, { project, agent, expect }) => { + const slice = buildSlice({ id: "hero", name: "Hero" }); + slice.variations = [ + ...slice.variations, + { ...slice.variations[0], id: "imageRight", name: "Image Right" }, + ]; + await writeLocalSlice(project, slice); + + const result = await agent(`Add a "centered" variation to the "Hero" slice.`); + + expect(result).toHaveRun("prismic", ["slice", "add-variation"]); + const model = await readLocalSlice(project, slice.id); + const ids = model?.variations.map((variation) => variation.id); + expect(ids).toContain("default"); + expect(ids).toContain("imageRight"); + expect(ids?.some((id) => /centered/i.test(id))).toBe(true); + }, +); + +// The CLI cannot rename a field ID; `field edit` only changes label and config. +it.todo("renames a field without disturbing field order", async ({ project, agent, expect }) => { + const article = buildCustomType({ + id: "article", + label: "Article", + json: { + Main: { + title: { type: "StructuredText", config: { label: "Title", single: "heading1" } }, + tagline: { type: "Text", config: { label: "Tagline" } }, + body: { type: "StructuredText", config: { label: "Body", multi: "paragraph" } }, + }, + }, + }); + await writeLocalCustomType(project, article); + + const result = await agent(`Rename the "tagline" field on "article" to "subtitle".`); + + expect(result).toHaveRun("prismic", ["field", "edit"]); + const model = await readLocalCustomType(project, article.id); + expect(Object.keys(model.json.Main)).toEqual(["title", "subtitle", "body"]); + expect(model.json.Main.subtitle.type).toBe("Text"); +}); + +it.for(trials)( + "extends a slice to match a design change", + async (_, { project, agent, expect }) => { + const slice = buildSlice({ id: "testimonial", name: "Testimonial" }); + slice.variations[0].primary = { + quote: { type: "StructuredText", config: { label: "Quote", multi: "paragraph" } }, + author: { type: "Text", config: { label: "Author" } }, + }; + await writeLocalSlice(project, slice); + + const result = await agent( + `The testimonial design now also shows the author's company logo and a star rating. Update the "Testimonial" slice.`, + ); + + expect(result).toHaveRun("prismic", ["field", "add"]); + const model = await readLocalSlice(project, slice.id); + const primary = model?.variations[0].primary ?? {}; + expect(primary.quote).toEqual(slice.variations[0].primary.quote); + expect(primary.author).toEqual(slice.variations[0].primary.author); + const addedTypes = Object.values(primary).map((field) => field.type); + expect(addedTypes).toContain("Image"); + }, +); + +it.for(trials)( + "is idempotent when the same task runs twice", + async (_, { project, agent, expect }) => { + const homepage = buildCustomType({ id: "homepage", label: "Homepage" }); + await writeLocalCustomType(project, homepage); + + const firstRun = await agent(`add a "body" rich text field to "homepage"`); + await agent(`add a "body" rich text field to "homepage"`); + + expect(firstRun).toHaveRun("prismic", ["field", "add"]); + const model = await readLocalCustomType(project, homepage.id); + const bodyLikeKeys = Object.keys(model.json.Main).filter((key) => /body/i.test(key)); + expect(bodyLikeKeys).toEqual(["body"]); + expect(model.json.Main.body.type).toBe("StructuredText"); + }, +); + +it.for(trials)("adds a group field with nested fields", async (_, { project, agent, expect }) => { + const product = buildCustomType({ id: "product", label: "Product" }); + await writeLocalCustomType(project, product); + + const result = await agent( + `Add a repeatable "features" group to the "product" type. Each feature has an icon image and a label.`, + ); + + expect(result).toHaveRun("prismic", ["field", "add", "group"]); + const model = await readLocalCustomType(project, product.id); + const features = model.json.Main.features; + expect(features.type).toBe("Group"); + const nestedTypes = Object.values( + (features.config as { fields: Record }).fields, + ).map((field) => field.type); + expect(nestedTypes).toContain("Image"); + expect(nestedTypes).toContain("Text"); +}); + +it.for(trials)( + "adds constrained fields with the right config", + async (_, { project, agent, expect }) => { + const author = buildCustomType({ id: "author", label: "Author" }); + const product = buildCustomType({ id: "product", label: "Product" }); + await writeLocalCustomType(project, author); + await writeLocalCustomType(project, product); + + const result = await agent( + `On the "product" type, add a "size" dropdown with the options S, M, and L, and an "author" link that can only point to "author" documents.`, + ); + + expect(result).toHaveRun("prismic", ["field", "add", "select"]); + expect(result).toHaveRun("prismic", ["field", "add", "content-relationship"]); + const model = await readLocalCustomType(project, product.id); + expect(model.json.Main.size.type).toBe("Select"); + expect((model.json.Main.size.config as { options: string[] }).options).toEqual(["S", "M", "L"]); + expect(model.json.Main.author.type).toBe("Link"); + expect(JSON.stringify(model.json.Main.author.config)).toContain("author"); + }, +); + +it.for(trials)( + "uses the documented config for a single-heading constraint", + async (_, { project, agent, expect }) => { + const post = buildCustomType({ + id: "post", + label: "Post", + json: { + Main: { + title: { + type: "StructuredText", + config: { label: "Title", multi: "heading1,heading2,paragraph" }, + }, + }, + }, + }); + await writeLocalCustomType(project, post); + + const result = await agent( + `Restrict the "title" field on "post" so editors can only write a single H1.`, + ); + + expect(result).toHaveRun("prismic", ["field", "edit"]); + const model = await readLocalCustomType(project, post.id); + const config = model.json.Main.title.config as { single?: string; multi?: string }; + expect(config.single).toBe("heading1"); + expect(config.multi).toBeUndefined(); + }, +); + +it.for(trials)("connects a slice to a page type", async (_, { project, agent, expect }) => { + const page = buildCustomType({ + id: "page", + label: "Page", + json: { Main: { slices: { type: "Slices", fieldset: "Slice Zone", config: { choices: {} } } } }, + }); + await writeLocalCustomType(project, page); + const slice = buildSlice({ id: "testimonial", name: "Testimonial" }); + await writeLocalSlice(project, slice); + + const result = await agent(`Make the "Testimonial" slice available on the "page" type.`); + + expect(result).toHaveRun("prismic", ["slice", "connect"]); + const model = await readLocalCustomType(project, page.id); + const choices = (model.json.Main.slices.config as { choices: Record }).choices; + expect(Object.keys(choices)).toContain(slice.id); +}); diff --git a/evals/feature-slice.png b/evals/feature-slice.png new file mode 100644 index 0000000..f79a421 Binary files /dev/null and b/evals/feature-slice.png differ diff --git a/evals/initialize-projects.eval.ts b/evals/initialize-projects.eval.ts new file mode 100644 index 0000000..e86f448 --- /dev/null +++ b/evals/initialize-projects.eval.ts @@ -0,0 +1,48 @@ +import { readFile, rm, writeFile } from "node:fs/promises"; + +import { it, trials } from "./it"; + +it.for(trials)( + "initializes Prismic in a Next.js project", + async (_, { project, agent, expect }) => { + await rm(new URL("prismic.config.json", project)); + + const result = await agent(`Set up Prismic in this Next.js project.`); + + expect(result).toHaveRun("prismic", ["init"]); + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + expect(config.repositoryName).toBeTruthy(); + }, +); + +it.for(trials)( + "adds Prismic to an existing Next.js app without clobbering it", + async (_, { project, agent, expect }) => { + await rm(new URL("prismic.config.json", project)); + const existingPage = `export default function Home() {\n\treturn
KEEP-ME
;\n}\n`; + await writeFile(new URL("app/page.tsx", project), existingPage); + + const result = await agent(`Add Prismic to this existing Next.js app.`); + + expect(result).toHaveRun("prismic", ["init"]); + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + expect(config.repositoryName).toBeTruthy(); + const page = await readFile(new URL("app/page.tsx", project), "utf8"); + expect(page).toContain("KEEP-ME"); + }, +); + +it.for(trials)( + "initializes with an existing repository", + async (_, { project, agent, expect, repo }) => { + await rm(new URL("prismic.config.json", project)); + + const result = await agent( + `Set up Prismic in this project using the existing "${repo}" Prismic repository.`, + ); + + expect(result).toHaveRun("prismic", ["init"]); + const config = JSON.parse(await readFile(new URL("prismic.config.json", project), "utf8")); + expect(config.repositoryName).toBe(repo); + }, +); diff --git a/evals/it.ts b/evals/it.ts new file mode 100644 index 0000000..bdf5fb5 --- /dev/null +++ b/evals/it.ts @@ -0,0 +1,266 @@ +import { query, type SDKResultMessage } from "@anthropic-ai/claude-agent-sdk"; +import dedent from "dedent"; +import { copyFile, mkdir, mkdtemp, readFile, symlink, writeFile } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect } from "vitest"; + +import type { Trial } from "./reporter"; + +import { it as base } from "../test/it"; +import { deleteRepository } from "../test/prismic"; + +if (process.env.PRISMIC_ALLOW_EVALS !== "true") { + throw new Error( + "Refusing to run evals outside an isolated environment. They run an agent with " + + "--dangerously-skip-permissions against a real account. Set PRISMIC_ALLOW_EVALS=true " + + "only inside a container or disposable VM.", + ); +} + +const BIN = new URL("../dist/index.mjs", import.meta.url); +const EVAL_MODEL = process.env.EVAL_MODEL ?? "claude-sonnet-5"; +const EVAL_TRIALS = Number(process.env.EVAL_TRIALS ?? 3); +const JUDGE_MODEL = "claude-sonnet-5"; +const PRISMIC_SKILL_REF = "2bd340e6af4e67a9c1179e97b495f7bda564b46f"; + +const SKILL = await fetchSkill(); + +export const trials = Array.from({ length: EVAL_TRIALS }, (_, i) => i + 1); + +declare module "vitest" { + interface TaskMeta { + agent?: Trial; + } + // oxlint-disable-next-line no-explicit-any + interface Matchers { + toHaveRun(bin: string, positionals?: string[]): T; + toSatisfyJudge(criterion: string): Promise; + } +} + +export const it = base.extend<{ + isolateRepo: boolean; + agent: (prompt: string) => Promise; +}>({ + isolateRepo: true, + agent: async ({ home, project, login, task, repo, token, host, password }, use) => { + await login(); + + const claudeConfigDir = await createClaudeConfigDir(); + + const nodeModulesBinDir = new URL("node_modules/.bin/", project); + await mkdir(nodeModulesBinDir, { recursive: true }); + await symlink(BIN, new URL("prismic", nodeModulesBinDir)); + + const env = { + ...process.env, + HOME: fileURLToPath(home), + PRISMIC_CONFIG_DIR: fileURLToPath(new URL(".config/prismic/", home)), + PRISMIC_TYPE_BUILDER_ENABLED: "true", + PRISMIC_SENTRY_ENABLED: "false", + PRISMIC_TELEMETRY_ENABLED: "false", + NO_UPDATE_NOTIFIER: "1", + CLAUDE_CONFIG_DIR: claudeConfigDir, + CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1", + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", + }; + + const trial: Trial = { model: EVAL_MODEL, costUsd: 0, durationS: 0, calls: [] }; + task.meta.agent = trial; + let durationMs = 0; + + await use(async (prompt: string) => { + const result = await agent(prompt, { + systemPromptAppend: SKILL, + cwd: project, + env, + // Recorded as commands stream so a timed-out trial keeps its trail. + onCommand: (command) => { + if (/(^|\s)(npx\s+)?prismic(@|\s|$)/.test(command)) { + trial.calls.push(command.replace(/^.*?(^|\s)(npx\s+)?prismic(@\S+)?\s+/, "")); + } + }, + }); + + const run = result.result; + trial.costUsd += run.total_cost_usd; + durationMs += run.duration_ms; + trial.durationS = Math.round(durationMs / 1000); + + return result; + }); + + try { + const configFile = await readFile(new URL("prismic.config.json", project), "utf8"); + const created = JSON.parse(configFile).repositoryName; + if (created && created !== repo && password) { + await deleteRepository(created, { token, password, host }); + } + } catch {} + }, +}); + +expect.extend({ + toHaveRun(result: AgentResult, bin: string, positionals: string[] = []) { + const pass = result.commands.some((command) => { + return command.split(/&&|\|\||;|\||\n/).some((segment) => { + const words = segment.split(/\s+/).filter(Boolean); + if (words.includes("--help") || words.includes("-h")) return false; + const start = words.findIndex((word) => new RegExp(`^${bin}@?`).test(word)); + if (start === -1) return false; + + const got = words.slice(start + 1).filter((w) => !w.startsWith("-")); + return positionals.every((p, i) => got[i] === p); + }); + }); + + return { + pass, + message: () => { + const wanted = [bin, ...positionals].join(" "); + if (pass) return `expected no command matching \`${wanted}\`, but one ran`; + const seen = result.commands.map((c) => ` ${c}`).join("\n") || " (no commands ran)"; + return `expected a command matching \`${wanted}\`, but saw:\n${seen}`; + }, + }; + }, + + async toSatisfyJudge(content: string, criterion: string) { + const { pass, reason } = await judge(content, criterion); + return { + pass, + message: () => `judge: ${reason}`, + }; + }, +}); + +type AgentResult = { + result: SDKResultMessage; + commands: string[]; +}; + +async function agent( + prompt: string, + config: { + systemPromptAppend?: string; + cwd: URL; + env: NodeJS.ProcessEnv; + onCommand?: (command: string) => void; + }, +) { + const { systemPromptAppend, cwd, env, onCommand } = config; + + let result: SDKResultMessage | undefined; + const commands: string[] = []; + + for await (const message of query({ + prompt, + options: { + model: EVAL_MODEL, + systemPrompt: { + type: "preset", + preset: "claude_code", + append: systemPromptAppend, + }, + permissionMode: "bypassPermissions", + allowDangerouslySkipPermissions: true, + settingSources: [], + persistSession: false, + cwd: fileURLToPath(cwd), + env, + }, + })) { + if (message.type === "result") result = message; + if (message.type === "assistant") { + for (const block of message.message.content) { + if (block.type === "tool_use" && block.name === "Bash") { + const command = (block.input as { command?: string }).command; + if (typeof command !== "string") continue; + commands.push(command); + onCommand?.(command); + } + } + } + } + + if (result?.subtype !== "success") { + throw new Error(`Agent run failed (${result?.subtype ?? "no result message"})`); + } + + return { result, commands }; +} + +async function judge( + content: string, + criterion: string, +): Promise<{ reason: string; pass: boolean }> { + const prompt = dedent` + You are judging an AI agent's work against a criterion. + First write a short reason, then decide whether the work satisfies the criterion. + + + ${criterion} + + + + ${content} + + `; + + let result: SDKResultMessage | undefined; + for await (const message of query({ + prompt, + options: { + model: JUDGE_MODEL, + systemPrompt: "You are a strict grader.", + allowedTools: [], + persistSession: false, + settingSources: [], + outputFormat: { + type: "json_schema", + schema: { + type: "object", + properties: { reason: { type: "string" }, pass: { type: "boolean" } }, + required: ["reason", "pass"], + additionalProperties: false, + }, + }, + }, + })) { + if (message.type === "result") result = message; + } + + if (result?.subtype !== "success") { + throw new Error(`Judge run failed (${result?.subtype ?? "no result message"})`); + } + + return result.structured_output as { reason: string; pass: boolean }; +} + +async function fetchSkill() { + const response = await fetch( + `https://raw.githubusercontent.com/prismicio/skills/${PRISMIC_SKILL_REF}/skills/prismic/SKILL.md`, + ); + if (!response.ok) throw new Error(`Prismic skill fetch failed (${response.status})`); + const text = await response.text(); + return text.replace(/^---\n[\s\S]*?\n---\n/, "").trim(); +} + +async function createClaudeConfigDir() { + const claudeConfigDir = await mkdtemp(join(tmpdir(), "prismic-eval-claude-")); + await writeFile( + join(claudeConfigDir, ".claude.json"), + JSON.stringify({ hasCompletedOnboarding: true }), + ); + await writeFile(join(claudeConfigDir, "settings.json"), JSON.stringify({})); + if (!process.env.ANTHROPIC_API_KEY) { + // Copy subscription credentials + const source = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"); + try { + await copyFile(join(source, ".credentials.json"), join(claudeConfigDir, ".credentials.json")); + } catch {} + } + return claudeConfigDir; +} diff --git a/evals/render-content.eval.ts b/evals/render-content.eval.ts new file mode 100644 index 0000000..2f69ef0 --- /dev/null +++ b/evals/render-content.eval.ts @@ -0,0 +1,58 @@ +import dedent from "dedent"; +import { readdir, readFile } from "node:fs/promises"; + +import { buildSlice, readLocalCustomTypes, readLocalSlices, writeLocalSlice } from "../test/it"; +import { it, trials } from "./it"; + +it.for(trials)("writes a slice component", async (_, { project, agent, expect }) => { + const slice = buildSlice({ id: "testimonial", name: "Testimonial" }); + slice.variations[0].primary = { + quote: { type: "StructuredText", config: { label: "Quote", multi: "paragraph" } }, + avatar: { type: "Image", config: { label: "Avatar" } }, + attribution: { type: "Text", config: { label: "Attribution" } }, + }; + await writeLocalSlice(project, slice); + + await agent(`Create the React component for the "Testimonial" slice.`); + + const dir = new URL("slices/Testimonial/", project); + const files = await readdir(dir); + const componentFile = files.find((file) => /\.(t|j)sx$/.test(file)); + expect(componentFile).toBeTruthy(); + const component = await readFile(new URL(componentFile!, dir), "utf8"); + expect(component).toContain("quote"); + expect(component).toContain("avatar"); + expect(component).toContain("attribution"); + await expect(component).toSatisfyJudge( + dedent` + This is a React component for a Prismic slice with a rich text "quote" field, an image "avatar" field, and a key text "attribution" field. + Passes if it follows Prismic conventions: renders the quote with PrismicRichText (not raw text), renders the avatar with PrismicNextImage or PrismicImage (not next/image or ), and receives the slice via props. + `, + ); +}); + +it.todo("wires a page that passes a production build"); + +it.for(trials)( + "models and wires a landing page end to end", + async (_, { project, agent, expect }) => { + const result = await agent( + `Build a Prismic landing page for this project: a "landing_page" type with a hero slice (heading, description, image, CTA), wired up so the page renders its slices.`, + ); + + expect(result).toHaveRun("prismic", ["type", "create"]); + expect(result).toHaveRun("prismic", ["slice", "create"]); + expect(result).toHaveRun("prismic", ["slice", "connect"]); + const models = await readLocalCustomTypes(project); + const landingPage = models.find((model) => model.id === "landing_page"); + expect(landingPage).toBeTruthy(); + const slices = await readLocalSlices(project); + expect(slices.length).toBeGreaterThan(0); + + const sliceZone = Object.values(landingPage!.json.Main).find( + (field) => field.type === "Slices", + ); + const choices = (sliceZone?.config as { choices?: Record })?.choices ?? {}; + expect(slices.some((slice) => slice.id in choices)).toBe(true); + }, +); diff --git a/evals/reporter.ts b/evals/reporter.ts new file mode 100644 index 0000000..cce88b0 --- /dev/null +++ b/evals/reporter.ts @@ -0,0 +1,73 @@ +import type { Reporter, TestModule } from "vitest/node"; + +import { readdirSync, writeFileSync } from "node:fs"; +import { basename } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Per-trial stats recorded by the agent fixture; the reporter adds `pass`. */ +export type Trial = { + model: string; + /** Billed agent cost for the trial; judge calls not included. */ + costUsd: number; + /** Agent wall time in seconds, excluding fixture setup and judging. */ + durationS: number; + /** prismic CLI invocations, verbatim minus the leading `npx prismic`. */ + calls: string[]; +}; + +const EVALS_DIR = fileURLToPath(new URL(".", import.meta.url)); +const RESULTS_PATH = fileURLToPath(new URL("results.json", import.meta.url)); +const LOCAL_RESULTS_PATH = fileURLToPath(new URL("results.local.json", import.meta.url)); + +// Writes the run's trials grouped by eval, sorted by name, to +// results.local.json (gitignored) — and, when every eval ran, to results.json. +// Filtered runs (file filters, -t, it.only) never touch results.json, so it +// always holds one full run; history lives in git. +export default class EvalReporter implements Reporter { + onTestRunEnd(testModules: ReadonlyArray): void { + let model = ""; + let filtered = false; + const evals: Record = {}; + + for (const testModule of testModules) { + for (const test of testModule.children.allTests()) { + const state = test.result().state; + // Vitest reports -t-filtered, .only-suppressed, and it.skip tests + // identically (skipped, mode "skip"); only it.todo stays apart. So + // permanent disables must be authored as it.todo, and any other + // skipped test means the run was filtered. + if (state === "skipped" && test.options.mode !== "todo") filtered = true; + if (state !== "passed" && state !== "failed") continue; + const trial = test.meta().agent; + // A missing trial means fixture setup failed before the agent got a + // prompt: nothing was measured, so the run is not a full snapshot. + if (!trial) { + filtered = true; + continue; + } + model = trial.model; + (evals[test.name] ??= []).push({ + pass: state === "passed", + costUsd: Math.round(trial.costUsd * 100) / 100, + durationS: trial.durationS, + calls: trial.calls, + }); + } + } + + if (Object.keys(evals).length === 0) return; + const sorted = Object.fromEntries(Object.entries(evals).sort(([a], [b]) => a.localeCompare(b))); + // Single-line output keeps run-over-run diffs to one changed line; read + // it with jq or `node --run evals:report`. + const report = JSON.stringify({ model, evals: sorted }) + "\n"; + writeFileSync(LOCAL_RESULTS_PATH, report); + + const evalFiles = readdirSync(EVALS_DIR).filter((file) => file.endsWith(".eval.ts")); + const ranFiles = new Set(testModules.map((testModule) => basename(testModule.moduleId))); + if (filtered || !evalFiles.every((file) => ranFiles.has(file))) { + console.info("Partial run: wrote results.local.json, leaving results.json untouched."); + return; + } + writeFileSync(RESULTS_PATH, report); + } +} diff --git a/evals/results.json b/evals/results.json new file mode 100644 index 0000000..6d846dc --- /dev/null +++ b/evals/results.json @@ -0,0 +1 @@ +{"model":"claude-sonnet-5","evals":{"adds a field":[{"pass":true,"costUsd":0.14,"durationS":15,"calls":["--help","field --help","field add --help","field add rich-text --help","field add rich-text excerpt --to-type article --label Excerpt"]},{"pass":true,"costUsd":0.16,"durationS":25,"calls":["--help","field --help && echo --- && npx prismic type --help","field add --help","field add rich-text --help && echo --- && npx prismic type list && echo --- && npx prismic type view article","field add rich-text excerpt --to-type article --label \"Excerpt\" --allow paragraph,strong,em,hyperlink","type view article"]},{"pass":true,"costUsd":0.17,"durationS":21,"calls":["--help","field --help","field add --help","field add rich-text --help && echo \"---TYPES---\" && npx prismic type --help","type list","field add rich-text excerpt --to-type article --label Excerpt","type view article"]}],"adds a group field with nested fields":[{"pass":true,"costUsd":0.26,"durationS":41,"calls":["--help","field --help","field add --help","field add group --help; echo \"---\"; npx prismic field add image --help; echo \"---\"; npx prismic field add text --help","type --help","type list","field add group features --to-type product --label Features","field add image features.icon --to-type product --label Icon && npx prismic field add text features.label --to-type product --label Label","type view product","field view features --to-type product","field view --help","field view features --from-type product --json","status"]},{"pass":true,"costUsd":0.23,"durationS":38,"calls":["--help","field --help","field add --help","field add group --help; echo \"---\"; npx prismic field add image --help; echo \"---\"; npx prismic field add text --help; echo \"---\"; npx prismic type --help","type list; echo \"---\"; npx prismic type view product","field add group features --to-type product --label Features","field add image features.icon --to-type product --label Icon && npx prismic field add text features.label --to-type product --label Label","type view product","field view features --to-type product","field view --help","field view features --from-type product --json"]},{"pass":true,"costUsd":0.22,"durationS":36,"calls":["--help","types --help 2>&1 || true","type --help 2>&1; echo ---; npx prismic field --help 2>&1","type view product 2>&1","field add --help 2>&1","field add group --help 2>&1; echo ---; npx prismic field add image --help 2>&1; echo ---; npx prismic field add text --help 2>&1","field add group features --to-type product --label Features 2>&1","field add image features.icon --to-type product --label Icon 2>&1\necho ---\nnpx prismic field add text features.label --to-type product --label Label 2>&1","type view product 2>&1","field view features --to-type product 2>&1","field view --help 2>&1","field view features --from-type product --json 2>&1"]}],"adds a locale":[{"pass":true,"costUsd":0.11,"durationS":15,"calls":["--help","locale --help","locale add --help","locale add fr-fr"]},{"pass":true,"costUsd":0.14,"durationS":16,"calls":["--help","locale --help","locale add --help; echo \"---LIST---\"; npx prismic locale list --help","locale list","locale add fr-fr"]},{"pass":true,"costUsd":0.11,"durationS":12,"calls":["--help","locale --help","locale add --help","locale add fr-fr"]}],"adds a slice variation without losing existing ones":[{"pass":true,"costUsd":0.17,"durationS":17,"calls":["--help","slice --help","slice add-variation --help","slice list","slice view --help","slice view hero --json","slice add-variation \"Centered\" --to hero"]},{"pass":true,"costUsd":0.16,"durationS":19,"calls":["--help","slice --help","slice add-variation --help","slice list","slice add-variation \"Centered\" --to hero --id centered","slice view hero"]},{"pass":true,"costUsd":0.16,"durationS":21,"calls":["--help","slice --help","slice list 2>&1; echo \"---\"; npx prismic slice add-variation --help 2>&1","slice add-variation \"Centered\" --to hero 2>&1","slice view hero 2>&1","status 2>&1"]}],"adds constrained fields with the right config":[{"pass":true,"costUsd":0.19,"durationS":31,"calls":["--help","field --help","field add --help && echo \"----\" && npx prismic type --help","type view product 2>&1 | head -100","field add select --help && echo \"----\" && npx prismic field add link --help","field add content-relationship --help","field add select size --to-type product --label Size --option S --option M --option L","field add content-relationship author --to-type product --label Author --custom-type author","type view product"]},{"pass":true,"costUsd":0.25,"durationS":40,"calls":["--help","field --help","field add --help","field add select --help","field add link --help","field add content-relationship --help","type --help && echo --- && npx prismic field view --help","type view product","type list","field add select size --to-type product --label Size --option S --option M --option L","field add content-relationship author --to-type product --label Author --custom-type author","type view product"]},{"pass":true,"costUsd":0.22,"durationS":32,"calls":["--help","field --help","field add --help","field add select --help; echo \"----\"; npx prismic field add link --help","field add content-relationship --help","type --help","type list","field add select size --to-type product --label Size --option S --option M --option L","field add content-relationship author --to-type product --label Author --custom-type author","type view product"]}],"adds Prismic to an existing Next.js app without clobbering it":[{"pass":true,"costUsd":0.25,"durationS":70,"calls":["--help","init --help","docs list","whoami 2>&1; echo \"---\"; cat app/page.tsx","init 2>&1","status 2>&1"]},{"pass":false,"costUsd":0.25,"durationS":28,"calls":["--help","init --help && echo \"---docs---\" && npx prismic docs list","whoami 2>&1","docs view nextjs 2>&1","repo --help 2>&1"]},{"pass":true,"costUsd":0.18,"durationS":55,"calls":["--help","init --help","whoami 2>&1","init --no-browser 2>&1"]}],"commits and pushes local model changes":[{"pass":false,"costUsd":0.13,"durationS":19,"calls":["--help","push --help && echo \"---\" && npx prismic status --help","status"]},{"pass":true,"costUsd":0.17,"durationS":24,"calls":["--help","type --help","type list 2>&1; echo \"---\"; npx prismic push --help 2>&1","status 2>&1","push 2>&1"]},{"pass":true,"costUsd":0.23,"durationS":44,"calls":["--help","type --help","type list && echo \"---STATUS---\" && npx prismic status","push --help","push","status","pull --help 2>&1 | head -30","pull && git diff customtypes/article/index.json","status"]}],"connects a slice to a page type":[{"pass":true,"costUsd":0.16,"durationS":23,"calls":["--help","type --help && echo \"---\" && npx prismic slice --help","slice connect --help","slice list && echo \"---\" && npx prismic type list","slice connect testimonial --to page","status"]},{"pass":true,"costUsd":0.16,"durationS":20,"calls":["--help","type --help && echo \"---\" && npx prismic field --help","slice --help && echo \"---EDIT---\" && npx prismic type edit --help","slice connect --help","slice list && echo \"---TYPES---\" && npx prismic type list","slice connect testimonial --to page"]},{"pass":true,"costUsd":0.14,"durationS":18,"calls":["--help","type --help && echo --- && npx prismic slice --help && echo --- && npx prismic field --help","slice connect --help","slice list && echo --- && npx prismic type list","slice connect testimonial --to page"]}],"consults the docs before an unfamiliar task":[{"pass":true,"costUsd":0.51,"durationS":100,"calls":["--help","preview --help","preview add --help","docs list","docs view previews","docs view nextjs","gen --help && echo \"---\" && npx prismic gen setup --help","gen setup","preview list","preview add http://localhost:3000/api/preview --name Development && echo \"---\" && npx prismic preview set-simulator http://localhost:3000 && echo \"---\" && npx prismic preview list"]},{"pass":true,"costUsd":0.73,"durationS":139,"calls":["--help","preview --help","preview add --help && echo \"---LIST---\" && npx prismic preview list --help && echo \"---SIM---\" && npx prismic preview set-simulator --help","docs list","docs view previews","docs view nextjs 2>&1 | head -400","docs view nextjs 2>&1 | sed -n '400,650p'","gen --help; echo \"---\"; npx prismic gen setup --help","gen setup","docs view technical-reference/prismicio-next/v2 2>&1 | grep -n -A 30 \"PrismicPreview\"| head -60","preview add http://localhost:3000/api/preview --name Development && echo \"---set-sim---\" && npx prismic preview set-simulator http://localhost:3000 && echo \"---list---\" && npx prismic preview list","status; echo \"---sync help---\"; npx prismic sync --help"]},{"pass":true,"costUsd":0.38,"durationS":71,"calls":["--help","preview --help","preview add --help","docs list","docs view previews","docs view nextjs > /tmp/nextjs_doc.md; wc -l /tmp/nextjs_doc.md","gen --help","gen setup --help; echo \"---current previews---\"; npx prismic preview list","gen setup","preview set-simulator http://localhost:3000 2>&1\necho \"---\"\nnpx prismic preview add http://localhost:3000/api/preview --name Development 2>&1","preview list"]}],"creates an API token and makes the API private":[{"pass":true,"costUsd":0.15,"durationS":26,"calls":["--help","token --help && echo \"---\" && npx prismic repo --help","token create --help && echo \"---\" && npx prismic repo set-api-access --help","token create --name \"ci\"","repo set-api-access private"]},{"pass":true,"costUsd":0.16,"durationS":26,"calls":["--help","token --help","repo --help","token create --help","repo set-api-access --help","whoami; npx prismic repo list","token create --name ci --json","repo set-api-access private"]},{"pass":true,"costUsd":0.14,"durationS":19,"calls":["--help","token --help","repo --help","token create --help","repo set-api-access --help","token create --name ci","repo set-api-access private"]}],"does not destructively act on an ambiguous request":[{"pass":true,"costUsd":0.21,"durationS":44,"calls":["--help","field --help && echo \"---\" && npx prismic type --help","type list && echo \"---\" && find customtypes -maxdepth 3","type view article","status 2>&1; echo \"---app---\"; find app -maxdepth 4 -type f | head -100"]},{"pass":true,"costUsd":0.22,"durationS":55,"calls":["--help","types --help 2>&1 || true","type --help; echo ---; npx prismic field --help","type list","type view article 2>&1 || true","type view --help","type view article --json 2>&1 || npx prismic type view article -o json 2>&1 || true","status 2>&1 || true","field view --help"]},{"pass":true,"costUsd":0.32,"durationS":67,"calls":["--help","customtypes --help 2>&1 || npx prismic types --help 2>&1","type --help","field --help","type view --help","type list","type view article --json","status 2>&1 | head -50","docs list 2>&1 | head -60"]}],"does not try to reassign a document to another type":[{"pass":true,"costUsd":0.19,"durationS":23,"calls":["--help","type --help","docs list 2>/dev/null | grep -i -E \"document|type|migrat\""]},{"pass":true,"costUsd":0.05,"durationS":9,"calls":["--help"]},{"pass":true,"costUsd":0.45,"durationS":59,"calls":["--help","type --help","status 2>&1 | head -50"]}],"extends a slice to match a design change":[{"pass":true,"costUsd":0.19,"durationS":27,"calls":["--help","slice --help && echo \"---FIELD---\" && npx prismic field --help","field add --help","field add image --help && echo \"---SELECT---\" && npx prismic field add select --help && echo \"---NUMBER---\" && npx prismic field add number --help","field add image company_logo --to-slice testimonial --variation default --label \"Company Logo\" 2>&1","field add select rating --to-slice testimonial --variation default --label \"Rating\" --option 1 --option 2 --option 3 --option 4 --option 5 --default-value 5 2>&1"]},{"pass":true,"costUsd":0.23,"durationS":32,"calls":["--help","slice --help && echo ---- && npx prismic field --help","field add --help","field add image --help && echo ---- && npx prismic field add number --help && echo ---- && npx prismic field add select --help","field add image company_logo --to-slice testimonial --variation default --label \"Company Logo\"","field add select rating --to-slice testimonial --variation default --label \"Star Rating\" --option \"1\" --option \"2\" --option \"3\" --option \"4\" --option \"5\" --default-value \"5\"","status"]},{"pass":true,"costUsd":0.21,"durationS":31,"calls":["--help","slice --help","slice list","slice view testimonial","field --help","field add --help","field add image --help; echo \"---\"; npx prismic field add select --help; echo \"---\"; npx prismic field add number --help","field add image company_logo --to-slice testimonial --variation default --label \"Company Logo\"","field add select rating --to-slice testimonial --variation default --label \"Star Rating\" --option \"1\" --option \"2\" --option \"3\" --option \"4\" --option \"5\" --default-value \"5\"","slice view testimonial"]}],"handles a vague design request reasonably":[{"pass":true,"costUsd":0.81,"durationS":188,"calls":["--help","docs list","type --help && echo --- && npx prismic slice --help && echo --- && npx prismic field --help","type list 2>&1; echo \"---SLICES---\"; npx prismic slice list 2>&1","type view homepage 2>&1","slice create --help 2>&1; echo ---; npx prismic slice add-variation --help 2>&1; echo ---; npx prismic slice connect --help 2>&1","field add --help 2>&1","field add text --help 2>&1; echo ---; npx prismic field add image --help 2>&1; echo ---; npx prismic field add link --help 2>&1","docs view slices 2>&1 | head -150","docs view slices 2>&1 | sed -n '150,260p'","docs view cli 2>&1 | grep -n -A 15 \"add-variation\"","docs view cli 2>&1 | head -200","slice create \"Hero\" 2>&1","field add rich-text heading --to-slice hero --label \"Heading\" --allow \"heading1,strong,em\" 2>&1\nnpx prismic field add rich-text description --to-slice hero --label \"Description\" --allow \"paragraph,strong,em,hyperlink\" 2>&1\nnpx prismic field add image image --to-slice hero --label \"Image\" 2>&1\nnpx prismic field add link link --to-slice hero --label \"Button Link\" --allow-text --allow-target-blank 2>&1\n","slice view hero 2>&1","slice add-variation \"Image Left\" --to hero --id imageLeft 2>&1\necho ---\nnpx prismic slice add-variation \"Centered\" --to hero --id centered 2>&1","slice view hero 2>&1","field add rich-text heading --to-slice hero --variation imageLeft --label \"Heading\" --allow \"heading1,strong,em\" 2>&1\nnpx prismic field add rich-text description --to-slice hero --variation imageLeft --label \"Description\" --allow \"paragraph,strong,em,hyperlink\" 2>&1\nnpx prismic field add image image --to-slice hero --variation imageLeft --label \"Image\" 2>&1\nnpx prismic field add link link --to-slice hero --variation imageLeft --label \"Button Link\" --allow-text --allow-target-blank 2>&1\necho ---\nnpx prismic field add rich-text heading --to-slice hero --variation centered --label \"Heading\" --allow \"heading1,strong,em\" 2>&1\nnpx prismic field add rich-text description --to-slice hero --variation centered --label \"Description\" --allow \"paragraph,strong,em,hyperlink\" 2>&1\nnpx prismic field add link link --to-slice hero --variation centered --label \"Button Link\" --allow-text --allow-target-blank 2>&1\n","slice connect hero --to homepage 2>&1","type edit --help 2>&1; echo ---; npx prismic docs view content-modeling 2>&1 | grep -n -i \"slice zone\" ","type create --help 2>&1; echo ---; npx prismic --help 2>&1 | grep -i zone; echo ---; npx prismic field add group --help 2>&1","type edit homepage --format page 2>&1","type view homepage 2>&1","slice connect hero --to homepage 2>&1","type add-tab --help 2>&1","type edit-tab --help 2>&1","type edit-tab Main --from-type homepage --with-slice-zone 2>&1","slice connect hero --to homepage 2>&1","type view homepage 2>&1; echo ===; npx prismic slice view hero 2>&1","status 2>&1"]},{"pass":true,"costUsd":0.61,"durationS":125,"calls":["--help","type --help; echo \"---SLICE---\"; npx prismic slice --help","slice list 2>&1; echo \"---\"; npx prismic type list 2>&1","slice create --help; echo \"---VARIATION---\"; npx prismic slice add-variation --help","field --help","docs list 2>&1","docs view slices 2>&1","slice create \"Hero\" 2>&1","field add --help 2>&1","field add rich-text --help 2>&1; echo \"---LINK---\"; npx prismic field add link --help 2>&1; echo \"---IMAGE---\"; npx prismic field add image --help 2>&1","field add rich-text heading --to-slice hero --label \"Heading\" --allow \"heading1,heading2\" --single 2>&1\nnpx prismic field add rich-text subheading --to-slice hero --label \"Subheading\" --allow \"paragraph,strong,em,hyperlink\" 2>&1\nnpx prismic field add image image --to-slice hero --label \"Image\" 2>&1\nnpx prismic field add link cta_link --to-slice hero --label \"Call to Action\" --allow-text --allow-target-blank 2>&1\n","slice add-variation --help 2>&1","slice add-variation \"Image Right\" --to hero --id imageRight 2>&1\nnpx prismic slice add-variation \"Background Image\" --to hero --id backgroundImage 2>&1\n","slice view hero 2>&1","set -e\nfor VAR in imageRight backgroundImage; do\n npx prismic field add rich-text heading --to-slice hero --variation $VAR --label \"Heading\" --allow \"heading1,heading2\" --single 2>&1\n npx prismic field add rich-text subheading --to-slice hero --variation $VAR --label \"Subheading\" --allow \"paragraph,strong,em,hyperlink\" 2>&1\n npx prismic field add image image --to-slice hero --variation $VAR --label \"Image\" 2>&1\n npx prismic field add link cta_link --to-slice hero --variation $VAR --label \"Call to Action\" --allow-text --allow-target-blank 2>&1\ndone\n","slice view hero 2>&1","slice connect --help 2>&1","slice connect hero --to homepage 2>&1","type view homepage 2>&1","type edit --help 2>&1; echo \"---ADD-TAB---\"; npx prismic type add-tab --help 2>&1","type edit-tab --help 2>&1","type edit-tab Main --from-type homepage --with-slice-zone 2>&1","slice connect hero --to homepage 2>&1","type view homepage 2>&1; echo \"---\"; npx prismic slice view hero 2>&1","status 2>&1"]},{"pass":true,"costUsd":0.81,"durationS":171,"calls":["--help","docs list","slice --help && echo \"---\" && npx prismic type --help && echo \"---\" && npx prismic field --help","type list && echo \"---SLICES---\" && npx prismic slice list","type view homepage && echo \"---\" && npx prismic slice create --help","slice add-variation --help && echo \"---\" && npx prismic field add --help && echo \"---\" && npx prismic slice connect --help","docs view slices","slice create Hero","field add rich-text --help && echo \"---LINK---\" && npx prismic field add link --help && echo \"---IMAGE---\" && npx prismic field add image --help","field add rich-text heading --to-slice hero --allow heading1,heading2 --single --label \"Heading\"\nnpx prismic field add rich-text tagline --to-slice hero --allow paragraph,strong,em,hyperlink --label \"Tagline\"\nnpx prismic field add image image --to-slice hero --label \"Image\"\nnpx prismic field add link primary_button --to-slice hero --allow-text --label \"Primary Button\"\nnpx prismic field add link secondary_button --to-slice hero --allow-text --label \"Secondary Button\"\n","slice add-variation --help | grep -A2 \"duplicate\\|from\" ; npx prismic slice add-variation \"Centered\" --to hero","slice view hero","field add rich-text heading --to-slice hero --variation centered --allow heading1,heading2 --single --label \"Heading\"\nnpx prismic field add rich-text tagline --to-slice hero --variation centered --allow paragraph,strong,em,hyperlink --label \"Tagline\"\nnpx prismic field add link primary_button --to-slice hero --variation centered --allow-text --label \"Primary Button\"\nnpx prismic field add link secondary_button --to-slice hero --variation centered --allow-text --label \"Secondary Button\"\n","slice add-variation \"Background Image\" --to hero","field add rich-text heading --to-slice hero --variation backgroundImage --allow heading1,heading2 --single --label \"Heading\"\nnpx prismic field add rich-text tagline --to-slice hero --variation backgroundImage --allow paragraph,strong,em,hyperlink --label \"Tagline\"\nnpx prismic field add image image --to-slice hero --variation backgroundImage --label \"Background Image\"\nnpx prismic field add link primary_button --to-slice hero --variation backgroundImage --allow-text --label \"Primary Button\"\nnpx prismic field add link secondary_button --to-slice hero --variation backgroundImage --allow-text --label \"Secondary Button\"\n","slice connect hero --to homepage && echo \"---\" && npx prismic slice view hero && echo \"---\" && npx prismic type view homepage","type edit --help","docs view content-modeling | grep -i -A 20 \"slice zone\"","docs list | grep -i slice; npx prismic docs view custom-types-api | grep -i -A10 \"slice zone\" | head -50","type create --help","type edit homepage -f page && npx prismic type view homepage","type view --help; npx prismic type view homepage --json 2>/dev/null || npx prismic type view homepage -v 2>/dev/null; echo done","slice connect hero --to homepage","docs view cli | grep -i -B2 -A15 \"slice.zone\\|slice zone\"","--help 2>&1 | grep -i slice; npx prismic type add-tab --help; echo \"---\"; npx prismic field add group --help","type add-tab Main --to homepage --with-slice-zone 2>&1","type edit-tab --help","type edit-tab Main --from-type homepage --with-slice-zone && npx prismic type view homepage --json","slice connect hero --to homepage && npx prismic type view homepage --json","slice view hero --json 2>/dev/null || npx prismic slice view hero; echo \"---STATUS---\"; npx prismic status","push"]}],"initializes Prismic in a Next.js project":[{"pass":false,"costUsd":0.21,"durationS":36,"calls":["--help","init --help","docs list","whoami 2>&1","repo --help","repo list 2>&1"]},{"pass":true,"costUsd":0.22,"durationS":58,"calls":["--help","init --help","docs list","whoami 2>&1","init 2>&1"]},{"pass":true,"costUsd":0.52,"durationS":117,"calls":["--help","init --help","docs list","docs view cli","docs view nextjs","whoami","init --no-browser 2>&1","gen --help; echo \"---\"; npx prismic type --help","preview set-simulator http://localhost:3000 2>&1; echo \"---\"; npx prismic preview add http://localhost:3000/api/preview --name Development 2>&1"]}],"initializes with an existing repository":[{"pass":true,"costUsd":0.14,"durationS":39,"calls":["--help","init --help && echo \"---whoami---\" && npx prismic whoami","init --repo prismic-cli-isolated-cfdaa6a9","pull"]},{"pass":false,"costUsd":0.17,"durationS":80,"calls":["--help","init --help && echo \"---WHOAMI---\" && npx prismic whoami && echo \"---PKG---\" && cat package.json && echo \"---APP---\" && ls -la app","repo --help && echo \"---LOGIN---\" && npx prismic login --help","login --no-browser"]},{"pass":true,"costUsd":0.15,"durationS":41,"calls":["--help","init --help","whoami","init --repo prismic-cli-isolated-cb37e356","status"]}],"is idempotent when the same task runs twice":[{"pass":true,"costUsd":0.35,"durationS":40,"calls":["--help","field --help","field add --help","field add rich-text --help","type --help","type list","field add rich-text body --to-type homepage --label Body","type view homepage","--help","field --help","field add --help","field add rich-text --help","type --help","type list","field add rich-text body --to-type homepage --label Body"]},{"pass":true,"costUsd":0.32,"durationS":41,"calls":["--help","field --help","field add --help","field add rich-text --help","field add rich-text body --to-type homepage --label Body","--help","field --help","field add --help","field add rich-text --help; echo \"---\"; npx prismic type --help","type list","field add rich-text body --to-type homepage","field view --help","field view body --from-type homepage --json"]},{"pass":true,"costUsd":0.32,"durationS":40,"calls":["--help","field --help","field add --help","field add rich-text --help","field add rich-text body --to-type homepage --label Body","--help","field --help","field add --help","field add rich-text --help","field add rich-text body --to-type homepage --label Body","field view body --to-type homepage","field view --help","field view body --from-type homepage --json"]}],"models a page type with sensible field types":[{"pass":true,"costUsd":0.28,"durationS":72,"calls":["--help","type --help && echo \"---\" && npx prismic field --help","type create --help && echo \"---\" && npx prismic field add --help","field add text --help && echo \"---DATE---\" && npx prismic field add date --help && echo \"---IMAGE---\" && npx prismic field add image --help && echo \"---RICHTEXT---\" && npx prismic field add rich-text --help","type list 2>&1; echo \"---\"; npx prismic status --help 2>&1","type view author 2>&1","field add content-relationship --help","docs list 2>&1 | head -50","type create \"Blog Post\" --format page --id blog_post 2>&1 && echo \"---VIEW---\" && npx prismic type view blog_post 2>&1","field add rich-text title --to-type blog_post --label Title --allow heading1 --single 2>&1\necho \"---\"\nnpx prismic field add date publish_date --to-type blog_post --label \"Publish Date\" 2>&1\necho \"---\"\nnpx prismic field add image hero_image --to-type blog_post --label \"Hero Image\" 2>&1\necho \"---\"\nnpx prismic field add content-relationship author --to-type blog_post --label Author --custom-type author 2>&1\necho \"---\"\nnpx prismic field add rich-text body --to-type blog_post --label Body --allow heading2,heading3,heading4,paragraph,strong,em,hyperlink,image,embed,list-item,o-list-item 2>&1\n","type view blog_post 2>&1"]},{"pass":true,"costUsd":0.26,"durationS":64,"calls":["--help","type --help && echo \"---\" && npx prismic field --help","type create --help && echo \"---\" && npx prismic field add --help","field add text --help && echo \"---\" && npx prismic field add rich-text --help && echo \"---\" && npx prismic field add date --help && echo \"---\" && npx prismic field add image --help && echo \"---\" && npx prismic field add link --help","type list 2>&1","type view author 2>&1","field add content-relationship --help","type create \"Blog Post\" --format page --id blog_post 2>&1","field add rich-text title --to-type blog_post --allow heading1 --single --label \"Title\"\nnpx prismic field add date publish_date --to-type blog_post --label \"Publish Date\"\nnpx prismic field add image hero_image --to-type blog_post --label \"Hero Image\"\nnpx prismic field add content-relationship author --to-type blog_post --custom-type author --label \"Author\"\nnpx prismic field add rich-text body --to-type blog_post --allow heading1,heading2,heading3,paragraph,strong,em,hyperlink,image,embed,list-item,o-list-item --label \"Body\"\n","type view blog_post 2>&1"]},{"pass":false,"costUsd":0.21,"durationS":51,"calls":["--help","type --help && echo \"-----\" && npx prismic field --help","type create --help && echo \"-----\" && npx prismic field add --help","field add rich-text --help && echo \"-----\" && npx prismic field add date --help && echo \"-----\" && npx prismic field add image --help && echo \"-----\" && npx prismic field add text --help","type create \"Blog Post\" --format page --id blog_post","type view blog_post","field add rich-text title --to-type blog_post --allow heading1 --single --label \"Title\"\nnpx prismic field add date publish_date --to-type blog_post --label \"Publish Date\"\nnpx prismic field add image hero_image --to-type blog_post --label \"Hero Image\"\nnpx prismic field add text author --to-type blog_post --label \"Author\"\nnpx prismic field add rich-text body --to-type blog_post --allow heading1,heading2,heading3,paragraph,strong,em,hyperlink,list-item,o-list-item,image,embed --label \"Body\"","type view blog_post"]}],"models a slice from a screenshot":[{"pass":true,"costUsd":0.35,"durationS":77,"calls":["--help","slice --help","slice create --help; echo \"---\"; npx prismic field --help","docs list","field add --help","docs view fields/repeatable-group","field add rich-text --help; echo \"---TEXT---\"; npx prismic field add text --help; echo \"---IMAGE---\"; npx prismic field add image --help; echo \"---GROUP---\"; npx prismic field add group --help","slice create Feature --id feature","field add rich-text heading --to-slice feature --label \"Heading\" --allow heading1,heading2 --single\nnpx prismic field add rich-text body --to-slice feature --label \"Body\" --allow paragraph,strong,em,hyperlink\nnpx prismic field add image image --to-slice feature --label \"Image\"\nnpx prismic field add group stats --to-slice feature --label \"Stats\"\nnpx prismic field add text stats.value --to-slice feature --label \"Value\" --placeholder \"4,800+\"\nnpx prismic field add text stats.label --to-slice feature --label \"Label\" --placeholder \"marketers using Prismic\"\n","slice view feature","field view stats --to-slice feature","field view --help","field view stats --from-slice feature --json"]},{"pass":true,"costUsd":0.55,"durationS":116,"calls":["--help","slice --help","docs list","slice create --help; echo \"===ADD-VAR===\"; npx prismic slice add-variation --help; echo \"===FIELD===\"; npx prismic field --help","docs view slices","field add --help","docs view fields/repeatable-group | head -150","field add text --help; echo \"===RICHTEXT===\"; npx prismic field add rich-text --help; echo \"===IMAGE===\"; npx prismic field add image --help; echo \"===GROUP===\"; npx prismic field add group --help; echo \"===NUMBER===\"; npx prismic field add number --help","slice list; echo \"---TYPES---\"; npx prismic type list 2>&1 | head -30; echo \"---STATUS---\"; npx prismic status 2>&1 | head -30","slice create \"Feature\" --id feature","field add rich-text heading --to-slice feature --label \"Heading\" --allow heading1,heading2 --single\nnpx prismic field add rich-text body --to-slice feature --label \"Body\" --allow paragraph,strong,hyperlink\nnpx prismic field add image image --to-slice feature --label \"Image\"","field add text image_overlay_eyebrow --to-slice feature --label \"Image Overlay Eyebrow\" --placeholder \"Ask AI to make the edits\"\nnpx prismic field add text image_overlay_badge --to-slice feature --label \"Image Overlay Badge\" --placeholder \"NEW\"\nnpx prismic field add text image_overlay_placeholder --to-slice feature --label \"Image Overlay Placeholder\" --placeholder \"Can you change the CTA label to ...\"","field add group stats --to-slice feature --label \"Stats\"\nnpx prismic field add text stats.value --to-slice feature --label \"Value\" --placeholder \"4,800+\"\nnpx prismic field add text stats.label --to-slice feature --label \"Label\" --placeholder \"marketers using Prismic\"","slice view feature","field view stats --from-slice feature 2>&1 || npx prismic field view stats.value --from-slice feature 2>&1","slice edit-variation default --screenshot /tmp/prismic-test-Nhof1H/project/feature-slice.png --from-slice feature","slice edit-variation default --screenshot /tmp/prismic-test-Nhof1H/project/feature-slice.png --from-slice feature"]},{"pass":true,"costUsd":0.38,"durationS":94,"calls":["--help","slice --help","slice create --help && echo \"---FIELD---\" && npx prismic field --help","field add --help","field add rich-text --help && echo \"---TEXT---\" && npx prismic field add text --help && echo \"---IMAGE---\" && npx prismic field add image --help && echo \"---GROUP---\" && npx prismic field add group --help","slice create BuiltForGrowth --id built_for_growth 2>&1","field add rich-text heading --to-slice built_for_growth --label \"Heading\" --allow heading1,heading2 2>&1","field add rich-text description --to-slice built_for_growth --label \"Description\" --allow paragraph,strong,em,hyperlink 2>&1","field add image image --to-slice built_for_growth --label \"Image\" 2>&1","field add text badge_label --to-slice built_for_growth --label \"Badge Label\" --placeholder \"Ask AI to make the edits\" 2>&1","field add text badge_tag --to-slice built_for_growth --label \"Badge Tag\" --placeholder \"NEW\" 2>&1","field add text overlay_text --to-slice built_for_growth --label \"Overlay Input Text\" --placeholder \"Can you change the CTA label to ...\" 2>&1","field add group stats --to-slice built_for_growth --label \"Stats\" 2>&1","field add text stats.value --to-slice built_for_growth --label \"Value\" --placeholder \"4,800+\" 2>&1","field add text stats.label --to-slice built_for_growth --label \"Label\" --placeholder \"marketers using Prismic\" 2>&1","slice view built_for_growth 2>&1","field view stats --to-slice built_for_growth 2>&1","field view --help","field view stats --from-slice built_for_growth --json 2>&1","push --help 2>&1","whoami 2>&1"]}],"models a testimonial slice from a text description":[{"pass":true,"costUsd":0.18,"durationS":37,"calls":["--help","slice --help && echo \"---FIELD---\" && npx prismic field --help","slice create --help && echo \"---FIELD ADD---\" && npx prismic field add --help","field add text --help && echo \"---RICH---\" && npx prismic field add rich-text --help && echo \"---IMAGE---\" && npx prismic field add image --help","slice create testimonial --id testimonial","field add rich-text quote --to-slice testimonial --label \"Quote\" --allow paragraph --single\nnpx prismic field add text author_name --to-slice testimonial --label \"Author Name\"\nnpx prismic field add text author_role --to-slice testimonial --label \"Author Role\"\nnpx prismic field add image avatar --to-slice testimonial --label \"Avatar\"\nnpx prismic field add image company_logo --to-slice testimonial --label \"Company Logo\"\n","slice view testimonial"]},{"pass":true,"costUsd":0.21,"durationS":38,"calls":["--help","slice --help","slice create --help","field --help","field add --help","field add text --help; echo ---; npx prismic field add rich-text --help; echo ---; npx prismic field add image --help","slice create Testimonial 2>&1","field add rich-text quote --to-slice testimonial --label \"Quote\" --allow paragraph --single 2>&1\nnpx prismic field add text author_name --to-slice testimonial --label \"Author Name\" 2>&1\nnpx prismic field add text author_role --to-slice testimonial --label \"Author Role\" 2>&1\nnpx prismic field add image avatar --to-slice testimonial --label \"Avatar\" 2>&1\nnpx prismic field add image company_logo --to-slice testimonial --label \"Company Logo\" 2>&1\n","slice view testimonial 2>&1"]},{"pass":true,"costUsd":0.19,"durationS":38,"calls":["--help","slice --help","slice create --help","field --help && echo \"---\" && npx prismic field add --help","field add rich-text --help && echo \"---TEXT---\" && npx prismic field add text --help && echo \"---IMAGE---\" && npx prismic field add image --help","slice create Testimonial --id testimonial","field add rich-text quote --to-slice testimonial --label \"Quote\" --allow paragraph --single\nnpx prismic field add text author_name --to-slice testimonial --label \"Author Name\"\nnpx prismic field add text author_role --to-slice testimonial --label \"Author Role\"\nnpx prismic field add image avatar --to-slice testimonial --label \"Avatar\"\nnpx prismic field add image company_logo --to-slice testimonial --label \"Company Logo\"","slice view testimonial"]}],"models a title as single-heading rich text and a social media handle as key text":[{"pass":false,"costUsd":0.21,"durationS":32,"calls":["--help","type --help && echo \"---\" && npx prismic field --help","type create --help && echo \"---FIELD ADD---\" && npx prismic field add --help","type list 2>&1","type view blog_post 2>&1","field add text --help 2>&1 && echo \"---LINK---\" && npx prismic field add link --help 2>&1","field add text title --to-type blog_post --label \"Title\" 2>&1","field add text author_bluesky_handle --to-type blog_post --label \"Author Bluesky Handle\" --placeholder \"e.g. @username.bsky.social\" 2>&1","type view blog_post 2>&1"]},{"pass":false,"costUsd":0.2,"durationS":35,"calls":["--help","type --help && echo ---- && npx prismic field --help","type create --help && echo ---- && npx prismic field add --help","field add text --help","type create \"Blog Post\" --format page --id blog_post","field add text title --to-type blog_post --label \"Title\" && npx prismic field add text author_bluesky_handle --to-type blog_post --label \"Author Bluesky Handle\" --placeholder \"@handle.bsky.social\"","type view blog_post","status","push"]},{"pass":false,"costUsd":0.21,"durationS":39,"calls":["--help","type --help && echo \"---FIELD---\" && npx prismic field --help","type create --help && echo \"---\" && npx prismic field add --help","field add text --help","type create blog_post --format page","type view blog_post","field add text title --to-type blog_post --label \"Title\" && npx prismic field add text author_bluesky_handle --to-type blog_post --label \"Author Bluesky Handle\" --placeholder \"@handle.bsky.social\"","type view blog_post","status"]}],"models and wires a landing page end to end":[{"pass":true,"costUsd":1.13,"durationS":277,"calls":["--help","type --help && echo \"---SLICE---\" && npx prismic slice --help && echo \"---FIELD---\" && npx prismic field --help","type create --help","slice create --help && echo \"---CONNECT---\" && npx prismic slice connect --help && echo \"---FIELD ADD---\" && npx prismic field add --help","field add text --help && echo \"---RICH---\" && npx prismic field add rich-text --help && echo \"---IMAGE---\" && npx prismic field add image --help && echo \"---LINK---\" && npx prismic field add link --help","type list 2>&1 && echo \"---SLICES---\" && npx prismic slice list 2>&1","push --help && echo \"---GEN---\" && npx prismic gen --help && echo \"---WHOAMI---\" && npx prismic whoami 2>&1","gen setup --help && echo \"---DOCS---\" && npx prismic docs list","type create \"Landing Page\" --format page --id landing_page\necho \"---\"\nnpx prismic slice create Hero --id hero","field add rich-text heading --to-slice hero --allow heading1 --single --label \"Heading\"\nnpx prismic field add rich-text description --to-slice hero --allow paragraph,strong,em,hyperlink --label \"Description\"\nnpx prismic field add image image --to-slice hero --label \"Image\"\nnpx prismic field add link cta --to-slice hero --allow-text --allow-target-blank --label \"CTA\"\necho \"---VIEW---\"\nnpx prismic slice view hero","slice connect hero --to landing_page\necho \"---VIEW TYPE---\"\nnpx prismic type view landing_page","status 2>&1","push 2>&1","gen setup 2>&1","status 2>&1"]},{"pass":true,"costUsd":1.1,"durationS":220,"calls":["--help","type --help; echo \"----\"; npx prismic slice --help; echo \"----\"; npx prismic field --help","whoami 2>&1; echo \"---\"; npx prismic docs list 2>&1","docs view nextjs 2>&1","init --help 2>&1; echo \"===TYPE CREATE===\"; npx prismic type create --help 2>&1; echo \"===SLICE CREATE===\"; npx prismic slice create --help 2>&1","field add --help 2>&1; echo \"===SLICE CONNECT===\"; npx prismic slice connect --help 2>&1; echo \"===GEN===\"; npx prismic gen --help 2>&1","init 2>&1","gen setup --help 2>&1; echo \"===STATUS===\"; npx prismic status 2>&1; echo \"===TYPE LIST===\"; npx prismic type list 2>&1; echo \"===SLICE LIST===\"; npx prismic slice list 2>&1","gen setup 2>&1","type create \"Landing Page\" --format page --id landing_page 2>&1","type view landing_page 2>&1; echo \"---config---\"; cat prismic.config.json","slice create Hero 2>&1","field add rich-text --help 2>&1; echo \"===TEXT===\"; npx prismic field add text --help 2>&1; echo \"===IMAGE===\"; npx prismic field add image --help 2>&1; echo \"===LINK===\"; npx prismic field add link --help 2>&1","field add rich-text heading --to-slice hero --allow heading1,heading2 --single --label Heading 2>&1\necho \"---\"\nnpx prismic field add rich-text description --to-slice hero --allow paragraph,strong,em,hyperlink --label Description 2>&1\necho \"---\"\nnpx prismic field add image image --to-slice hero --label Image 2>&1\necho \"---\"\nnpx prismic field add link cta --to-slice hero --allow-text --label \"CTA\" 2>&1\n","slice view hero 2>&1","slice connect hero --to landing_page 2>&1; echo \"---\"; npx prismic type view landing_page 2>&1","status 2>&1; echo \"===PUSH HELP===\"; npx prismic push --help 2>&1","push 2>&1","gen types --help 2>&1","gen types 2>&1; echo \"---files---\"; find . -path ./node_modules -prune -o -type f -print | sort","status 2>&1"]},{"pass":true,"costUsd":0.63,"durationS":152,"calls":["--help","type --help && echo \"---SLICE---\" && npx prismic slice --help && echo \"---FIELD---\" && npx prismic field --help","type create --help && echo \"---\" && npx prismic slice create --help && echo \"---\" && npx prismic field add --help","docs list","docs view nextjs 2>&1 | head -200","docs view nextjs 2>&1 | sed -n '200,400p'","docs view slices 2>&1 | head -150","whoami 2>&1; echo \"---gen---\"; npx prismic gen --help 2>&1; echo \"---status---\"; npx prismic status 2>&1","gen setup --help 2>&1; echo \"---types list---\"; npx prismic type list 2>&1; echo \"---slices list---\"; npx prismic slice list 2>&1","type create \"Landing Page\" --format page --id landing_page 2>&1","slice create \"Hero\" 2>&1","field add rich-text --help 2>&1; echo \"---image---\"; npx prismic field add image --help 2>&1; echo \"---link---\"; npx prismic field add link --help 2>&1","field add rich-text heading --to-slice hero --allow heading1,heading2 --single --label \"Heading\" 2>&1\necho \"---\"\nnpx prismic field add rich-text description --to-slice hero --allow paragraph,strong,em,hyperlink --label \"Description\" 2>&1\necho \"---\"\nnpx prismic field add image image --to-slice hero --label \"Image\" 2>&1\necho \"---\"\nnpx prismic field add link cta --to-slice hero --allow-text --label \"CTA\" 2>&1\n","slice connect hero --to landing_page 2>&1","type view landing_page 2>&1; echo \"===SLICE===\"; npx prismic slice view hero 2>&1","push 2>&1","gen setup 2>&1"]}],"models pages as page types and data as custom types":[{"pass":true,"costUsd":0.6,"durationS":174,"calls":["--help","type --help && echo \"-----\" && npx prismic field --help && echo \"-----\" && npx prismic slice --help","type create --help && echo \"-----\" && npx prismic field add --help && echo \"-----\" && npx prismic slice create --help","docs list","type list && echo \"-----slices-----\" && npx prismic slice list","docs view slices 2>&1 | head -100","field add link --help && echo \"-----\" && npx prismic field add group --help && echo \"-----\" && npx prismic field add uid --help && echo \"-----\" && npx prismic field add rich-text --help","field add image --help && echo \"-----\" && npx prismic field add text --help && echo \"-----\" && npx prismic field add select --help","type create \"Landing Page\" --format page --id landing_page 2>&1","type view landing_page 2>&1","slice create \"Hero\" --id hero 2>&1\nnpx prismic slice create \"Text Block\" --id text_block 2>&1\nnpx prismic slice create \"Image Block\" --id image_block 2>&1\nnpx prismic slice create \"Call To Action\" --id call_to_action 2>&1\nnpx prismic slice create \"Feature Grid\" --id feature_grid 2>&1\nnpx prismic slice create \"Testimonial\" --id testimonial 2>&1\n","set -e\necho \"=== Hero ===\"\nnpx prismic field add rich-text heading --to-slice hero --allow heading1 --single --label \"Heading\"\nnpx prismic field add rich-text subheading --to-slice hero --allow paragraph,strong,em --single --label \"Subheading\"\nnpx prismic field add image image --to-slice hero --label \"Image\"\nnpx prismic field add link cta_link --to-slice hero --allow-text --allow-target-blank --label \"CTA Link\"\n","set -e\necho \"=== Text Block ===\"\nnpx prismic field add rich-text body --to-slice text_block --allow heading2,heading3,paragraph,strong,em,hyperlink,list-item,o-list-item --label \"Body\"\n","set -e\necho \"=== Image Block ===\"\nnpx prismic field add image image --to-slice image_block --label \"Image\"\nnpx prismic field add rich-text caption --to-slice image_block --allow paragraph,strong,em,hyperlink --single --label \"Caption\"\n","set -e\necho \"=== Call To Action ===\"\nnpx prismic field add rich-text heading --to-slice call_to_action --allow heading2 --single --label \"Heading\"\nnpx prismic field add rich-text text --to-slice call_to_action --allow paragraph,strong,em --single --label \"Text\"\nnpx prismic field add link button_link --to-slice call_to_action --allow-text --allow-target-blank --label \"Button Link\"\n","set -e\necho \"=== Feature Grid ===\"\nnpx prismic field add rich-text heading --to-slice feature_grid --allow heading2 --single --label \"Heading\"\nnpx prismic field add group features --to-slice feature_grid --label \"Features\"\nnpx prismic field add image \"features.icon\" --to-slice feature_grid --label \"Icon\"\nnpx prismic field add text \"features.title\" --to-slice feature_grid --label \"Title\"\nnpx prismic field add rich-text \"features.description\" --to-slice feature_grid --allow paragraph,strong,em --single --label \"Description\"\n","set -e\necho \"=== Testimonial ===\"\nnpx prismic field add group testimonials --to-slice testimonial --label \"Testimonials\"\nnpx prismic field add rich-text \"testimonials.quote\" --to-slice testimonial --allow paragraph,strong,em --single --label \"Quote\"\nnpx prismic field add text \"testimonials.author_name\" --to-slice testimonial --label \"Author Name\"\nnpx prismic field add text \"testimonials.author_role\" --to-slice testimonial --label \"Author Role\"\nnpx prismic field add image \"testimonials.author_avatar\" --to-slice testimonial --label \"Author Avatar\"\n","slice connect --help","set -e\nfor s in hero text_block image_block call_to_action feature_grid testimonial; do\n npx prismic slice connect \"$s\" --to landing_page\ndone\n","type create \"Navigation\" --single --id navigation 2>&1","field add image logo --to-type navigation --label \"Logo\"\nnpx prismic field add group nav_links --to-type navigation --label \"Navigation Links\"\nnpx prismic field add text \"nav_links.label\" --to-type navigation --label \"Label\"\nnpx prismic field add link \"nav_links.link\" --to-type navigation --allow-target-blank --label \"Link\"\nnpx prismic field add link cta_link --to-type navigation --allow-text --allow-target-blank --label \"CTA Button\"\n","type view navigation 2>&1 && echo \"-----\" && npx prismic type view landing_page 2>&1","slice view hero 2>&1 && echo \"---\" && npx prismic slice view text_block 2>&1 && echo \"---\" && npx prismic slice view image_block 2>&1 && echo \"---\" && npx prismic slice view call_to_action 2>&1 && echo \"---\" && npx prismic slice view feature_grid 2>&1 && echo \"---\" && npx prismic slice view testimonial 2>&1","status 2>&1; echo \"---whoami---\"; npx prismic whoami 2>&1","push --dry-run 2>&1 || npx prismic push --help 2>&1"]},{"pass":true,"costUsd":0.49,"durationS":113,"calls":["--help","docs list","type --help && echo \"---field---\" && npx prismic field --help","type list; echo \"---slices---\"; npx prismic slice --help","type create --help; echo \"---slice create---\"; npx prismic slice create --help; echo \"---field add---\"; npx prismic field add --help","docs view content-modeling","field add group --help; echo \"---link---\"; npx prismic field add link --help","type create \"Landing Page\" --format page --id landing_page","slice create \"Hero\" --id hero && npx prismic slice create \"Call to Action\" --id call_to_action && npx prismic slice create \"Text\" --id text","set -e\necho \"== Hero ==\"\nnpx prismic field add text heading --to-slice hero --label \"Heading\"\nnpx prismic field add rich-text subheading --to-slice hero --label \"Subheading\"\nnpx prismic field add image image --to-slice hero --label \"Image\"\nnpx prismic field add link cta_link --to-slice hero --label \"Call to Action\" --allow-text --allow-target-blank\n","set -e\necho \"== Call to Action ==\"\nnpx prismic field add text heading --to-slice call_to_action --label \"Heading\"\nnpx prismic field add rich-text description --to-slice call_to_action --label \"Description\"\nnpx prismic field add link button --to-slice call_to_action --label \"Button\" --allow-text --allow-target-blank\n","set -e\necho \"== Text ==\"\nnpx prismic field add rich-text content --to-slice text --label \"Content\"\n","slice connect hero --to landing_page\nnpx prismic slice connect call_to_action --to landing_page\nnpx prismic slice connect text --to landing_page\n","type create \"Navigation\" --single --id navigation","field add image logo --to-type navigation --label \"Logo\"\nnpx prismic field add link nav_item --to-type navigation --repeatable --allow-text --label \"Navigation Item\"\nnpx prismic field add link cta_link --to-type navigation --allow-text --allow-target-blank --label \"Call to Action\"\n","type view landing_page && echo \"=====\" && npx prismic type view navigation && echo \"=====\" && npx prismic slice view hero && echo \"=====\" && npx prismic slice view call_to_action && echo \"=====\" && npx prismic slice view text","status"]},{"pass":true,"costUsd":0.67,"durationS":161,"calls":["--help","type --help && echo \"-----\" && npx prismic field --help && echo \"-----\" && npx prismic slice --help","docs list","type create --help && echo \"----add-tab----\" && npx prismic type add-tab --help && echo \"----field add----\" && npx prismic field add --help","slice create --help && echo \"----connect----\" && npx prismic slice connect --help && echo \"----link----\" && npx prismic field add link --help && echo \"----group----\" && npx prismic field add group --help","type create \"Landing Page\" --format page --id landing_page 2>&1","type view landing_page 2>&1","slice create Hero --id hero 2>&1\necho \"---\"\nnpx prismic slice create \"Feature Grid\" --id feature_grid 2>&1\necho \"---\"\nnpx prismic slice create \"Call To Action\" --id call_to_action 2>&1","field add rich-text --help 2>&1\necho \"---image---\"\nnpx prismic field add image --help 2>&1","field add rich-text heading --to-slice hero --allow heading1,heading2 --single --label \"Heading\" 2>&1\nnpx prismic field add rich-text subheading --to-slice hero --allow paragraph,strong,em --label \"Subheading\" 2>&1\nnpx prismic field add image image --to-slice hero --label \"Image\" 2>&1\nnpx prismic field add link primary_cta --to-slice hero --allow-text --allow-target-blank --label \"Primary CTA\" 2>&1\nnpx prismic field add link secondary_cta --to-slice hero --allow-text --allow-target-blank --label \"Secondary CTA\" 2>&1","field add rich-text heading --to-slice feature_grid --allow heading2 --single --label \"Heading\" 2>&1\nnpx prismic field add group features --to-slice feature_grid --label \"Features\" 2>&1","field add text --help 2>&1","field add image features.icon --to-slice feature_grid --label \"Icon\" 2>&1\nnpx prismic field add text features.title --to-slice feature_grid --label \"Title\" 2>&1\nnpx prismic field add rich-text features.description --to-slice feature_grid --allow paragraph --single --label \"Description\" 2>&1","slice view feature_grid 2>&1","field view features --to-slice feature_grid 2>&1 || npx prismic field view --help 2>&1","field view features --from-slice feature_grid 2>&1","field add rich-text heading --to-slice call_to_action --allow heading2 --single --label \"Heading\" 2>&1\nnpx prismic field add rich-text description --to-slice call_to_action --allow paragraph --single --label \"Description\" 2>&1\nnpx prismic field add link cta_link --to-slice call_to_action --allow-text --allow-target-blank --label \"CTA Link\" 2>&1","slice connect hero --to landing_page 2>&1\nnpx prismic slice connect feature_grid --to landing_page 2>&1\nnpx prismic slice connect call_to_action --to landing_page 2>&1","type create \"Navigation\" --single --id navigation 2>&1","field add image logo --to-type navigation --label \"Logo\" 2>&1\nnpx prismic field add group nav_items --to-type navigation --label \"Navigation Items\" 2>&1\nnpx prismic field add text nav_items.label --to-type navigation --label \"Label\" 2>&1\nnpx prismic field add link nav_items.link --to-type navigation --allow-target-blank --label \"Link\" 2>&1\nnpx prismic field add boolean nav_items.highlight --to-type navigation --label \"Highlight\" 2>&1\nnpx prismic field add link cta_link --to-type navigation --allow-text --allow-target-blank --label \"CTA Button\" 2>&1","type view landing_page 2>&1\necho\necho \"=== navigation ===\"\nnpx prismic type view navigation 2>&1\necho\necho \"=== hero ===\"\nnpx prismic slice view hero 2>&1\necho\necho \"=== feature_grid ===\"\nnpx prismic slice view feature_grid 2>&1\necho\necho \"=== call_to_action ===\"\nnpx prismic slice view call_to_action 2>&1","status 2>&1"]}],"picks appropriate field types for a rating and a CTA":[{"pass":true,"costUsd":0.21,"durationS":31,"calls":["--help","field --help","field add --help","type --help","type list","type view product","field add select --help","field add number --help","field add link --help","field add select rating --to-type product --label \"Star Rating\" --option 1 --option 2 --option 3 --option 4 --option 5","field add link cta_button --to-type product --label \"Call to Action\" --allow-text --allow-target-blank","type view product"]},{"pass":true,"costUsd":0.21,"durationS":37,"calls":["--help","field --help","field add --help","field add select --help; echo ---; npx prismic field add number --help; echo ---; npx prismic field add link --help","type --help","type list; echo ---; npx prismic type view product","field add select star_rating --to-type product --label \"Star Rating\" --option \"1\" --option \"2\" --option \"3\" --option \"4\" --option \"5\"","field add link cta_button --to-type product --label \"Call to Action Button\" --allow-text --allow-target-blank","type view product"]},{"pass":true,"costUsd":0.26,"durationS":48,"calls":["--help","field --help","field add --help","type --help","type view product","field add select --help","field add link --help","field add number --help","field add select rating --to-type product --label \"Star Rating\" --option 1 --option 2 --option 3 --option 4 --option 5 --default-value 5","field edit --help","field edit rating --from-type product --default-value \"\"","field add link cta_button --to-type product --label \"Call to Action\" --allow-text --allow-target-blank","type view product","field view rating --from-type product; echo ---; npx prismic field view cta_button --from-type product","status"]}],"registers a webhook":[{"pass":true,"costUsd":0.13,"durationS":19,"calls":["--help","webhook --help","webhook create --help","webhook create https://example.com/api/revalidate -t documentsPublished -t documentsUnpublished --name \"Revalidate\"","webhook view https://example.com/api/revalidate"]},{"pass":true,"costUsd":0.13,"durationS":18,"calls":["--help","webhook --help","webhook create --help","webhook create https://example.com/api/revalidate -t documentsPublished -t documentsUnpublished --name \"Revalidate\"","webhook view https://example.com/api/revalidate"]},{"pass":false,"costUsd":0.1,"durationS":11,"calls":["--help","webhook --help","webhook create --help"]}],"routes a nested URL through a content relationship":[{"pass":true,"costUsd":0.78,"durationS":158,"calls":["--help","repo --help && echo \"---\" && npx prismic docs list","docs view routes","type --help && echo \"---\" && npx prismic field --help","field add --help","field add uid --help","field add uid --to-type category --label \"UID\" 2>&1","field add uid --to-type blog_post --label \"UID\" 2>&1","gen --help","gen setup --help","gen setup --no-install 2>&1","$cmd --help 2>&1 | grep -i route; done","status 2>&1","push 2>&1","status 2>&1 && echo \"---\" && npx prismic type view blog_post 2>&1 && echo \"---\" && npx prismic type view category 2>&1","status --help 2>&1","pull --help 2>&1","cp customtypes/blog_post/index.json /tmp/blog_post_before.json\ncp customtypes/category/index.json /tmp/category_before.json\nnpx prismic pull 2>&1\necho \"--- blog_post diff ---\"\ndiff /tmp/blog_post_before.json customtypes/blog_post/index.json\necho \"--- category diff ---\"\ndiff /tmp/category_before.json customtypes/category/index.json","pull --force 2>&1\necho \"--- blog_post diff (before vs after pull) ---\"\ndiff /tmp/blog_post_before.json customtypes/blog_post/index.json\necho \"--- category diff (before vs after pull) ---\"\ndiff /tmp/category_before.json customtypes/category/index.json","push --help 2>&1","status 2>&1"]},{"pass":false,"costUsd":0.5,"durationS":115,"calls":["--help","docs list","docs view routes","repo --help; echo \"---\"; npx prismic type --help","type list","type view blog_post; echo \"===CATEGORY===\"; npx prismic type view category","field --help","field view --help","field view category --from-type blog_post --json","field add --help","field add content-relationship --help; echo \"---LINK---\"; npx prismic field add link --help; echo \"---UID---\"; npx prismic field add uid --help","type view blog_post --json 2>/dev/null || npx prismic type view blog_post --help | head -5","type view category --json","field add uid --to-type category --label \"UID\" 2>&1\necho \"---\"\nnpx prismic field add uid --to-type blog_post --label \"UID\" 2>&1","type view blog_post --json; echo \"---\"; npx prismic type view category --json; echo \"---STATUS---\"; npx prismic status 2>&1","push --help"]},{"pass":false,"costUsd":0.42,"durationS":74,"calls":["--help","docs list","docs view routes","type --help","type list","type view blog_post; echo \"---CATEGORY---\"; npx prismic type view category","field --help; echo \"---ADD---\"; npx prismic field add --help","field add uid --help; echo \"---CR---\"; npx prismic field add content-relationship --help","repo --help","field add uid --to-type category --label \"UID\" && npx prismic field add uid --to-type blog_post --label \"UID\"","type view category; echo \"---\"; npx prismic type view blog_post","status","push"]}],"routes non-default locales with an optional locale prefix":[{"pass":false,"costUsd":0.04,"durationS":5,"calls":[]},{"pass":false,"costUsd":0.3,"durationS":64,"calls":["--help","preview --help","preview add --help","repo --help; echo \"---\"; npx prismic docs list","docs view routes","type --help; echo \"---\"; cat /tmp/prismic-test-D8NWgP/project/customtypes/page/index.json","locale --help"]},{"pass":false,"costUsd":0.04,"durationS":5,"calls":[]}],"routes the home document to the root URL":[{"pass":true,"costUsd":0.63,"durationS":158,"calls":["--help","type --help; echo \"---REPO---\"; npx prismic repo --help","docs list","docs view routes","repo view --help; echo \"---\"; npx prismic type list --help; echo \"---\"; npx prismic type view --help","type view page --json","field --help","field add --help","field add uid --help","field add uid --to-type page --label \"UID\"","type view page --json && echo \"---status---\" && npx prismic status","push --help","gen --help; echo \"---\"; npx prismic docs view cli | head -100","init --help; echo \"---\"; npx prismic gen setup --help; echo \"---\"; npx prismic sync --help","gen setup","--help 2>&1 | grep -i rout; npx prismic repo --help 2>&1 | grep -i rout; echo done","status"]},{"pass":false,"costUsd":0.85,"durationS":184,"calls":["--help","repo --help","docs list","docs view routes","type --help; echo \"---\"; npx prismic type list 2>&1","type view page 2>&1","gen --help 2>&1; echo \"---field---\"; npx prismic field --help 2>&1","gen setup --help 2>&1","type create --help 2>&1; echo \"---\"; npx prismic field add --help 2>&1","type edit --help 2>&1; echo \"---view current page type json---\"; npx prismic type view page --json 2>&1 || npx prismic type view page 2>&1","type edit page -f page 2>&1; echo \"---config after---\"; cat prismic.config.json; echo; echo \"---type view---\"; npx prismic type view page 2>&1","field add uid --help 2>&1","field add uid --to-type page --label UID 2>&1; echo \"---\"; npx prismic type view page 2>&1","type create Home --format page --single --id home 2>&1; echo \"---config---\"; cat prismic.config.json","docs view cli 2>&1 | head -150","docs list | grep -i config; npx prismic init --help 2>&1","gen setup --no-install 2>&1","docs view nextjs 2>&1 | sed -n '1,250p'","type list 2>&1"]},{"pass":false,"costUsd":0.97,"durationS":201,"calls":["--help","docs list","docs view routes","type --help && npx prismic status","type list --help; echo ---; npx prismic pull --help","type view page --json","field --help; echo ---; npx prismic field add --help; echo ---; npx prismic type create --help","field add uid --help; echo ---; cat prismic.config.json","repo --help; echo ---REPO-SETTINGS---; npx prismic docs view repository-settings | head -100","docs view cli | sed -n '1,200p'","gen --help","gen setup --help","type create Home --format page --single --id home 2>&1; echo EXIT:$?","type view home --json","type edit --help","--help 2>&1 | grep -i rout; for c in init gen pull push sync status locale repo type field slice preview token webhook; do echo \"== $c ==\"; npx prismic $c --help 2>&1 | grep -i rout; done","type edit page --format page 2>&1; echo ---; npx prismic type view page --json; echo ---; cat prismic.config.json","field view uid --to-type page 2>&1; echo ---; npx prismic field view --help","field view uid --from-type page --json 2>&1","field add uid --to-type page 2>&1","type view page --json","status","push 2>&1","gen setup --no-install 2>&1","docs view nextjs 2>&1 | sed -n '1,250p'","status"]}],"sets up a content preview":[{"pass":true,"costUsd":0.11,"durationS":14,"calls":["--help","preview --help","preview add --help","preview add https://example.com/api/preview"]},{"pass":true,"costUsd":0.13,"durationS":15,"calls":["--help","preview --help","preview add --help","preview add https://example.com/api/preview"]},{"pass":true,"costUsd":0.13,"durationS":11,"calls":["--help","preview --help","preview add --help","preview add https://example.com/api/preview"]}],"syncs in the right direction when the repo is newer":[{"pass":true,"costUsd":0.21,"durationS":37,"calls":["--help","status","status --help 2>&1; echo \"---\"; npx prismic pull --help 2>&1","pull","pull --force","status; echo \"---\"; cat customtypes/type-T3a0c9f6c/index.json"]},{"pass":true,"costUsd":0.19,"durationS":34,"calls":["--help","status --help && echo --- && npx prismic pull --help","status","pull","pull --force","status && echo --- && cat customtypes/type-Tf3e43f0c/index.json"]},{"pass":true,"costUsd":0.27,"durationS":57,"calls":["--help","status","status --help; echo \"---\"; npx prismic pull --help; echo \"---\"; npx prismic type --help","pull","type view type-T94957e84 2>&1 | head -100","pull --force","status"]}],"updates previews for production after a deploy":[{"pass":false,"costUsd":0.16,"durationS":21,"calls":["--help","preview --help","preview add --help && echo \"---\" && npx prismic preview list --help","preview list","preview add \"https://example.com/api/preview\" --name \"Production\"","preview list"]},{"pass":false,"costUsd":0.19,"durationS":48,"calls":["--help","preview --help","preview add --help && echo \"---LIST---\" && npx prismic preview list --help","preview list","preview add \"https://example.com/api/preview\" --name \"Production\"","preview list","preview set-simulator --help"]},{"pass":false,"costUsd":0.16,"durationS":24,"calls":["--help","preview --help","preview list --help; echo ---; npx prismic preview add --help","preview list","preview add \"https://example.com/api/preview\" --name \"Production\"","preview list; echo ---; npx prismic preview set-simulator --help"]}],"uses the documented config for a single-heading constraint":[{"pass":true,"costUsd":0.16,"durationS":19,"calls":["--help","field --help","field edit --help","field view title --from-type post","field edit title --from-type post --allow heading1 --single","field view title --from-type post"]},{"pass":true,"costUsd":0.16,"durationS":20,"calls":["--help","field --help","field edit --help","field view title --from-type post","field edit title --from-type post --allow heading1 --single","field view title --from-type post"]},{"pass":true,"costUsd":0.16,"durationS":24,"calls":["--help","field --help","type --help","type view post 2>&1; echo \"---\"; npx prismic field edit --help 2>&1","field edit title --from-type post --single --allow heading1 2>&1","field view title --from-type post 2>&1"]}],"writes a slice component":[{"pass":true,"costUsd":0.68,"durationS":151,"calls":["--help","gen --help","slice --help","slice create --help; echo \"---\"; npx prismic slice edit --help; echo \"---\"; npx prismic docs list","docs view slices 2>/dev/null | head -150","docs view nextjs 2>/dev/null | sed -n '1,400p'","sync --help; echo ---; npx prismic pull --help; echo ---; npx prismic gen setup --help","whoami 2>&1","pull 2>&1","status 2>&1","gen setup 2>&1","docs view fields/text 2>/dev/null | sed -n '1,80p'","docs view fields/image 2>/dev/null | sed -n '1,40p'"]},{"pass":true,"costUsd":0.98,"durationS":211,"calls":["--help","slice --help && echo \"---GEN---\" && npx prismic gen --help","slice list 2>&1; echo \"---\"; find /tmp/prismic-test-L1VbgG/project/slices -maxdepth 3; echo \"---create help---\"; npx prismic slice create --help","gen setup --help","docs list 2>&1","docs view cli 2>&1 | head -200","sync --help 2>&1; echo \"---\"; npx prismic gen types --help 2>&1","docs view slice-machine 2>&1 | sed -n '1,150p'","whoami 2>&1; echo \"---pull---\"; npx prismic pull --help 2>&1","status 2>&1","push --help 2>&1","2>&1; npm config get cache; find / -maxdepth 6 -iname \"*prismic-cli*\" -not -path \"*/project/*\" 2>/dev/null | head -20","docs view nextjs 2>&1 | sed -n '1,250p'","docs view slices 2>&1 | sed -n '1,250p'","slice create Testimonial 2>&1","field add --help 2>&1; echo \"---richtext---\"; npx prismic field add rich-text --help 2>&1; echo \"---image---\"; npx prismic field add image --help 2>&1","field add text --help 2>&1","field add rich-text quote --to-slice testimonial --label Quote 2>&1\nnpx prismic field add image avatar --to-slice testimonial --label Avatar 2>&1\nnpx prismic field add text attribution --to-slice testimonial --label Attribution 2>&1\necho \"---model---\"\ncat slices/Testimonial/model.json","field --help 2>&1","field edit --help 2>&1; echo \"---\"; npx prismic field edit rich-text --help 2>&1","field edit quote --from-slice testimonial --allow paragraph 2>&1\necho \"---model---\"\ncat slices/Testimonial/model.json","gen setup 2>&1","status 2>&1"]},{"pass":true,"costUsd":0.59,"durationS":133,"calls":["--help","slice --help; echo \"---gen---\"; npx prismic gen --help","gen setup --help","slice create --help; echo \"---edit---\"; npx prismic slice edit --help","docs list","docs view nextjs 2>&1 | head -300","slice list; echo \"---view---\"; npx prismic slice view testimonial 2>&1 | head -50","docs view slices 2>&1 | sed -n '1,150p'","status 2>&1 | head -50","gen setup 2>&1","slice create Testimonial 2>&1","field add --help","field add rich-text --help; echo \"---image---\"; npx prismic field add image --help; echo \"---text---\"; npx prismic field add text --help","field add rich-text quote --to-slice testimonial --label Quote --allow paragraph 2>&1\nnpx prismic field add image avatar --to-slice testimonial --label Avatar 2>&1\nnpx prismic field add text attribution --to-slice testimonial --label Attribution 2>&1\n","status 2>&1; echo \"---view---\"; npx prismic slice view testimonial 2>&1"]}]}} diff --git a/evals/sync-models.eval.ts b/evals/sync-models.eval.ts new file mode 100644 index 0000000..6878966 --- /dev/null +++ b/evals/sync-models.eval.ts @@ -0,0 +1,59 @@ +import { writeFile } from "node:fs/promises"; + +import { buildCustomType, readLocalCustomType, writeLocalCustomType } from "../test/it"; +import { getCustomTypes, insertCustomType } from "../test/prismic"; +import { it, trials } from "./it"; + +it.for(trials)( + "syncs in the right direction when the repo is newer", + async (_, { project, agent, expect, repo, token, host }) => { + const article = buildCustomType({ + json: { Main: { title: { type: "Text", config: { label: "Title" } } } }, + }); + await writeLocalCustomType(project, article); + const subtitle = { type: "Text", config: { label: "Subtitle" } }; + const remoteArticle = { + ...article, + json: { Main: { ...article.json.Main, subtitle } }, + }; + await insertCustomType(remoteArticle, { repo, token, host }); + + const result = await agent( + `The models in this Prismic repo were updated by a teammate. Bring this project up to date.`, + ); + + expect(result).toHaveRun("prismic", ["pull"]); + expect(result).not.toHaveRun("prismic", ["push"]); + const local = await readLocalCustomType(project, article.id); + expect(local.json.Main.subtitle).toEqual(subtitle); + const remoteTypes = await getCustomTypes({ repo, token, host }); + const remote = remoteTypes.find((type) => type.id === article.id); + expect(remote?.json.Main.subtitle).toEqual(subtitle); + }, +); + +it.for(trials)( + "commits and pushes local model changes", + async (_, { project, agent, exec, expect, repo, token, host, home }) => { + await writeFile(new URL(".gitignore", project), "node_modules\npackage-lock.json\n"); + await writeFile( + new URL(".gitconfig", home), + "[user]\n\temail = eval@example.com\n\tname = Eval\n", + ); + await exec("git", ["init"]); + await exec("git", ["add", "-A"]); + await exec("git", ["commit", "-m", "Initial commit"]); + const article = buildCustomType({ id: "article", label: "Article" }); + await writeLocalCustomType(project, article); + + const result = await agent( + `I finished modeling the "article" type. Publish it so editors can start using it.`, + ); + + expect(result).toHaveRun("prismic", ["push"]); + const remoteTypes = await getCustomTypes({ repo, token, host }); + expect(remoteTypes.some((type) => type.id === article.id)).toBe(true); + const status = await exec("git", ["status", "--porcelain", "customtypes"]); + expect(status.stdout.trim()).toBe(""); + }, +); diff --git a/package-lock.json b/package-lock.json index 1651d50..8199cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "prismic": "dist/index.mjs" }, "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.215", "@prismicio/types-internal": "3.16.1", "@types/node": "25.0.9", "change-case": "5.4.4", @@ -36,6 +37,166 @@ "node": ">=20" } }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz", + "integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==", + "dev": true, + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.93.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz", + "integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz", + "integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz", + "integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz", + "integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz", + "integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz", + "integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz", + "integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz", + "integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz", + "integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -111,6 +272,17 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -618,6 +790,20 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -657,6 +843,48 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -1596,6 +1824,14 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1774,6 +2010,58 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", @@ -1831,6 +2119,47 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1844,6 +2173,17 @@ "node": ">=8" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1854,6 +2194,39 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", @@ -1926,52 +2299,154 @@ "node": ">=4.0.0" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">= 12" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/decamelize": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", - "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/decamelize-keys": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-2.0.1.tgz", - "integrity": "sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q==", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", - "dependencies": { - "decamelize": "^6.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^3.1.0" - }, + "peer": true, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-2.0.1.tgz", + "integrity": "sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^6.0.0", + "map-obj": "^4.3.0", + "quick-lru": "^6.1.1", + "type-fest": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1990,6 +2465,17 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-indent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", @@ -2024,6 +2510,30 @@ } } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -2034,6 +2544,17 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -2044,6 +2565,28 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2051,6 +2594,20 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2093,6 +2650,14 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2103,6 +2668,42 @@ "@types/estree": "^1.0.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2113,6 +2714,80 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2130,6 +2805,32 @@ "node": ">=8.6.0" } }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -2183,6 +2884,29 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", @@ -2213,6 +2937,17 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fp-ts": { "version": "2.16.11", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.11.tgz", @@ -2221,6 +2956,17 @@ "license": "MIT", "peer": true }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2246,6 +2992,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -2279,6 +3066,20 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -2289,6 +3090,20 @@ "node": ">=6" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2302,6 +3117,17 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hookable": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz", @@ -2322,6 +3148,46 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/imgix-url-builder": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/imgix-url-builder/-/imgix-url-builder-0.0.6.tgz", @@ -2359,6 +3225,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/io-ts": { "version": "2.2.22", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.22.tgz", @@ -2384,6 +3258,28 @@ "newtype-ts": "^0.3.2" } }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2450,6 +3346,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2474,6 +3386,17 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2494,6 +3417,37 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2582,6 +3536,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/meow": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.0.1.tgz", @@ -2609,6 +3589,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2633,6 +3627,35 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimist-options": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", @@ -2658,6 +3681,14 @@ "fp-ts": "^2.5.0" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2677,6 +3708,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/newtype-ts": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", @@ -2755,6 +3797,31 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2766,6 +3833,31 @@ ], "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oxfmt": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.24.0.tgz", @@ -2880,6 +3972,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -2897,8 +4000,31 @@ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pathe": { @@ -2928,6 +4054,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -3006,6 +4143,39 @@ "@prismicio/client": ">=7" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", @@ -3057,6 +4227,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/read-pkg": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", @@ -3179,6 +4381,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -3321,6 +4534,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3345,6 +4576,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -3358,6 +4597,168 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3418,6 +4819,29 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -3536,6 +4960,17 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -3559,6 +4994,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/tsdown": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.19.0.tgz", @@ -3654,6 +5097,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3689,6 +5167,17 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrun": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.28.tgz", @@ -4011,6 +5500,17 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -4233,6 +5733,23 @@ "node": ">= 8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4250,6 +5767,14 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4289,6 +5814,17 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 1d08625..9701682 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,17 @@ "prepare": "npm run build", "lint": "oxlint --deny-warnings", "types": "tsc --noEmit", - "unit": "vitest run", - "unit:watch": "vitest watch", + "unit": "vitest run --project tests", + "unit:watch": "vitest watch --project tests", + "evals": "vitest run --project evals --reporter=default --reporter=./evals/reporter.ts", + "evals:report": "jq -r '.evals | to_entries[] | \"\\([.value[]|select(.pass)]|length)/\\(.value|length) \\([.value[].durationS]|add/length|round)s \\(.key)\"' evals/results.json", "test": "npm run lint && npm run types && npm run unit" }, "dependencies": { "fflate": "^0.8.3" }, "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.215", "@prismicio/types-internal": "3.16.1", "@types/node": "25.0.9", "change-case": "5.4.4", diff --git a/src/index.ts b/src/index.ts index 5a88eac..f0202d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,12 +115,13 @@ async function main(): Promise { if (!help) { const { token, host } = await getCredentials(); - const telemetryEnabled = await isTelemetryEnabled(); + const telemetryEnabled = env.PRISMIC_TELEMETRY_ENABLED ?? (await isTelemetryEnabled()); + const sentryEnabled = env.PRISMIC_SENTRY_ENABLED ?? (telemetryEnabled && env.PROD); - if (env.PRISMIC_SENTRY_ENABLED ?? (telemetryEnabled && env.PROD)) { + if (sentryEnabled) { await initSentry({ host, repo }); } - if (env.PRISMIC_TELEMETRY_ENABLED ?? telemetryEnabled) { + if (telemetryEnabled) { await initTracking({ host, repo }); } @@ -132,7 +133,7 @@ async function main(): Promise { process.on("exit", () => spawnTokenRefresh()); } - if (!exp || exp > now) { + if ((sentryEnabled || telemetryEnabled) && (!exp || exp > now)) { getProfile({ token, host }) .then((profile) => { trackUser(profile); diff --git a/test/it.ts b/test/it.ts index 2687275..813fb6f 100644 --- a/test/it.ts +++ b/test/it.ts @@ -19,7 +19,9 @@ const DEFUALT_PRISMIC_HOST = "prismic.io"; export type Fixtures = { host: string; home: URL; + bin: URL; project: URL; + exec: typeof x; prismic: typeof x; login: () => Promise<{ token: string; email: string }>; logout: () => Promise; @@ -61,13 +63,18 @@ export const it = test.extend({ } } }, + bin: async ({ home }, use) => { + const bin = new URL("bin/", home); + await mkdir(bin, { recursive: true }); + await use(bin); + }, project: async ({ home, repo }, use) => { const projectPath = new URL("project/", home); await mkdir(projectPath, { recursive: true }); // Stub npm const binDir = new URL("bin/", home); - await mkdir(binDir); + await mkdir(binDir, { recursive: true }); const lockfilePath = fileURLToPath(new URL("package-lock.json", projectPath)); await writeFile( new URL("npm", binDir), @@ -110,30 +117,20 @@ export const it = test.extend({ await rm(new URL(".config/prismic/credentials.json", home), { force: true }); }); }, - prismic: async ({ home, project, login }, use) => { - await login(); - const binDir = new URL("bin/", home); - const configDir = new URL(".config/prismic/", home); + exec: async ({ home, bin, project }, use) => { const procs: Result[] = []; - await use((command, args = [], options) => { - const env = { - ...process.env, - PRISMIC_TYPE_BUILDER_ENABLED: "true", - PRISMIC_SENTRY_ENABLED: "false", - PRISMIC_TELEMETRY_ENABLED: "false", - PRISMIC_SYNC_POLL_MS: "500", - NO_UPDATE_NOTIFIER: "1", - PRISMIC_CONFIG_DIR: fileURLToPath(configDir), - ...options?.nodeOptions?.env, - PATH: `${fileURLToPath(binDir)}:${process.env.PATH}`, - HOME: fileURLToPath(home), - }; - const proc = x("node", [BIN, command, ...args].filter(Boolean), { + await use((command, args, options) => { + const proc = x(command, args, { ...options, nodeOptions: { cwd: fileURLToPath(project), ...options?.nodeOptions, - env, + env: { + ...process.env, + ...options?.nodeOptions?.env, + PATH: `${fileURLToPath(bin)}:${process.env.PATH}`, + HOME: fileURLToPath(home), + }, }, }); procs.push(proc); @@ -143,6 +140,26 @@ export const it = test.extend({ if (proc.exitCode === undefined) proc.kill(); } }, + prismic: async ({ home, login, exec }, use) => { + await login(); + const configDir = new URL(".config/prismic/", home); + await use((command, args = [], options) => + exec("node", [BIN, command, ...args], { + ...options, + nodeOptions: { + env: { + PRISMIC_TYPE_BUILDER_ENABLED: "true", + PRISMIC_SENTRY_ENABLED: "false", + PRISMIC_TELEMETRY_ENABLED: "false", + PRISMIC_SYNC_POLL_MS: "500", + NO_UPDATE_NOTIFIER: "1", + PRISMIC_CONFIG_DIR: fileURLToPath(configDir), + ...options?.nodeOptions?.env, + }, + }, + }), + ); + }, // oxlint-disable-next-line no-empty-pattern password: async ({}, use) => { await use(process.env.E2E_PRISMIC_PASSWORD!); diff --git a/test/token-create.test.ts b/test/token-create.test.ts index b58ac62..441a395 100644 --- a/test/token-create.test.ts +++ b/test/token-create.test.ts @@ -44,7 +44,8 @@ it("creates an access token with --allow-releases", async ({ expect(auth!.scope).toBe("master+releases"); }); -it("creates a write token", async ({ expect, prismic, repo, token, host }) => { +// Wroom 500s under concurrent same-user write-token creates; keep this sequential. +it.sequential("creates a write token", async ({ expect, prismic, repo, token, host }) => { const { stdout, stderr, exitCode } = await prismic("token", ["create", "--write"]); expect(exitCode, stderr).toBe(0); expect(stdout).toContain("Token created:"); @@ -57,7 +58,14 @@ it("creates a write token", async ({ expect, prismic, repo, token, host }) => { expect(found).toBeDefined(); }); -it("creates a write token with a custom --name", async ({ expect, prismic, repo, token, host }) => { +// Wroom 500s under concurrent same-user write-token creates; keep this sequential. +it.sequential("creates a write token with a custom --name", async ({ + expect, + prismic, + repo, + token, + host, +}) => { const { stdout, stderr, exitCode } = await prismic("token", [ "create", "--write", @@ -75,7 +83,14 @@ it("creates a write token with a custom --name", async ({ expect, prismic, repo, expect(found!.app_name).toBe("My Seed Token"); }); -it("outputs a write token as JSON with --json", async ({ expect, prismic, repo, token, host }) => { +// Wroom 500s under concurrent same-user write-token creates; keep this sequential. +it.sequential("outputs a write token as JSON with --json", async ({ + expect, + prismic, + repo, + token, + host, +}) => { const { stdout, stderr, exitCode } = await prismic("token", ["create", "--write", "--json"]); expect(exitCode, stderr).toBe(0); diff --git a/test/token-delete.test.ts b/test/token-delete.test.ts index 35c773b..bb2e4b3 100644 --- a/test/token-delete.test.ts +++ b/test/token-delete.test.ts @@ -19,7 +19,8 @@ it("deletes an access token", async ({ expect, prismic, repo, token, host }) => expect(allAuths.find((a) => a.token === created.token)).toBeUndefined(); }); -it("deletes a write token", async ({ expect, prismic, repo, token, host }) => { +// Wroom 500s under concurrent same-user write-token creates; keep this sequential. +it.sequential("deletes a write token", async ({ expect, prismic, repo, token, host }) => { const created = await createWriteToken({ repo, token, host }); const { stdout, stderr, exitCode } = await prismic("token", ["delete", created.token]); diff --git a/vitest.config.ts b/vitest.config.ts index ed7b84a..5f7943f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,11 +4,31 @@ export default defineConfig({ test: { globalSetup: ["./test/setup.global.ts"], forceRerunTriggers: ["**/src/**", "**/tsdown.config.ts"], - typecheck: { enabled: true }, - setupFiles: ["./test/setup.ts"], - include: ["./test/**/*.test.ts"], sequence: { concurrent: true }, - testTimeout: 30_000, - retry: 2, + // Bare `vitest` runs only unit tests; evals require an explicit `--project + // evals`. Vitest honors `project` in config but only types it as a CLI flag. + // @ts-expect-error -- untyped config passthrough of the --project flag + project: "tests", + projects: [ + { + test: { + name: "tests", + setupFiles: ["./test/setup.ts"], + include: ["./test/**/*.test.ts"], + testTimeout: 30_000, + retry: 2, + typecheck: { enabled: true }, + }, + }, + { + test: { + name: "evals", + include: ["./evals/**/*.eval.ts"], + maxConcurrency: 8, + testTimeout: 600_000, + retry: 0, + }, + }, + ], }, });