@cimplify/cli 0.15.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/add-C3DPTVH3.mjs +140 -0
- package/dist/{add-KRIIKAS4.mjs → add-JX2GXWXU.mjs} +4 -4
- package/dist/ai-HUZTVJIQ.mjs +17 -0
- package/dist/{assets-CWB72PZG.mjs → assets-MPESJSUV.mjs} +12 -7
- package/dist/{auth-step-up-Z6KOTTV7.mjs → auth-step-up-VJAFGSW6.mjs} +5 -5
- package/dist/build-E7RRQWVF.mjs +616 -0
- package/dist/{cancel-ZHVVILOU.mjs → cancel-UMKR274A.mjs} +4 -4
- package/dist/capture-TK4NIUYM.mjs +69 -0
- package/dist/chunk-25ZPFHLF.mjs +51 -0
- package/dist/{chunk-OLZKNUMK.mjs → chunk-3LJG22FS.mjs} +6 -6
- package/dist/chunk-454RQ6NR.mjs +10614 -0
- package/dist/{chunk-O2TO2J4I.mjs → chunk-6CQMRZ2W.mjs} +2 -2
- package/dist/chunk-6KKLF5KG.mjs +32 -0
- package/dist/chunk-74QZQSCL.mjs +84 -0
- package/dist/chunk-BGM7CH5G.mjs +81 -0
- package/dist/chunk-BTNVYAC4.mjs +5557 -0
- package/dist/{chunk-I2UP7YTG.mjs → chunk-ECGDRU56.mjs} +1 -1
- package/dist/{chunk-UTD34C2H.mjs → chunk-EOOOXVHR.mjs} +2 -4
- package/dist/chunk-EYEMPIMP.mjs +74 -0
- package/dist/{chunk-3RPR33MO.mjs → chunk-FV273ECI.mjs} +11 -7
- package/dist/{chunk-MLE4MVYC.mjs → chunk-GGPN4QYQ.mjs} +2 -2
- package/dist/chunk-HQPZQIWW.mjs +1456 -0
- package/dist/{chunk-3QUK5BPY.mjs → chunk-I7GEHLDD.mjs} +16 -1
- package/dist/chunk-ITDAICBY.mjs +363 -0
- package/dist/chunk-JKECUAAV.mjs +2169 -0
- package/dist/chunk-JNCEKW2Y.mjs +41 -0
- package/dist/chunk-JTYVGORV.mjs +101 -0
- package/dist/chunk-KQNZKV6E.mjs +535 -0
- package/dist/chunk-LJDIQ4PF.mjs +6463 -0
- package/dist/{chunk-2MKZKUVH.mjs → chunk-LLRUOIMU.mjs} +1 -2
- package/dist/chunk-MJMRIMR7.mjs +86 -0
- package/dist/chunk-N2WZJX5P.mjs +1989 -0
- package/dist/chunk-NEB372WD.mjs +1152 -0
- package/dist/chunk-OPX7KPOU.mjs +68 -0
- package/dist/chunk-PHUBMRAS.mjs +623 -0
- package/dist/chunk-PQBALZUH.mjs +1941 -0
- package/dist/chunk-PVW5D5AW.mjs +42 -0
- package/dist/{chunk-LZEG3FEY.mjs → chunk-QQWYQ2HS.mjs} +7 -7
- package/dist/chunk-QVR6V6QP.mjs +46 -0
- package/dist/chunk-SGT5IJVW.mjs +28 -0
- package/dist/chunk-SZXO6YSX.mjs +502 -0
- package/dist/chunk-THATLMEX.mjs +1227 -0
- package/dist/chunk-TYR4Q37L.mjs +391 -0
- package/dist/{chunk-DMLQBAGC.mjs → chunk-V3R2NYQR.mjs} +1 -1
- package/dist/chunk-VHK7WBO3.mjs +189 -0
- package/dist/{chunk-TH4XENKH.mjs → chunk-XASDB7YS.mjs} +1 -1
- package/dist/{chunk-NYXA6X56.mjs → chunk-XBQ4LO2N.mjs} +31 -11
- package/dist/chunk-XF3MGADK.mjs +2946 -0
- package/dist/{chunk-5KYR6WJB.mjs → chunk-XQAHREKO.mjs} +17 -1
- package/dist/chunk-XZG3DTXV.mjs +696 -0
- package/dist/chunk-Y4YGR2NY.mjs +52 -0
- package/dist/{chunk-LJEKKFC7.mjs → chunk-YS2JUNSQ.mjs} +19 -11
- package/dist/chunk-ZG7NJLXN.mjs +104 -0
- package/dist/create-3TGG7B3X.mjs +10 -0
- package/dist/credits-MEG7V5DI.mjs +33 -0
- package/dist/{deploy-STWZ3OSM.mjs → deploy-CNRORHJ7.mjs} +11 -11
- package/dist/dev-RXSBY7U4.mjs +8 -0
- package/dist/dev-YLYC6B7R.mjs +401 -0
- package/dist/dispatcher.mjs +131 -30
- package/dist/{doctor-3RW3OGZJ.mjs → doctor-XRTNG2IT.mjs} +13 -21
- package/dist/{domains-FTNK4IKY.mjs → domains-JIHFKDWR.mjs} +6 -6
- package/dist/engine-runtime-fetch-QVKJVC3W.mjs +10 -0
- package/dist/{env-TNCYUAMT.mjs → env-LJ3DCAA5.mjs} +7 -7
- package/dist/env-R72AHBT2.mjs +19 -0
- package/dist/explain-32RSXEXC.mjs +251 -0
- package/dist/inspect-PYLYN45K.mjs +86 -0
- package/dist/{inspect-X3M7WSTL.mjs → inspect-ZOLZHNSZ.mjs} +28 -13
- package/dist/introspect-27MMTACH.mjs +9 -0
- package/dist/kernel-IC4ZC54Z.mjs +662 -0
- package/dist/kernel-plugin.js +6862 -0
- package/dist/keys-WKOQ4KQE.mjs +71 -0
- package/dist/lines-I6ZB2IOU.mjs +56 -0
- package/dist/{link-76Z6V6CJ.mjs → link-TE2FUX74.mjs} +6 -6
- package/dist/{lint-DDZ7HUQN.mjs → lint-735IPMC6.mjs} +2 -2
- package/dist/{list-2MFOQKKQ.mjs → list-CHYY2P4T.mjs} +7 -11
- package/dist/lock-XVR3RRZ6.mjs +57 -0
- package/dist/{login-RLMDEWAL.mjs → login-LW7CHZDZ.mjs} +10 -8
- package/dist/{logout-Z57X6HPE.mjs → logout-6U6OP5DM.mjs} +2 -2
- package/dist/{logs-TNDM4RFE.mjs → logs-HKH4H6PT.mjs} +5 -5
- package/dist/logs-SCQC5HNO.mjs +44 -0
- package/dist/mcp-VRHQOEGG.mjs +1077 -0
- package/dist/{mock-EVER34IX.mjs → mock-5H3CVPHC.mjs} +2 -2
- package/dist/outdated-T7QW6F3Z.mjs +58 -0
- package/dist/port-24R7ZNP4.mjs +2408 -0
- package/dist/{projects-YL4T24VV.mjs → projects-HYITVEXS.mjs} +6 -6
- package/dist/publish-DZRR5A5X.mjs +20 -0
- package/dist/remix-4HVXRIEB.mjs +125 -0
- package/dist/repo-FG3CMZQS.mjs +9 -0
- package/dist/report-M7F4ZYP7.mjs +2 -0
- package/dist/rollback-H7Q4XYGX.mjs +43 -0
- package/dist/{rollback-47VQ4HJK.mjs → rollback-OM64O67F.mjs} +6 -6
- package/dist/scene-QTHT3ZPL.mjs +324 -0
- package/dist/{status-TWPOMWNA.mjs → status-5D4RR3MM.mjs} +5 -5
- package/dist/status-AKI6E3JK.mjs +18 -0
- package/dist/test-W6MURUGZ.mjs +233 -0
- package/dist/testers-EI32JB5F.mjs +49 -0
- package/dist/{unlink-H3BO3DOQ.mjs → unlink-BND47IWW.mjs} +2 -2
- package/dist/{update-FLY46XI3.mjs → update-XKKFREKH.mjs} +4 -4
- package/dist/{whoami-CQPPARRS.mjs → whoami-WHLGCVRC.mjs} +3 -3
- package/dist/workshop-F2T4IDOU.mjs +1703 -0
- package/package.json +11 -7
- package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/package.json +1 -1
- package/dist/chunk-ARIDYKLO.mjs +0 -6463
- package/dist/dev-7O65HY6C.mjs +0 -8
- package/dist/explain-DLRUUCNS.mjs +0 -251
- package/dist/introspect-RC265ACF.mjs +0 -9
- package/dist/repo-UUT3AZ3L.mjs +0 -9
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { ENGINE_RUNTIME_FILES_ROUTE, ENGINE_RUNTIME_ROUTE, ensureEngineRuntime, refreshEngineRuntime } from './chunk-QVR6V6QP.mjs';
|
|
3
|
+
import './chunk-N2WZJX5P.mjs';
|
|
4
|
+
import './chunk-EYEMPIMP.mjs';
|
|
5
|
+
import './chunk-BGM7CH5G.mjs';
|
|
6
|
+
import './chunk-25ZPFHLF.mjs';
|
|
7
|
+
import './chunk-SGT5IJVW.mjs';
|
|
8
|
+
import './chunk-XBQ4LO2N.mjs';
|
|
9
|
+
import './chunk-GGPN4QYQ.mjs';
|
|
10
|
+
import './chunk-I7GEHLDD.mjs';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { formatEnvFile, parseEnvFile, parseKeyValueArg } from './chunk-
|
|
3
|
-
import { promptYesNo } from './chunk-
|
|
4
|
-
import { ENV_SCOPE, PUBLIC_ENV_PREFIX, SECRET_MASK } from './chunk-
|
|
5
|
-
import { parseArgs, flagString, flagBool } from './chunk-
|
|
6
|
-
import { ApiClient } from './chunk-
|
|
7
|
-
import { readAuth, readProjectLink } from './chunk-
|
|
8
|
-
import { CliError, CLI_ERROR_CODE, info, dim, result, bold, success } from './chunk-
|
|
2
|
+
import { formatEnvFile, parseEnvFile, parseKeyValueArg } from './chunk-ECGDRU56.mjs';
|
|
3
|
+
import { promptYesNo } from './chunk-XASDB7YS.mjs';
|
|
4
|
+
import { ENV_SCOPE, PUBLIC_ENV_PREFIX, SECRET_MASK } from './chunk-LLRUOIMU.mjs';
|
|
5
|
+
import { parseArgs, flagString, flagBool } from './chunk-XQAHREKO.mjs';
|
|
6
|
+
import { ApiClient } from './chunk-XBQ4LO2N.mjs';
|
|
7
|
+
import { readAuth, readProjectLink } from './chunk-GGPN4QYQ.mjs';
|
|
8
|
+
import { CliError, CLI_ERROR_CODE, info, dim, result, bold, success } from './chunk-I7GEHLDD.mjs';
|
|
9
9
|
import { promises } from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { run as default, localEnvironmentName, parseEnvCommand, renderDiff, renderEnvOutcome, runEnvCommand } from './chunk-ITDAICBY.mjs';
|
|
3
|
+
import './chunk-NEB372WD.mjs';
|
|
4
|
+
import './chunk-JNCEKW2Y.mjs';
|
|
5
|
+
import './chunk-BTNVYAC4.mjs';
|
|
6
|
+
import './chunk-VHK7WBO3.mjs';
|
|
7
|
+
import './chunk-454RQ6NR.mjs';
|
|
8
|
+
import './chunk-JKECUAAV.mjs';
|
|
9
|
+
import './chunk-HQPZQIWW.mjs';
|
|
10
|
+
import './chunk-KQNZKV6E.mjs';
|
|
11
|
+
import './chunk-N2WZJX5P.mjs';
|
|
12
|
+
import './chunk-EYEMPIMP.mjs';
|
|
13
|
+
import './chunk-BGM7CH5G.mjs';
|
|
14
|
+
import './chunk-25ZPFHLF.mjs';
|
|
15
|
+
import './chunk-SGT5IJVW.mjs';
|
|
16
|
+
import './chunk-XQAHREKO.mjs';
|
|
17
|
+
import './chunk-XBQ4LO2N.mjs';
|
|
18
|
+
import './chunk-GGPN4QYQ.mjs';
|
|
19
|
+
import './chunk-I7GEHLDD.mjs';
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { package_default } from './chunk-FV273ECI.mjs';
|
|
3
|
+
import { parseArgs } from './chunk-XQAHREKO.mjs';
|
|
4
|
+
import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-I7GEHLDD.mjs';
|
|
5
|
+
|
|
6
|
+
// src/embedded-docs.ts
|
|
7
|
+
var TOPICS = [
|
|
8
|
+
{
|
|
9
|
+
"name": "brand",
|
|
10
|
+
"title": "Brand schema",
|
|
11
|
+
"description": "Single source of truth for visible strings (lib/brand.ts)",
|
|
12
|
+
"source_url": "https://cimplify.dev/docs/templates/brand",
|
|
13
|
+
"body": '## Field reference\n\n \n \n\n| Field | Required | Purpose |\n| --- | --- | --- |\n| `name` | yes | Full brand name; used in metadata, header, schema.org Organization |\n| `shortName` | yes | Compact form for tight spots (header, OG, app icons) |\n| `microTag` | yes | Tiny industry label shown beside the wordmark |\n| `description` | yes | Default OG / SEO description (\u2265 20 chars) |\n| `schemaType` | yes | schema.org Organization subtype (`Store`, `Bakery`, \u2026) |\n| `currency` | yes | ISO 4217 (e.g. `GHS`); used by every `formatPrice` |\n| `locale` | yes | BCP-47-ish, e.g. `en_GH` |\n| `contact` | yes | Address, email, phone, hours, country code |\n| `socials[]` | yes | Footer / contact chips with built-in icon keys |\n| `header.nav` | yes | Top nav links (label + href) |\n| `hero` | yes | Home hero: badge, title, subtitle, CTAs |\n| `trustItems?` | no | Hero trust strip (free shipping, warranty, \u2026) |\n| `brandStrip?` | no | "Authorised dealer for\u2026" strip |\n| `promo?` | no | Time-limited banner |\n| `terms` / `privacy` | yes | Standalone policy pages (eyebrow, sections, last updated) |\n| `shipping` / `returns` / `accessibility` | yes | Same shape as terms/privacy |\n| `newsletter` | yes | Footer signup copy |\n| `about` | yes | Eyebrow, title, paragraphs, sections |\n| `faq` | yes | Sectioned Q/A list |\n| `account` | yes | Login, signup, and account page copy for SDK-rendered account routes |\n| `contactPage` | yes | Eyebrow, title, body for `/contact` |\n| `trackOrder` | yes | Copy for the guest order lookup page |\n| `footer` | yes | Sitemap sections, blurb, optional "Powered by" |\n| `llms.summary` | yes | \u2265 20-char blurb that opens the `/llms.txt` index |\n| `mock.seed` | yes | Mock seed name (must match `SeedNameSchema`) |\n| `mock.businessId` | yes | Must start with `bus_` |\n\n \n\n \n\n## A complete example\n\n \n \n\n```ts title="lib/brand.ts (excerpt: the retail template)"\n\nexport const brand: Brand = {\n name: "Currents Electronics",\n shortName: "Currents",\n microTag: "ELECTRONICS",\n description:\n "Authorised dealer for Apple, Samsung, Sony, Bose. Same-day Accra delivery, two-year warranty.",\n schemaType: "Store",\n currency: "GHS",\n locale: "en_GH",\n\n contact: {\n email: "hello@currentselectronics.test",\n phone: "+233 244 000 000",\n phoneTel: "+233244000000",\n streetAddress: "Atomic Junction, East Legon",\n city: "Accra",\n countryCode: "GH",\n hours: "Mon\u2013Sat \xB7 9am\u20138pm",\n },\n\n socials: [\n { label: "Instagram", href: "https://instagram.com/currentselectronics", icon: "instagram" },\n { label: "WhatsApp", href: "https://wa.me/233244000000", icon: "whatsapp" },\n ],\n\n header: { nav: [\n { label: "Shop", href: "/shop" },\n { label: "Deals", href: "/categories/deals" },\n { label: "Support",href: "/faq" },\n ]},\n\n hero: {\n badge: "LAPTOPS \xB7 PHONES \xB7 AUDIO",\n title: "The tech you want, in stock today.",\n subtitle: "Same-day delivery in Accra. Two-year warranty on every product.",\n primaryCtaLabel: "Shop now",\n secondaryCtaLabel: "See deals",\n secondaryCtaHref: "/categories/deals",\n },\n\n // \u2026terms, privacy, shipping, returns, accessibility, about, faq, \u2026\n\n newsletter: {\n title: "New drops, real prices, in your inbox.",\n body: "One email a week. No newsletter chaff.",\n eyebrow: "The shortlist",\n },\n account: {\n loginEyebrow: "Welcome back", loginTitle: "Sign in to Currents",\n signupEyebrow: "Welcome", signupTitle: "Create your Currents account",\n accountEyebrow: "Your account", accountTitle: "Welcome back",\n },\n contactPage: { title: "Talk to a real human.", body: "We reply within a business day.", eyebrow: "Contact" },\n trackOrder: { title: "Where\'s my order?", body: "Enter your order number and email.", eyebrow: "Track an order" },\n footer: { sitemap: [/* \u2026 */], poweredBy: { label: "Cimplify", href: "https://app.cimplify.io" } },\n llms: { summary: "Authorised dealer for Apple, Samsung, Sony, Bose. Two-year warranty." },\n\n mock: { seed: "retail", businessId: "bus_currents_electronics" },\n};\n```\n\n \n\n \n\n## Validation: `assertBrand`\n\n \n\nTemplates run `assertBrand(brand)` in their `brand.test.ts`. Failures list every offending field with its dot-path; drift between the template and the schema is impossible to ship by accident.\n\n \n \n\n```ts title="__tests__/brand.test.ts"\n\ncreateBrandSuite({ brand });\n```\n\n \n\n \n\n## Industry-specific extensions\n\n \n\nNeed fields the base schema doesn\'t have (fashion\'s `lookbook`, services\' `bookingPolicy`)? Extend, don\'t fork. The base shape stays canonical and your custom fields type-check alongside it.\n\n \n \n\n```ts title="Extending BrandSchema"\n\nexport const FashionBrandSchema = BrandSchema.extend({\n lookbook: z.object({\n eyebrow: z.string(),\n title: z.string().min(1),\n drops: z.array(z.object({\n slug: z.string(),\n title: z.string(),\n heroImage: z.string().url(),\n })),\n }),\n});\n\nexport type FashionBrand = z.infer<typeof FashionBrandSchema>;\n```\n\n \n\n \n\n## Next\n\n \n \n- [**Templates overview**](/docs/templates)\n Six industry templates and what they ship\n\n \n- [**Customizing**](/docs/templates/customizing)\n Beyond brand: ejection and schema extensions\n'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "eject",
|
|
17
|
+
"title": "Customizing a template",
|
|
18
|
+
"description": "When and how to eject SDK components into your project",
|
|
19
|
+
"source_url": "https://cimplify.dev/docs/templates/customizing",
|
|
20
|
+
"body": '## The customisation tiers\n\n \n \n\n| Tier | When | Cost |\n| --- | --- | --- |\n| Edit `brand.ts` | Copy, links, contact, hero, FAQ, policies | Free updates from the SDK forever |\n| Edit `globals.css @theme` | Palette, radius, fonts | Free updates from the SDK forever |\n| `classNames` overrides | Restyle a slot inside an SDK component | Free updates; no fork |\n| Eject a component | Restructure JSX, change behaviour, add merchant logic | You own the file; no auto-updates |\n| Extend `BrandSchema` | Industry-specific fields (lookbook, booking policy) | Type-checked, validated, schema co-evolves |\n\n \n\n \n\n## Edit content via `brand.ts`\n\n \n\nHardcoding a string in a page or component is the wrong move. Hoist it into the brand object first; everything that reads from `brand` stays consistent across pages, sitemap, and metadata.\n\n \n \n\n```tsx\n// \u274C wrong: hardcoded in a component\n<h1>Akua\'s Bakery</h1>\n\n// \u2705 right: sourced from the brand contract\n<h1>{brand.hero.title}</h1>\n```\n\n \n\n \n\n## Eject a component\n\n \n\nThe SDK ships ~67 ejectable components in its registry (cards, selectors, full pages, primitives). Eject when a `classNames` override can\'t reach the part you need to change, or when you\'re restructuring beyond what the component\'s props support.\n\n \n \n\n```bash\n# Browse the registry\ncimplify list\n\n# Eject one\ncimplify add cart-summary\n# \u2192 writes ./components/cart-summary.tsx, owned by you\n\n# Replace the SDK import in your page\n# import { CartSummary } from "@cimplify/sdk/react"\n# import { CartSummary } from "@/components/cart-summary"\n```\n\n \n \n\nOnce ejected, the file is yours. SDK upgrades won\'t touch it; you trade off auto-bugfixes for full control.\n\n \n \n\n**Don\'t reinvent the customizer.** Variant selection, add-on math, bundle pricing, and composite mode-switching took many iterations to get right. Eject `variant-selector`, `composite-selector`, `bundle-selector`, or `add-on-selector` and re-style; don\'t rewrite the cart payload contract. See `AGENTS.md` for the full doctrine.\n\n \n\n \n\n## Extend the brand schema\n\n \n\nIndustry-specific fields belong on the schema, not in scattered component props. Use `BrandSchema.extend` so your additions get the same boot-time validation as the canonical fields.\n\n \n \n\n```ts title="lib/schema.ts (services template)"\n\nexport const ServicesBrandSchema = BrandSchema.extend({\n bookingPolicy: z.object({\n cancellationWindowHours: z.number().int().positive(),\n depositPercent: z.number().int().min(0).max(100),\n rescheduleNoticeHours: z.number().int().positive(),\n }),\n});\n\nexport type ServicesBrand = z.infer<typeof ServicesBrandSchema>;\n```\n\n \n \n \n\n```ts title="lib/brand.ts"\n\nexport const brand: ServicesBrand = {\n // \u2026all the standard Brand fields\u2026\n bookingPolicy: {\n cancellationWindowHours: 24,\n depositPercent: 25,\n rescheduleNoticeHours: 12,\n },\n};\n```\n\n \n \n \n\n```ts title="__tests__/brand.test.ts: assert against the extended schema"\n\ndescribe("brand", () => {\n it("conforms to the services brand contract", () => {\n expect(ServicesBrandSchema.safeParse(brand).success).toBe(true);\n });\n});\n```\n\n \n\n \n\n## Add a new section\n\n \n\n1. Build the section as a Server Component in `components/`.\n2. Read merchant-specific copy from `brand`; extend the schema if the field isn\'t there.\n3. Wrap any client interactivity in a `*-client.tsx` island behind `<Suspense>` to keep the chrome cached.\n4. Compose into the page (`app/page.tsx`, `app/products/[slug]/page.tsx`, \u2026).\n5. Run `bun run check`.\n\n \n\n## Wire a Server Action\n\n \n \n\n```ts\n"use server";\n\nexport async function cancelMyOrder(orderId: string) {\n const r = await getServerClient().orders.cancel(orderId, "customer requested");\n if (!r.ok) return { ok: false as const, message: r.error.message };\n\n revalidatePath("/account/orders");\n return { ok: true as const };\n}\n```\n\nOrders are per-customer and never carry a `cimplify:*` cache tag, so there is no `revalidate*` helper for them; refresh the page that shows them. Catalogue writes use the tag helpers (`revalidateProducts`, `revalidateCollection`, and the rest) from `@cimplify/sdk/server`.\n\n \n\n \n\n## Don\'ts\n\n \n\n- Hardcode strings in pages or components.\n- Enable `cacheComponents: true` in `next.config.ts`. Cache Components (the `\'use cache\'` directive) is still experimental and its runtime constraints don\'t suit hosted storefronts. Stay on the ISR Previous Model the templates ship with.\n- Use `\'use cache\'` / `cacheTag` / `cacheLife` directives. Same reason: they require Cache Components, which we don\'t enable. Use `export const revalidate` per page + `cacheOptions: { revalidate, tags }` on SDK reads.\n- Bypass `getServerClient()` by instantiating `createCimplifyClient` directly in a Server Component; you\'ll lose per-request memoisation.\n\n \n\n## Next\n\n \n \n- [**Brand schema**](/docs/templates/brand)\n Field reference and a full example\n\n \n- [**Testing**](/docs/testing)\n Catch ejections that broke the contract\n'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "products",
|
|
24
|
+
"title": "Catalogue \u2014 Single product",
|
|
25
|
+
"description": "Product shape, single-product fetch, by id or slug",
|
|
26
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#single-product",
|
|
27
|
+
"body": "## Single product\n`getProduct` and `getProductBySlug` resolve to the same endpoint; the server accepts either an ID or a slug.\n\n \n \n\n```ts\nconst byId = await client.catalogue.getProduct('prod_xxx')\nconst bySlug = await client.catalogue.getProductBySlug('studio-tee-natural')\n\nif (bySlug.ok) {\n const product = bySlug.value // ProductWithDetails\n console.log(product.id, product.name)\n console.log(product.variants)\n console.log(product.add_ons)\n console.log(product.images)\n}\n```\n"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "variants",
|
|
31
|
+
"title": "Catalogue \u2014 Variants",
|
|
32
|
+
"description": "Variant axes, picking by axis selections",
|
|
33
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#variants",
|
|
34
|
+
"body": "## Variants\n```ts\nconst variants = await client.catalogue.getVariants('prod_xxx')\nconst single = await client.catalogue.getVariantById('prod_xxx', 'var_yyy')\nconst axes = await client.catalogue.getVariantAxes('prod_xxx')\n\n// Find a variant by axis selections (size: M, color: blue)\nconst found = await client.catalogue.getVariantByAxisSelections('prod_xxx', {\n size: 'M',\n color: 'blue',\n})\n\nif (found.ok && found.value) {\n console.log(found.value.id, found.value.price_adjustment)\n}\n```\n"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "bundles",
|
|
38
|
+
"title": "Catalogue \u2014 Bundles and composites",
|
|
39
|
+
"description": "Fixed bundles AND configurable composites (same catalogue section)",
|
|
40
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#bundles-and-composites",
|
|
41
|
+
"body": "## Bundles and composites\n**Bundles** are fixed product groupings with a single combined price. **Composites** are build-your-own: the customer picks components (with a quantity, and optionally a variant or add-on option) and the price is computed from those selections.\n\n \n \n\n```ts\n// Bundles\nconst bundles = await client.catalogue.getBundles()\nconst bundle = await client.catalogue.getBundle('bun_xxx')\nconst bySlug = await client.catalogue.getBundleBySlug('breakfast-box')\n\n// Composites\nconst composites = await client.catalogue.getComposites()\nconst composite = await client.catalogue.getComposite('comp_xxx')\nconst forProduct = await client.catalogue.getCompositeByProduct('prod_xxx')\n\n// Price a composite from a set of selections\nconst price = await client.catalogue.calculateCompositePrice(\n 'comp_xxx',\n [\n { component_id: 'comp_base', quantity: 1, variant_id: 'var_classic' },\n { component_id: 'comp_milk', quantity: 1, add_on_option_id: 'addopt_oat' },\n ],\n 'loc_main',\n)\nif (price.ok) {\n console.log(price.value.final_price, price.value.breakdown)\n}\n```\n"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "composites",
|
|
45
|
+
"title": "Catalogue \u2014 Bundles and composites",
|
|
46
|
+
"description": "Alias of `bundles` \u2014 customer-configurable bundles",
|
|
47
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#bundles-and-composites",
|
|
48
|
+
"body": "## Bundles and composites\n**Bundles** are fixed product groupings with a single combined price. **Composites** are build-your-own: the customer picks components (with a quantity, and optionally a variant or add-on option) and the price is computed from those selections.\n\n \n \n\n```ts\n// Bundles\nconst bundles = await client.catalogue.getBundles()\nconst bundle = await client.catalogue.getBundle('bun_xxx')\nconst bySlug = await client.catalogue.getBundleBySlug('breakfast-box')\n\n// Composites\nconst composites = await client.catalogue.getComposites()\nconst composite = await client.catalogue.getComposite('comp_xxx')\nconst forProduct = await client.catalogue.getCompositeByProduct('prod_xxx')\n\n// Price a composite from a set of selections\nconst price = await client.catalogue.calculateCompositePrice(\n 'comp_xxx',\n [\n { component_id: 'comp_base', quantity: 1, variant_id: 'var_classic' },\n { component_id: 'comp_milk', quantity: 1, add_on_option_id: 'addopt_oat' },\n ],\n 'loc_main',\n)\nif (price.ok) {\n console.log(price.value.final_price, price.value.breakdown)\n}\n```\n"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "add-ons",
|
|
52
|
+
"title": "Catalogue \u2014 Add-ons",
|
|
53
|
+
"description": "Product add-ons / customizations (gift wrap, engraving, \u2026)",
|
|
54
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#add-ons",
|
|
55
|
+
"body": "## Add-ons\n```ts\nconst addOns = await client.catalogue.getAddOns('prod_xxx')\nif (addOns.ok) {\n for (const group of addOns.value) {\n console.log(group.name, group.is_required, group.max_selections)\n }\n}\n\n// getProduct attaches the groups with their options\nconst product = await client.catalogue.getProduct('prod_xxx')\nif (product.ok) {\n for (const group of product.value.add_ons ?? []) {\n console.log(group.name, group.options.map((o) => o.name))\n }\n}\n```\n"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "quotes",
|
|
59
|
+
"title": "Catalogue \u2014 Price quotes",
|
|
60
|
+
"description": "Price quotes \u2014 quote-first pricing flow before add-to-cart",
|
|
61
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#price-quotes",
|
|
62
|
+
"body": "## Price quotes\nWhenever variants, add-ons, bundle picks, or composite picks affect price, fetch a quote first and pass `quote_id` to `cart.addItem`. Quotes have an `expires_at`; refresh if the user dwells.\n\n \n \n\n```ts\nconst quote = await client.catalogue.fetchQuote({\n product_id: 'prod_xxx',\n variant_id: 'var_yyy',\n quantity: 2,\n add_on_option_ids: ['addopt_oat_milk'],\n location_id: 'loc_main',\n})\n\nif (!quote.ok) {\n console.error(quote.error.code, quote.error.message)\n return\n}\n\nawait client.cart.addItem({\n item_id: 'prod_xxx',\n quantity: 2,\n variant_id: 'var_yyy',\n add_on_options: ['addopt_oat_milk'],\n quote_id: quote.value.quote_id,\n})\n\n// Refresh an aging quote\nconst refreshed = await client.catalogue.refreshQuote({\n quote_id: quote.value.quote_id,\n quantity: 3,\n})\nif (refreshed.ok) {\n console.log(refreshed.value.quote.quote_id)\n}\n```\n"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "discounts",
|
|
66
|
+
"title": "Catalogue \u2014 Discounts",
|
|
67
|
+
"description": "Deals and promotional pricing",
|
|
68
|
+
"source_url": "https://cimplify.dev/docs/sdk/catalogue#discounts",
|
|
69
|
+
"body": "## Discounts\nValidate a code at the line / subtotal level before applying it to the cart. Order subtotal is passed as a `Money` string.\n\n \n \n\n```ts\nconst r = await client.catalogue.validateDiscountCode('WELCOME10', '49.99', 'loc_main')\nif (r.ok) {\n console.log(r.value.is_eligible, r.value.discount_amount, r.value.ineligibility_reason)\n}\n```\n"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "cart",
|
|
73
|
+
"title": "Cart",
|
|
74
|
+
"description": "Flat add-to-cart payload + quote-then-add flow",
|
|
75
|
+
"source_url": "https://cimplify.dev/docs/sdk/cart",
|
|
76
|
+
"body": "## Add an item\n\n \n\nThe add-to-cart payload is **flat**. There is no nested `configuration` wrapper and no `productId` field; everything sits at the top of the body.\n\n \n \n\n```ts\nconst added = await client.cart.addItem({\n item_id: 'prod_studio-tee-natural',\n quantity: 1,\n variant_id: 'var_studio-tee-natural_size_s',\n add_on_options: ['addopt_gift_wrap'],\n special_instructions: 'Please ship without invoice.',\n})\n\nif (!added.ok) {\n console.error(added.error.code, added.error.message)\n return\n}\n\nconsole.log(added.value.cart.id)\n```\n\n \n\n \n\n## Read the cart\n\n \n \n\n```ts\n\nconst r = await client.cart.get()\nif (!r.ok) return\n\nconst cart = r.value\nconsole.log(cart.items.length)\nconsole.log(cart.pricing.currency) // \"GHS\"\nconsole.log(cart.pricing.subtotal) // Money (branded string)\nconsole.log(cart.pricing.total_price) // Money\n\n// Money is a string. Always parse before math, format before display.\nconst subtotal = parsePrice(cart.pricing.subtotal)\nif (subtotal !== 0) {\n console.log(formatPrice(subtotal, cart.pricing.currency)) // \"GH\u20B529.99\"\n}\n```\n\n \n\n \n\n## Update quantity / remove / clear\n\n \n \n\n```ts\n// You need the cart-item id from cart.items[i].id (NOT the product id)\nawait client.cart.updateQuantity('ci_xxx', 3)\nawait client.cart.removeItem('ci_xxx')\nawait client.cart.clear()\n```\n\n`updateItem(cartItemId, { quantity })` is the same as `updateQuantity`; it returns a `VALIDATION_ERROR` when `quantity` is missing, since other mid-cart edits are not supported. Mutations return `{ cart, notice? }`; show `notice.text` (`kind` is `info` or `warning`) when the server attaches one.\n\n \n\n \n\n## Coupons\n\n \n \n\n```ts\nconst apply = await client.cart.applyCoupon('WELCOME10')\nif (!apply.ok) {\n console.error(apply.error.code, apply.error.message)\n}\n\nawait client.cart.removeCoupon()\n\n// Custom idempotency key (otherwise the SDK auto-generates one)\nawait client.cart.applyCoupon('WELCOME10', { idempotencyKey: 'apply-welcome10-once' })\n```\n\n \n\n \n\n## Convenience reads\n\n \n \n\n```ts\nconst count = await client.cart.getCount() // Result<number>\nconst total = await client.cart.getTotal() // Result<string>\nconst items = await client.cart.getItems() // Result<CartItem[]>\nconst summary = await client.cart.getSummary() // Result<CartSummary>\nconst empty = await client.cart.isEmpty() // Result<boolean>\nconst has = await client.cart.hasItem('prod_xxx', 'var_yyy') // Result<boolean>\nconst found = await client.cart.findItem('prod_xxx', 'var_yyy') // Result<CartItem | undefined>\n```\n\n## Quote a destination\n\nPrice the cart for an order type and address before checkout: delivery fee, tax, statutory charges, and the total, without minting checkout terms.\n\n```ts\nconst q = await client.cart.quoteDestination({\n order_type: 'delivery',\n address_info: { street_address: '12 Independence Ave', city: 'Accra', country: 'GH', latitude: 5.55, longitude: -0.196 },\n delivery_rate_id: null,\n})\nif (q.ok) {\n console.log(q.value.delivery_fee, q.value.tax_amount, q.value.total, q.value.currency)\n}\n```\n\nPass the `rate_id` of an option from `client.delivery.getOptions()` as `delivery_rate_id` to price a named delivery rate.\n\n \n\n \n\n## Reorder from a past order\n\n \n\nWalks every line item on the order and adds it to the current cart. Returns added/failed lists; partial success is normal (e.g. a discontinued variant).\n\n \n \n\n```ts\nconst r = await client.cart.reorderFromOrder('ord_xxx')\nif (!r.ok) return\n\nconsole.log(r.value.added) // [{ item_id }, ...]\nconsole.log(r.value.failed) // [{ item_id, error: CimplifyError }, ...]\n```\n\n \n\n \n\n## addItem payload\n\n \n \n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `item_id` | `string` | Required. Product ID. |\n| `quantity` | `number` | Defaults to 1. |\n| `variant_id` | `string` | Selected variant. |\n| `add_on_options` | `string[]` | Selected add-on option IDs. |\n| `special_instructions` | `string` | Free-form note. |\n| `bundle_selections` | `BundleSelectionInput[]` | `{ component_id, quantity, variant_id? }`. Required when item is a bundle. |\n| `composite_selections` | `ComponentSelectionInput[]` | `{ component_id, quantity, variant_id?, add_on_option_id? }`. Required when item is a composite. |\n| `quote_id` | `string` | Pre-computed quote from `catalogue.fetchQuote`. Locks the price. |\n| `scheduled_start` / `scheduled_end` | `string` | ISO times of the booked slot for a service. |\n| `staff_id` / `resource_id` | `string` | Booked staff member or resource. |\n| `units` | `number` | Whole units booked for a service. |\n| `billing_plan_id` | `string` | Subscription or installment plan for the line. |\n| `customer_inputs` | `{ field_id, value }[]` | Answers to the product's customer input fields. |\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `get()` | `Result<UICart>` |\n| `getItems()` | `Result<CartItem[]>` |\n| `getCount()` | `Result<number>` |\n| `getTotal()` | `Result<string>` |\n| `getSummary()` | `Result<CartSummary>` |\n| `addItem(input, opts?)` | `Result<CartMutationResult>` |\n| `updateQuantity(cartItemId, qty)` | `Result<CartMutationResult>` |\n| `updateItem(cartItemId, { quantity })` | `Result<CartMutationResult>` |\n| `quoteDestination(input)` | `Result<DestinationQuote>` |\n| `removeItem(cartItemId)` | `Result<CartMutationResult>` |\n| `clear()` | `Result<CartMutationResult>` |\n| `applyCoupon(code, opts?)` | `Result<CartMutationResult>` |\n| `removeCoupon()` | `Result<CartMutationResult>` |\n| `reorderFromOrder(orderId)` | `Result<ReorderResult>` |\n| `isEmpty()` | `Result<boolean>` |\n| `hasItem(productId, variantId?)` | `Result<boolean>` |\n| `findItem(productId, variantId?)` | `Result<CartItem \\| undefined>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Catalogue**](/docs/sdk/catalogue)\n Fetch products and price quotes before adding\n\n \n- [**Checkout**](/docs/sdk/checkout)\n Convert the cart into a paid order\n\n \n- [**Scheduling**](/docs/sdk/scheduling)\n Pick a slot for service bookings before checkout\n\n \n- [**Money type**](/docs/concepts/money)\n parsePrice / formatPrice for cart totals\n"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "checkout",
|
|
80
|
+
"title": "Checkout",
|
|
81
|
+
"description": "PurchaseIntent mint, terms review and confirmation",
|
|
82
|
+
"source_url": "https://cimplify.dev/docs/sdk/checkout",
|
|
83
|
+
"body": "`client.purchaseIntents` is the checkout authority. Minting creates a durable\nPurchaseIntent and immutable terms; it does **not** create an Order. Confirmation\ncreates the Order only after the server revalidates the accepted terms, source,\ninventory, funding allocation, and opaque payment option.\n\n## Mint Cart terms\n\n```ts\n MOBILE_MONEY_TENDER_AUTHORITY_KIND,\n ORDER_TYPE,\n PURCHASE_INTENT_PAYMENT_METHOD,\n PURCHASE_INTENT_MOBILE_MONEY_NETWORK,\n PURCHASE_INTENT_SOURCE_KIND,\n} from '@cimplify/sdk'\n\nconst minted = await client.purchaseIntents.mint({\n client_intent_key: crypto.randomUUID(),\n source: {\n kind: PURCHASE_INTENT_SOURCE_KIND.CART,\n cart_id: cart.id,\n },\n checkout: {\n customer: {\n name: 'Jane Doe',\n email: 'jane@example.com',\n phone: '+233244000000',\n save_details: false,\n },\n order_type: ORDER_TYPE.PICKUP,\n address_info: { pickup_time: '2026-08-23T15:00:00Z' },\n tender_preference: {\n method_kind: PURCHASE_INTENT_PAYMENT_METHOD.MOBILE_MONEY,\n authority: {\n authority_kind: MOBILE_MONEY_TENDER_AUTHORITY_KIND.TRANSIENT,\n phone_number: '+233244000000',\n network: PURCHASE_INTENT_MOBILE_MONEY_NETWORK.MTN,\n },\n },\n },\n})\n\nif (!minted.ok) throw minted.error\nif (!minted.value.current_terms) throw new Error('Checkout terms are not prepared')\n```\n\nThe server derives seller and owner authority from the authenticated client. Do not\nsend a business, account, staff-session, or hosted-session owner in the body.\n\nFor a saved Mobile Money authority, send only the opaque ID:\n\n```ts\nconst tenderPreference = {\n method_kind: PURCHASE_INTENT_PAYMENT_METHOD.MOBILE_MONEY,\n authority: {\n authority_kind: MOBILE_MONEY_TENDER_AUTHORITY_KIND.SAVED_REFERENCE,\n stored_instrument_id: savedMobileMoney.id,\n },\n} as const\n```\n\nThe server independently verifies ownership and derives its network. Never pair a\nsaved reference with a phone number.\n\n## Review one exact option\n\n```ts\n\nconst terms = minted.value.current_terms\nconst option = selectPurchaseIntentPaymentOption(terms, {\n method_kind: PURCHASE_INTENT_PAYMENT_METHOD.MOBILE_MONEY,\n})\n\nif (!option) throw new Error('The selected payment method is unavailable')\n\nrenderPaymentReview({\n commercePrincipal: option.principal_amount,\n collectionFee: option.fee_amount,\n customerDebit: option.customer_debit_amount,\n currency: option.currency,\n})\n```\n\nThe exact equations are:\n\n```text\nstore_credit_consumed + voucher_credit_consumed + rail_amount = due_now\nprincipal_amount + fee_amount = customer_debit_amount\n```\n\nFunding allocations belong to the accepted terms. Payment options collect only the\nresidual principal and their own provider/offline fee. Do not display one top-level\npayment fee: different options may have different fees.\n\n## Confirm reviewed terms\n\n```ts\n PURCHASE_INTENT_CONFIRM_STATUS,\n createPurchaseIntentConfirmation,\n} from '@cimplify/sdk'\n\nconst confirmation = createPurchaseIntentConfirmation(\n terms,\n option,\n crypto.randomUUID(),\n)\nconst confirmed = await client.purchaseIntents.confirm(\n minted.value.id,\n confirmation,\n)\nif (!confirmed.ok) throw confirmed.error\n\nswitch (confirmed.value.status) {\n case PURCHASE_INTENT_CONFIRM_STATUS.ORDER_CREATED:\n showOrder(confirmed.value.order_id, confirmed.value.order_number)\n break\n case PURCHASE_INTENT_CONFIRM_STATUS.TERMS_CHANGED:\n showTermsReview(confirmed.value.replacement, confirmed.value.deltas)\n break\n case PURCHASE_INTENT_CONFIRM_STATUS.SELECTION_UNAVAILABLE:\n showUnavailableLines(confirmed.value.lines)\n break\n case PURCHASE_INTENT_CONFIRM_STATUS.PAYMENT_FAILED:\n showDeclinedPayment()\n break\n default:\n handleProviderOrRecoveryState(confirmed.value)\n}\n```\n\n`createPurchaseIntentConfirmation` binds the current intent revision, terms version,\ndue-now value, currency, selected option ID, and its exact\n`customer_debit_amount`. A true zero-rail purchase is the only case that can confirm\nwithout an option.\n\n## Resolve provider actions and replay\n\n`resolve` drives the canonical confirm/action/poll loop while keeping one durable\nattempt authority:\n\n```ts\n CHECKOUT_NEXT_ACTION,\n PAYMENT_AUTHORIZATION_OUTCOME_STATUS,\n PURCHASE_INTENT_ACTION_DECISION,\n PURCHASE_INTENT_CONFIRM_STATUS,\n} from '@cimplify/sdk'\n\nconst resolution = await client.purchaseIntents.resolve(minted.value.id, {\n confirmation,\n onActionRequired: async (action) => {\n if (action.status === PURCHASE_INTENT_CONFIRM_STATUS.REDIRECT_REQUIRED) {\n window.location.assign(action.authorization_url)\n return PURCHASE_INTENT_ACTION_DECISION.STOP\n }\n\n if (action.status === PURCHASE_INTENT_CONFIRM_STATUS.CARD_POPUP_REQUIRED) {\n const outcome = await client.checkout.authorizeCardPopup(\n {\n type: CHECKOUT_NEXT_ACTION.CARD_POPUP,\n provider: action.provider,\n client_secret: action.client_secret,\n public_key: action.public_key,\n provider_account_id: action.provider_account_id ?? undefined,\n },\n { currency: option.currency, email: 'jane@example.com' },\n )\n return outcome.status === PAYMENT_AUTHORIZATION_OUTCOME_STATUS.SUBMITTED\n ? PURCHASE_INTENT_ACTION_DECISION.CONTINUE\n : PURCHASE_INTENT_ACTION_DECISION.STOP\n }\n\n const authorization = await collectAuthorization(action.authorization_type)\n return {\n kind: PURCHASE_INTENT_ACTION_DECISION.SUBMIT,\n request: { authorization },\n }\n },\n})\n```\n\n`authorizeCardPopup(action, { currency, email?, reference?, returnUrl?, signal? })` runs the\nprovider's card UI. Pass `email` only when the shopper gave one. A `submitted` outcome is not\nproof of payment; the resolver keeps polling Cimplify for the result.\n\n`resolve` rides out `NETWORK_ERROR`, `TIMEOUT`, `SERVER_ERROR` and other `5xx` failures with\nbounded backoff until `deadlineMs` passes, reporting `reconnecting` through `onStatusChange`\nmeanwhile. A payment still processing at the deadline resolves as `timed_out`, never as a\nfailure. A redirect or card popup is handed over once per attempt: after you return\n`continue`, later replays that still show the same attempt waiting on the shopper are polled\nwithout calling `onActionRequired` again. Other options: `pollIntervalMs`, `maxPollAttempts`,\n`requestTimeoutMs`, `signal`, and `onTermsChanged` (return a new confirmation to accept\nreplacement terms, or `null` to stop).\n\nKeep the same confirmation idempotency key after an ambiguous response. A definitive\ndecline or an explicitly accepted replacement terms version starts a new attempt key.\nPersist only opaque intent, attempt, terms, option, and idempotency authorities for\nbrowser recovery, never customer contact, address, voucher, note, or raw tender data,\nand never a hash of low-entropy PII.\n\n## Show outcomes in a custom checkout\n\nThe same helpers the hosted checkout uses are exported for your own UI:\n\n```ts\n CHECKOUT_OUTCOME,\n checkoutErrorIsTransient,\n checkoutErrorMessage,\n checkoutOutcome,\n paymentOptionNeedsReview,\n} from '@cimplify/sdk'\n\nif (paymentOptionNeedsReview({ terms, option, displayedTotal: cart.pricing.total_price })) {\n renderPaymentReview(option)\n}\n\nif (!resolution.ok) {\n showToast(checkoutErrorMessage(resolution.error))\n if (checkoutErrorIsTransient(resolution.error)) showCheckAgain()\n} else {\n const outcome = checkoutOutcome(resolution.value)\n if (outcome.kind === CHECKOUT_OUTCOME.COMPLETED) {\n showOrder(outcome.resolution.response.order_id)\n } else if (outcome.kind === CHECKOUT_OUTCOME.REVIEW) {\n showTermsReview(outcome.resolution)\n } else {\n showToast(outcome.message)\n }\n}\n```\n\n- `checkoutOutcome(resolution)` maps a resolver result to `completed`, `failed`, `pending`\n (outcome unknown, timed out, or still waiting on the shopper), or `review`, with plain\n shopper copy for the failed and pending cases.\n- `checkoutErrorMessage(error)` returns plain words for a checkout failure. Server messages\n never reach the shopper; unknown codes get a generic line. `CHECKOUT_COPY` holds the strings.\n- `checkoutErrorIsTransient(error)` is true when the payment is unknown rather than declined:\n offer \"check again\", never \"pay again\".\n- `paymentOptionNeedsReview({ terms, option, displayedTotal? })` is true only when the option\n charges more than the shopper already agreed to: a fee, or a debit that differs from the\n total shown. `paymentOptionFee(option)` returns that fee as a number.\n- `createCheckoutRecoveryStore({ storage, namespace, ttlMs })` keeps one in-flight purchase per\n business so a reload can resume it. `find(businessId, { cartId, intentId })` answers `current`\n for the same cart or intent, `other` for a different pending payment, or `none`. Store only\n opaque IDs in the record. `purchaseConfirmationFingerprint(intentId, terms, option)` identifies\n one confirmation so a resume reuses its idempotency key.\n\n## Hosted checkout\n\nHosted PurchaseIntents belong to the CheckoutSession, not the storefront API session:\n\n```ts\nconst hosted = client.purchaseIntents.forHostedSession(sessionId)\nconst prepared = await hosted.prepare({\n expected_intent_revision: 0,\n idempotency_key: crypto.randomUUID(),\n checkout,\n})\n\nif (!prepared.ok || !prepared.value.current_terms) {\n throw prepared.ok ? new Error('Terms unavailable') : prepared.error\n}\n\nconst result = await hosted.resolve(prepared.value.id, {\n confirmation: createPurchaseIntentConfirmation(\n prepared.value.current_terms,\n selectPurchaseIntentPaymentOption(prepared.value.current_terms, tender) ?? null,\n crypto.randomUUID(),\n ),\n})\n```\n\nUse the hosted service for prepare, confirm, replay, and action submission. An intent\nID is the durable resource ID, but it does not replace the hosted session's owner\nauthority.\n\n## Collection options\n\n`client.checkout.collectionOptions(currency)` lists the payment methods the business can\ncollect online in one presentment currency. An unsupported currency returns\n`UNSUPPORTED_CURRENCY`; a response that cannot be verified returns\n`PAYMENT_CAPABILITY_MISMATCH`.\n\n## Compatibility adapter\n\n`client.checkout.process()` remains a deprecated SDK adapter over purchase-intent\nroutes for one migration window. New integrations should use\n`client.purchaseIntents` so terms review, per-option fees, attempt replay, and hosted\nownership remain explicit. The backend `/api/v1/checkout` endpoint no longer exists,\nand new checkout responses do not issue legacy Order bill tokens. A positive\n`tip_amount` is refused with `TIP_NOT_SUPPORTED`, and the adapter saves details to Link\nonly when you pass `enroll_in_link: true`.\n\nThe deprecated `client.checkout.submitAuthorization()` method is fail-closed. It\ncan submit an action only when the same in-memory `CheckoutService` instance has\njust received the matching `payment_id`, `intent_id`, and `attempt_id` from\n`process()`. The SDK sends the authorization to the owner-fenced purchase-intent\nattempt action route; it never falls back to the legacy payment-id authorization\nmutation. After a reload, use the durable purchase-intent replay/action API instead.\n\n- [**Checkout Elements**](/docs/checkout/elements)\n Use the hosted UI and its `reviewing_terms` state.\n\n- [**Orders**](/docs/sdk/orders)\n Read the committed Order after `order_created`.\n"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "orders",
|
|
87
|
+
"title": "Orders",
|
|
88
|
+
"description": "Order shape and status flow",
|
|
89
|
+
"source_url": "https://cimplify.dev/docs/sdk/orders",
|
|
90
|
+
"body": "`client.orders.list()` returns different results depending on how the\nclient was constructed:\n\n- **Guest client** (`getServerClient()` with no `accessToken`, or the\n browser `cimplify` client with no signed-in shopper): returns nothing\n by default; the API has no customer to scope to. `client.orders.get()`\n may still use a previously cached historical Order token for its original\n pre-convergence order. New purchase-intent checkout does not issue one.\n- **Authenticated client** (`getAuthenticatedServerClient()` from the\n templates, or `getServerClient({ accessToken })` from a custom integration):\n returns *that customer's* orders at *this merchant*.\n\nSee [Sign in with Cimplify \u2192 Personalized server-side reads](/docs/sdk/auth#personalized-server-side-reads)\nfor the full setup.\n\n## Render orders for the signed-in shopper\n\n```tsx title=\"app/account/orders/page.tsx\"\n\nexport const revalidate = 0; // personalized; do not ISR-cache\n\nexport default async function OrdersPage() {\n const client = await getAuthenticatedServerClient();\n const r = await client.orders.list({ limit: 20 });\n if (!r.ok) throw new Error(r.error.message);\n\n if (r.value.length === 0) {\n return <p>No orders yet.</p>;\n }\n return (\n <ul>\n {r.value.map((o) => (\n <li key={o.id}>#{o.user_friendly_id} \xB7 {o.status} \xB7 {o.total_price}</li>\n ))}\n </ul>\n );\n}\n```\n\nIf `cimplify_access` is missing (signed-out shopper), `client.orders.list()`\nreturns the empty guest view. Branch on `getSession()` from `lib/auth.ts`\nif you want to redirect to sign-in instead.\n\n## List orders\n\n \n \n\n```ts\nconst r = await client.orders.list({ limit: 20 })\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nconsole.log(r.value.length)\nfor (const order of r.value) {\n console.log(order.id, order.user_friendly_id, order.status, order.payment_state)\n}\n```\n\n \n\n \n\n## Filter by status\n\n \n \n\n```ts\nconst pending = await client.orders.list({ status: 'pending' })\nconst completed = await client.orders.list({ status: 'completed', limit: 50 })\n\n// Convenience wrappers\nconst recent = await client.orders.getRecent(5) // last 5\nconst cancelled = await client.orders.getByStatus('cancelled')\n```\n\n \n\n \n\n## Get a single order\n\n \n\nAuthenticated and recognised shoppers read the Order created by a confirmed purchase intent through their existing owner/session authority. The SDK still appends a cached historical Order token when one already exists, solely so old orders remain readable during the compatibility window.\n\n \n \n\n```ts\n\nconst r = await client.orders.get('ord_xxx')\nif (!r.ok) return\n\nconst order = r.value\nconsole.log(order.status)\nconsole.log(order.items.length)\nconsole.log(order.total_price) // Money string\nconsole.log(formatPrice(parsePrice(order.total_price), order.currency))\n```\n\n`getDetail(orderId)` reads the same endpoint typed as the enriched display contract (`EnrichedOrder`: product names, images, resolved selections, customer inputs, scheduling, fulfillment). Feed it to `toAccountOrderView` from `@cimplify/sdk/react` to render the account order components.\n\n \n\n \n\n## Cancel an order\n\n \n \n\n```ts\nconst cancelled = await client.orders.cancel('ord_xxx', 'changed_mind')\nif (!cancelled.ok) {\n console.error(cancelled.error.code, cancelled.error.message)\n}\n\n// Idempotent retry: same key, same outcome on the server\nawait client.orders.cancel('ord_xxx', 'changed_mind', {\n idempotencyKey: 'cancel-ord_xxx-once',\n})\n```\n\n \n\n \n\n## Order status values\n\n \n \n\n| Status | Meaning |\n| --- | --- |\n| `pending` / `created` | Placed, not yet accepted |\n| `confirmed` | Accepted by the business |\n| `in_preparation` | Being prepared or assembled |\n| `ready_to_serve` | Ready for pickup, serving, or dispatch |\n| `partially_served` / `served` | Some or all lines served |\n| `delivered` / `picked_up` | Handed over |\n| `completed` | Fulfilled; terminal state |\n| `cancelled` | Cancelled; terminal state |\n\nPayment is tracked separately in `payment_state`: `not_paid`, `partially_paid`, `paid`, `partially_refunded`, or `refunded`. `lifecycle_phase` (`draft`, `open`, `completed`, `cancelled`) gives the coarse phase.\n\n \n\n \n\n## Poll until terminal\n\n \n \n\n```ts\nasync function pollUntilTerminal(orderId: string, intervalMs = 5000) {\n for (;;) {\n const r = await client.orders.get(orderId)\n if (!r.ok) return r\n\n const status = r.value.status\n if (status === 'completed' || status === 'cancelled') {\n return r\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n}\n```\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `list({ status?, limit?, offset? })` | `Result<Order[]>` |\n| `get(orderId)` | `Result<Order>` |\n| `getDetail(orderId)` | `Result<EnrichedOrder>` |\n| `cancel(orderId, reason?, opts?)` | `Result<Order>` |\n| `getRecent(limit?)` | `Result<Order[]>` |\n| `getByStatus(status)` | `Result<Order[]>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Checkout**](/docs/sdk/checkout)\n Create the orders that this page lists\n\n \n- [**Subscriptions**](/docs/sdk/subscriptions)\n For recurring orders, see the subscription surface\n\n \n- [**Scheduling**](/docs/sdk/scheduling)\n Reschedule or cancel a booking line item\n\n \n- [**Error handling**](/docs/sdk/errors)\n Cancellation guards and retryable errors\n"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "subscriptions",
|
|
94
|
+
"title": "Subscriptions",
|
|
95
|
+
"description": "Recurring billing, plan changes, cancellation",
|
|
96
|
+
"source_url": "https://cimplify.dev/docs/sdk/subscriptions",
|
|
97
|
+
"body": "## List subscriptions\n\n \n \n\n```ts\nconst r = await client.subscriptions.list()\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nfor (const sub of r.value) {\n console.log(sub.id, sub.status, sub.next_billing_date, sub.current_period_end)\n}\n```\n\n`status` is one of `incomplete`, `incomplete_expired`, `trialing`, `active`, `past_due`, `canceled`, `unpaid`, or `paused`. `skip_next_renewal`, `cancel_at_period_end`, and `cycles_completed` describe the renewal state.\n\n \n\n \n\n## Get a single subscription\n\n \n \n\n```ts\nconst r = await client.subscriptions.get('sub_xxx')\nif (!r.ok) return\n\nconst detail = r.value // SubscriptionWithDetails\nconsole.log(detail.subscription.status)\nconsole.log(detail.billing_plan)\nconsole.log(detail.catalogue_product)\nconsole.log(detail.items)\nconsole.log(detail.latest_invoice)\n```\n\n \n\n \n\n## Pause / resume\n\n \n\nPausing freezes future renewals indefinitely; resuming reactivates without re-running checkout.\n\n \n \n\n```ts\nawait client.subscriptions.pause('sub_xxx')\n// ...later\nawait client.subscriptions.resume('sub_xxx')\n```\n\n \n\n \n\n## Skip the next renewal\n\n \n \n\n```ts\nconst r = await client.subscriptions.skipNextRenewal('sub_xxx', {\n idempotencyKey: 'skip-sub_xxx-2026-05', // optional\n})\n\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nconsole.log(r.value.success)\n```\n\n \n\n \n\n## Cancel\n\n \n\nCancellation is terminal; the subscription cannot be resumed. Pass a reason for analytics.\n\n \n \n\n```ts\nconst r = await client.subscriptions.cancel('sub_xxx', 'too_expensive', {\n idempotencyKey: 'cancel-sub_xxx-once', // optional\n})\n\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n}\n```\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `list()` | `Result<Subscription[]>` |\n| `get(id)` | `Result<SubscriptionWithDetails>` |\n| `pause(id)` | `Result<{ success: boolean }>` |\n| `resume(id)` | `Result<{ success: boolean }>` |\n| `skipNextRenewal(id, opts?)` | `Result<{ success: boolean }>` |\n| `cancel(id, reason?, opts?)` | `Result<{ success: boolean }>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Checkout**](/docs/sdk/checkout)\n Subscriptions are created at checkout time\n\n \n- [**Orders**](/docs/sdk/orders)\n Each renewal becomes a new order\n\n \n- [**Auth**](/docs/sdk/auth)\n Subscriptions require an authenticated customer\n\n \n- [**Error handling**](/docs/sdk/errors)\n Branch on `error.code` and `retryable`\n"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "services",
|
|
101
|
+
"title": "Scheduling \u2014 List services",
|
|
102
|
+
"description": "Bookable services + variant-aware availability",
|
|
103
|
+
"source_url": "https://cimplify.dev/docs/sdk/scheduling#list-services",
|
|
104
|
+
"body": "## List services\n```ts\nconst r = await client.scheduling.getServices()\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nfor (const service of r.value) {\n console.log(service.id, service.name, service.duration_minutes)\n}\n\nconst single = await client.scheduling.getService('svc_xxx')\n```\n"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "bookings",
|
|
108
|
+
"title": "Scheduling \u2014 Get available slots (variant-aware)",
|
|
109
|
+
"description": "Picking slots, creating bookings, reschedule and cancel",
|
|
110
|
+
"source_url": "https://cimplify.dev/docs/sdk/scheduling#get-available-slots-variant-aware",
|
|
111
|
+
"body": "## Get available slots (variant-aware)\nPass `variant_id` when the service has variants; slot times and capacities depend on which variant the customer is booking.\n\n \n \n\n```ts\nconst r = await client.scheduling.getAvailableSlots({\n service_id: 'svc_haircut',\n date: '2026-05-08',\n variant_id: 'var_haircut_premium', // 60-minute variant\n participant_count: 1,\n})\n\nif (!r.ok) return\n\nfor (const slot of r.value) {\n console.log(slot.start_time, slot.end_time, slot.capacity_available)\n console.log(slot.available_staff)\n}\n```\n"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "errors",
|
|
115
|
+
"title": "Errors",
|
|
116
|
+
"description": "CimplifyError codes and remediation",
|
|
117
|
+
"source_url": "https://cimplify.dev/docs/sdk/errors",
|
|
118
|
+
"body": "## Result type\n\n \n \n\n```ts\ntype Result<T, E = Error> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error: E }\n```\n\nEvery SDK method returns `Result<T, CimplifyError>`. The exported type's default error parameter is `Error`, so spell out `CimplifyError` when you write your own helpers.\n\n \n\n \n\n## CimplifyError\n\n \n \n\n```ts\n\nclass CimplifyError extends Error {\n code: string // 'VALIDATION_ERROR', 'NOT_FOUND', 'RATE_LIMITED', ...\n message: string // human-readable\n retryable: boolean // safe to retry? defaults to false\n docs_url?: string // link to this code's entry in the error reference\n suggestion?: string // what to do next, for logs and developer tooling\n}\n```\n\n`ErrorCode` lists the codes the SDK itself produces (`ErrorCode.CART_EXPIRED`, `ErrorCode.PRICE_CHANGED`, ...). Helpers: `isCimplifyError(e)`, `isRetryableError(e)`, `isQuoteError(e)` (any quote expiry, consumption, mismatch, or price change), `isQuoteConflictRequiringConsent(e)` (`PRICE_CHANGED` or `QUOTE_VALIDATION_FAILED`: re-quote and ask the shopper to confirm), and `getErrorHint(code)`. A replayed idempotency key with a different body returns an `IdempotencyMismatchError` (`code: 'IDEMPOTENCY_MISMATCH'`, plus `originalCreatedAt`).\n\nMessages are written for developers. For shopper-facing checkout copy use [`checkoutErrorMessage`](/docs/sdk/checkout#show-outcomes-in-a-custom-checkout).\n\n \n\n \n\n## Read errors\n\n \n \n\n```ts\nconst r = await client.catalogue.getProducts()\n\nif (!r.ok) {\n console.error(r.error.code) // machine-readable\n console.error(r.error.message) // human-readable\n console.error(r.error.retryable) // boolean\n console.error(r.error.docs_url) // reference link for the code\n return\n}\n\nconsole.log(r.value.items)\n```\n\n \n\n \n\n## Branch on code\n\n \n \n\n```ts\nconst r = await client.cart.addItem({ item_id: 'prod_xxx', quantity: 1 })\n\nif (!r.ok) {\n switch (r.error.code) {\n case 'OUT_OF_STOCK':\n showToast('This item is out of stock')\n break\n case 'ITEM_UNAVAILABLE':\n showToast('No longer available')\n break\n case 'VALIDATION_ERROR':\n showToast(r.error.message)\n break\n case 'RATE_LIMITED':\n showToast('Slow down; try again in a moment')\n break\n default:\n showToast(r.error.message)\n }\n}\n```\n\n \n\n \n\n## Retry helper\n\n \n\nThe client already retries each request before it returns: `429` and `503` (honouring `Retry-After`), other `5xx` responses, and network failures, up to `maxRetries` (default 3) with exponential backoff from `retryDelay` (default 1000 ms). A `5xx` that still fails comes back `retryable: true`. Add your own loop only around whole operations, only when the error is `retryable`, with backoff and a small attempt cap.\n\n \n \n\n```ts\n\nasync function withRetry<T>(\n fn: () => Promise<Result<T, CimplifyError>>,\n maxAttempts = 3,\n): Promise<Result<T, CimplifyError>> {\n let last!: Result<T, CimplifyError>\n for (let i = 0; i < maxAttempts; i++) {\n last = await fn()\n if (last.ok) return last\n if (!last.error.retryable) return last\n await new Promise((resolve) => setTimeout(resolve, 250 * 2 ** i))\n }\n return last\n}\n\nconst r = await withRetry(() => client.catalogue.getProducts({ limit: 24 }))\n```\n\n \n\n \n\n## Common error codes\n\n \n \n\n| Code | Retryable | Meaning |\n| --- | --- | --- |\n| `VALIDATION_ERROR` | No | Bad input; fix the request |\n| `NOT_FOUND` | No | Resource missing or filtered out |\n| `UNAUTHORIZED` | No | Sign in / refresh the session |\n| `FORBIDDEN` | No | Authenticated but not allowed |\n| `RATE_LIMITED` | Yes | Back off and retry |\n| `TOO_MANY_REQUESTS` | No | A sign-in code limit: the resend wait, or the contact's or network's codes for the day. Show the message; see [Sign-in code limits](/docs/sdk/auth#sign-in-code-limits) |\n| `NETWORK_ERROR` | Yes | Transport failed before response |\n| `TIMEOUT` | Yes | Server did not respond in time |\n| `SERVER_ERROR` | Yes | Generic 5xx |\n| `API_ERROR` | No | The server returned an error without a code, or an unreadable body |\n| `CANCELLED` | Yes | Your `AbortSignal` cancelled the request |\n| `IDEMPOTENCY_MISMATCH` | No | Same idempotency key, different body |\n\n \n\n \n\n## Checkout-specific codes\n\n \n \n\n| Code | Recoverable | Meaning |\n| --- | --- | --- |\n| `INVALID_CART` | No | Cart is empty or no longer valid |\n| `ALREADY_PROCESSING` | No | A checkout for this cart is already in flight |\n| `PAYMENT_FAILED` | Yes | Provider declined; let the user retry |\n| `FX_QUOTE_FAILED` | Yes | Could not lock an FX quote for the pay currency |\n| `AUTH_INCOMPLETE` | Yes | Customer must finish OTP / PIN authorization |\n| `AUTH_LOST` | Yes | Session expired mid-checkout |\n| `PRICE_CHANGED` / `QUOTE_VALIDATION_FAILED` | Yes | The cart no longer matches its quote; re-quote and ask the shopper to confirm |\n| `PAYMENT_OUTCOME_UNKNOWN` | Yes | The payment may still settle; check status before charging again |\n| `POPUP_BLOCKED` | Yes | The browser blocked the provider window; let the shopper open it from a tap |\n| `UNSUPPORTED_CURRENCY` / `PAYMENT_CAPABILITY_MISMATCH` | No | No online payment method for that currency |\n| `TIP_NOT_SUPPORTED` | No | A positive `tip_amount` was passed; tips are not part of checkout terms |\n| `ORIGIN_NOT_ALLOWED` | No | The checkout element is embedded on a site the business has not allowed |\n| `CHECKOUT_FAILED` | Sometimes | Generic terminal failure; read message + retryable |\n\n## Booking codes\n\n| Code | Meaning |\n| --- | --- |\n| `SLOT_UNAVAILABLE` | The slot was taken; refresh slots and ask the shopper to pick again |\n| `BOOKING_CONFLICT` | Overlaps an existing reservation |\n| `STAFF_UNAVAILABLE` | A reschedule's staff member is busy at the new time |\n| `CANNOT_RESCHEDULE` | The booking cannot move to that time |\n\n \n\n \n\n## Schema-shape errors (testing only)\n\n \n\nInside the testing harness, `assertX` helpers throw `SchemaViolationError` with structured `issues[]`. This is the only place the SDK throws.\n\n \n \n\n```ts\n\ntry {\n assertCart(response)\n} catch (e: unknown) {\n if (e instanceof SchemaViolationError) {\n console.error(e.toJSON()) // RFC 7807 + structured issues[]\n }\n}\n```\n\n \n\n \n\n## Related\n\n \n \n- [**Result<T, E>**](/docs/concepts/result)\n Why methods never throw, how to narrow\n\n \n- [**Idempotency**](/docs/concepts/idempotency)\n Safe retries for write methods\n\n \n- [**Checkout**](/docs/sdk/checkout)\n Where most error codes show up\n\n \n- [**Auth**](/docs/sdk/auth)\n UNAUTHORIZED + AUTH_LOST recovery\n"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "uploads",
|
|
122
|
+
"title": "Uploads",
|
|
123
|
+
"description": "Presigned upload flow: init \u2192 PUT \u2192 confirm",
|
|
124
|
+
"source_url": "https://cimplify.dev/docs/sdk/uploads",
|
|
125
|
+
"body": "## Upload a File (sugar)\n\n \n \n\n```ts\nasync function handleFile(file: File) {\n const r = await client.uploads.upload(file)\n if (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n }\n\n console.log(r.value.id, r.value.url)\n console.log(r.value.filename, r.value.content_type, r.value.size_bytes)\n}\n```\n\n \n\n \n\n## Step-by-step (when you need control)\n\n \n\nUse the explicit form when you want to show progress, retry the PUT independently, or upload bytes from a non-File source (Blob, stream, etc.).\n\n \n \n\n```ts\n\n// 1. Init: declare the bytes' SHA-256 and get a presigned URL\nconst digest = await contentSha256(file)\nconst init = await client.uploads.init(file.name, file.type, file.size, {\n digest,\n idempotencyKey: 'upload-' + digest.hex, // optional\n})\nif (!init.ok) {\n console.error(init.error.code, init.error.message)\n return\n}\n\nconst { upload_id, upload_url, expires_in_secs } = init.value\n\n// 2. PUT the bytes directly to storage, with the digest the URL is signed over\nconst put = await fetch(upload_url, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type, [CONTENT_SHA256_HEADER]: digest.base64 },\n})\n\nif (!put.ok) {\n console.error('PUT failed', put.status)\n return\n}\n\n// 3. Confirm: server validates and indexes\nconst confirmed = await client.uploads.confirm(upload_id)\nif (!confirmed.ok) return\n\nconsole.log(confirmed.value.id, confirmed.value.url)\n```\n\nWith `digest`, the presigned PUT is signed over `CONTENT_SHA256_HEADER` (`x-amz-checksum-sha256`), so storage refuses any other bytes; `upload(file)` always does this. Without `digest`, the server hashes the stored bytes at confirm. `contentSha256(blob)` hashes in chunks and returns `{ hex, base64 }`.\n\n \n\n \n\n## React: a drop zone\n\n \n \n\n```tsx\n'use client'\n\nexport function DropZone({ onUploaded }: { onUploaded: (result: UploadResult) => void }) {\n const { client } = useCimplifyClient()\n const [status, setStatus] = useState<'idle' | 'uploading' | 'error'>('idle')\n\n async function pick(e: React.ChangeEvent<HTMLInputElement>) {\n const file = e.target.files?.[0]\n if (!file) return\n\n setStatus('uploading')\n const r = await client.uploads.upload(file)\n if (!r.ok) {\n setStatus('error')\n return\n }\n\n setStatus('idle')\n onUploaded(r.value)\n }\n\n return (\n <label>\n <input type=\"file\" onChange={pick} />\n <span>{status === 'uploading' ? 'Uploading\u2026' : 'Choose a file'}</span>\n </label>\n )\n}\n```\n\n \n\n \n\n## UploadResult shape\n\n \n \n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `id` | `string` | Stable upload id |\n| `url` | `string` | Presigned read URL that expires after a few days; keep `id` as the durable reference |\n| `filename` | `string` | Original filename |\n| `content_type` | `string` | MIME |\n| `size_bytes` | `number` | |\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `init(filename, contentType, sizeBytes, { digest?, idempotencyKey? }?)` | `Result<UploadInitResponse>` |\n| `confirm(uploadId)` | `Result<UploadResult>` |\n| `upload(file)` | `Result<UploadResult>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Support**](/docs/sdk/support)\n Attach uploads to chat messages\n\n \n- [**Auth**](/docs/sdk/auth)\n Profile photo via `updateProfile`\n\n \n- [**Error handling**](/docs/sdk/errors)\n UPLOAD_FAILED is retryable\n\n \n- [**Idempotency**](/docs/concepts/idempotency)\n Replay-safe upload init\n"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "assets",
|
|
129
|
+
"title": "Assets",
|
|
130
|
+
"description": "Storefront brand assets \u2014 upload to CDN, build URLs, render through next/image",
|
|
131
|
+
"source_url": "https://cimplify.dev/docs/sdk/assets",
|
|
132
|
+
"body": 'The asset surface on the SDK is **read-side only**: building URLs for images you\'ve already uploaded via [`cimplify assets upload`](/docs/cli/assets). Customer-runtime uploads still go through `client.uploads.upload(file)`.\n\n## `assetUrl(path, opts?)`\n\nPure URL builder. Prepends the CDN base for relative paths, passes absolute URLs through with the transform query contract appended.\n\n```ts\n\nassetUrl("hero/main.jpg")\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg"\n\nassetUrl("hero/main.jpg", { w: 1200, format: "webp", quality: 80 })\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg?w=1200&format=webp&quality=80"\n\nassetUrl("hero/main.jpg", { base: "http://localhost:8787/storage" })\n// "http://localhost:8787/storage/hero/main.jpg"\n\nassetUrl("https://res.cloudinary.com/foo/bar.jpg")\n// "https://res.cloudinary.com/foo/bar.jpg" (absolute \u2192 passthrough)\n```\n\n### Options\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `base` | `string` | Override the CDN base. Defaults to `DEFAULT_CDN_BASE_URL`. |\n| `w` / `h` | `number` | Width / height in pixels (on-the-fly resizing) |\n| `quality` | `number` | 0\u2013100 |\n| `format` | `"auto" \\| "webp" \\| "avif" \\| "jpeg"` | `"auto"` lets the CDN pick by `Accept` header |\n\nThe transform query is only meaningful when the URL terminates on the Cimplify storefront CDN (which supports on-the-fly resizing). For external hosts, unknown params are typically ignored; the image still loads at its original size.\n\n## `isCimplifyAsset(src, base?)`\n\nDecides whether `src` is hosted on a Cimplify CDN. Used by the storefront\'s `next/image` loader to skip transforms for external URLs.\n\n```ts\n\nisCimplifyAsset("hero/main.jpg") // true (relative \u2192 ours)\nisCimplifyAsset("/img/seed/bakery/x.jpg") // true (dev mock passthrough)\nisCimplifyAsset("https://storefrontassetscdn.cimplify.io/x.jpg") // true\nisCimplifyAsset("https://cdn.cimplify.io/x.jpg") // true\nisCimplifyAsset("https://static-tmp.cimplify.io/seed/x.jpg") // true\nisCimplifyAsset("https://res.cloudinary.com/demo/upload/x.jpg") // false\nisCimplifyAsset("https://images.unsplash.com/photo-xyz") // false\n```\n\n## Built-in `next/image` loader\n\nEvery storefront template scaffolded by `cimplify init` ships `lib/cimplify-loader.ts` and points `images.loaderFile` at it. The loader:\n\n```ts\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, { base: cdnBase, w: width, quality, format: "auto" });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n```\n\n**Cloudinary URLs from `client.catalogue.getProducts()` flow through untouched.** Cimplify-hosted URLs get the transform query. The result: every `<Image>` works regardless of where its asset lives, and Cimplify-hosted ones get edge-optimized.\n\nTo point the loader at a different CDN (for testing, multi-CDN setups, etc.) set `NEXT_PUBLIC_CIMPLIFY_CDN_URL` in your `.env.local`:\n\n```\nNEXT_PUBLIC_CIMPLIFY_CDN_URL=http://localhost:8787/storage\n```\n\n## Per-component transforms\n\nWith `images.loaderFile` set globally in your `next.config.ts`, `<Image src={assetUrl("hero/main.jpg")} />` just works. On a project without a global loader, or to override transforms for one image, pass a loader built from `assetUrl`:\n\n```tsx\n\nconst heroLoader: ImageLoader = ({ src, width, quality }) =>\n assetUrl(src, { w: width, quality: quality ?? 85, format: "webp" });\n\nexport function Hero() {\n return <Image src="hero/main.jpg" loader={heroLoader} alt="..." width={1200} height={800} />;\n}\n```\n\nIn a React component, `useImage(path, opts?)` from `@cimplify/sdk/react` builds both halves at once: `src` is `assetUrl(path, opts)` and `loader` is a `next/image` loader that keeps your transforms (`base`, `h`, `quality`, and `format`, which defaults to `"auto"`) and sets the width `next/image` asks for. It memoises on the path and options, so the loader is stable across renders.\n\n```tsx\n\nexport function Hero() {\n const { src, loader } = useImage("hero/main.jpg", { quality: 85, format: "webp" });\n return <Image src={src} loader={loader} alt="..." width={1200} height={800} />;\n}\n```\n\n## Constants\n\n```ts\n\nDEFAULT_CDN_BASE_URL // "https://storefrontassetscdn.cimplify.io"\nCIMPLIFY_CDN_HOSTS // readonly ["storefrontassetscdn.cimplify.io", "cdn.cimplify.io", "static-tmp.cimplify.io"]\n```\n\n## What\'s not in v1\n\n- **`useUpload` hook**: runtime customer-facing uploads with progress + cancel. Deferred to v1.1. Customer uploads today go through `client.uploads.upload(file)` (no progress, no cancel; just `await`).\n- **Cloudinary / S3 path-transform support**: the loader passes external hosts through unchanged; Cloudinary\'s `c_fill,w_500/` path transforms aren\'t synthesized.\n- **`assets rm` server-side delete**: CLI removes manifest entries; the remote blob stays until v1.1\'s media-manager `DELETE` wiring.\n\n## Where it lives in the platform\n\n- Wire format: `POST /v1/businesses/{id}/assets/{init,confirm}`; see the management API reference\n- Served from the Cimplify asset CDN at `storefrontassetscdn.cimplify.io`\n- Storage key: `assets/{business_id}/{folder}/{upload_id}/{filename}`: immutable per upload; use the returned preview URL\n\n## Related\n\n- [`cimplify assets`](/docs/cli/assets): CLI surface for upload/ls/rm\n- [Uploads (customer-runtime)](/docs/sdk/uploads): `client.uploads.upload(file)` for private 7-day-presigned uploads\n'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "exit-codes",
|
|
136
|
+
"title": "CLI \u2014 Exit codes",
|
|
137
|
+
"description": "CLI exit codes (NOT_LINKED=4, TIMEOUT=12, \u2026)",
|
|
138
|
+
"source_url": "https://cimplify.dev/docs/cli#exit-codes",
|
|
139
|
+
"body": "## Exit codes\nDistinct per error class so callers can branch on `$?` without parsing output:\n\n| Code | Name | Meaning |\n| --- | --- | --- |\n| `0` | OK | Success. For `deploy`, deployment is `active`. |\n| `1` | FAILED | Generic failure (build failed, deploy errored). |\n| `2` | SUPERSEDED | A newer push raced this deploy; usually fine. |\n| `3` | ABORTED | User canceled (Ctrl-C) or `cimplify_cancel_deployment`. |\n| `4` | NOT_LINKED | `.cimplify/project.json` missing. Run `cimplify link <id>`. |\n| `5` | ALREADY_LINKED | The directory is already linked to a project. |\n| `6` | GIT_ERROR | A git step (push, ref lookup) failed. |\n| `7` | INTERACTIVE_REQUIRED | A prompt was needed but `--yes` not passed and stdin is non-TTY. |\n| `8` | PROJECT_NOT_FOUND | The linked project no longer exists or is not visible to this account. |\n| `9` | NOT_FOUND | Resource (domain, deploy, component, topic) does not exist. |\n| `10` | NETWORK_ERROR | Could not reach `api.cimplify.io`. |\n| `11` | SERVER_ERROR | The API answered with a server error. |\n| `12` | TIMEOUT | Long-poll exceeded its window; the underlying job may still be running. |\n| `20` | NOT_LOGGED_IN | No saved token. Run `cimplify login` or pass `--api-key`. |\n| `21` | AUTH_FAILED | Token expired or revoked. Re-login. |\n| `22` | UNAUTHORIZED | The token is valid but lacks access to this resource. |\n| `23` | STEP_UP_REQUIRED | A destructive operation needs a fresh re-auth. Run `cimplify auth step-up`. |\n| `30` | INVALID_INPUT | Argument/flag validation failed. |\n| `31` | BUILD_FAILED | The project's local build failed, so the deploy was not started. |\n\nThe same table ships offline as `cimplify explain exit-codes`.\n"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "inspect-json",
|
|
143
|
+
"title": 'Inspect \u2014 The JSON envelope (locked contract, `schema_version: "1"`)',
|
|
144
|
+
"description": "The locked JSON envelope cimplify inspect emits",
|
|
145
|
+
"source_url": "https://cimplify.dev/docs/cli/inspect#the-json-envelope-locked-contract-schema_version-1",
|
|
146
|
+
"body": '## The JSON envelope (locked contract, `schema_version: "1"`)\n```jsonc\n{\n "ok": true,\n "schema_version": "1",\n "data": {\n "business": {\n "handle": "bakeryco",\n "name": "Bakery & Co.",\n "currencies": ["USD", "GHS"],\n "locations": 2\n },\n "totals": {\n "products": 142,\n "collections": 8,\n "categories": 12,\n "distinct_tags": 24\n },\n "gaps": {\n "no_image": 11,\n "no_description": 7,\n "no_price": 0,\n "no_collection": 23,\n "no_tags": 56,\n "empty_collections": [{ "id": "col_seasonal", "name": "Seasonal" }],\n "empty_categories": [{ "id": "cat_specials", "name": "Specials" }]\n },\n "products": {\n "sample": [\n {\n "id": "prod_abc1",\n "name": "Croissant",\n "slug": "croissant",\n "price": { "amount": "3.50", "currency": "USD" },\n "has_image": true,\n "has_description": true,\n "tags": ["vegan", "bestseller"],\n "collection_ids": ["col_featured"],\n "category_ids": ["cat_breakfast"],\n "variant_count": 3\n }\n ],\n "next_cursor": "eyJpZCI6IjE5In0",\n "returned": 20\n },\n "collections": [\n {\n "id": "col_featured",\n "name": "Featured",\n "slug": "featured",\n "product_count": 12,\n "sample_product_ids": ["prod_abc1", "prod_abc2", "prod_abc3"]\n }\n ],\n "categories": {\n "tree": [\n { "id": "cat_breakfast", "name": "Breakfast", "slug": "breakfast", "product_count": 24, "children": [] }\n ]\n },\n "tags": {\n "top": [{ "tag": "vegan", "count": 18 }, { "tag": "bestseller", "count": 12 }]\n }\n },\n "meta": {\n "fetched_at": "2026-05-23T10:15:32Z",\n "source": "hosted",\n "sampled": true,\n "sample_size": 20,\n "gaps_method": "sample",\n "shape_hints": ["medium_catalogue", "uses_collections", "has_empty_collection", "untagged_majority", "has_image_gaps"],\n "suggestions": [\n { "kind": "fix_no_image", "count": 11 },\n { "kind": "fix_no_collection", "count": 23 },\n { "kind": "merchandise_empty_collection", "collection_id": "col_seasonal", "name": "Seasonal" }\n ],\n "suggested_commands": ["cimplify inspect catalogue --all"]\n }\n}\n```\n\n### Error envelope\n\n```jsonc\n{\n "ok": false,\n "schema_version": "1",\n "error": {\n "code": "NO_PUBLIC_KEY",\n "message": "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env or pass --public-key.",\n "remediation": "Get one at desk.cimplify.io/settings/api."\n }\n}\n```\n\n### Field rules\n\n- `ok` is always present.\n- `schema_version` is a contract over the whole document; bump on **removal**, **rename**, or **type change** of any field, `shape_hints` token, or `suggestions[].kind`. **Adding** is non-breaking.\n- `data.gaps.empty_collections` / `empty_categories` are always arrays (return `[]`, never omit).\n- `data.products.sample` is **always page-ordered** (never randomized).\n- `next_cursor` is `null` when the walk is exhausted.\n- `meta.source` is `"hosted"` or `"mock"`. Locked.\n- `meta.gaps_method` is `"sample"` or `"exact"`. Locked.\n- `meta.shape_hints` is **sorted alphabetically** (deterministic, diffable).\n- `meta.suggestions` is ordered by impact (most actionable first).\n- `meta.fetched_at` is the only timestamp; nothing in `data` carries time.\n'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "shape-hints",
|
|
150
|
+
"title": "Inspect \u2014 `shape_hints` vocabulary",
|
|
151
|
+
"description": "The canonical shape_hints vocabulary for cimplify inspect",
|
|
152
|
+
"source_url": "https://cimplify.dev/docs/cli/inspect#shape_hints-vocabulary",
|
|
153
|
+
"body": "## `shape_hints` vocabulary\nFlat string tokens for agents to branch on. Adding tokens is non-breaking; removing/renaming bumps `schema_version`.\n\n### Catalogue size\n\n| Token | Meaning |\n| --- | --- |\n| `empty_catalogue` | 0 products. Whole storefront needs an empty-state. |\n| `tiny_catalogue` | 1\u20139 products. Skip search, deep filters. |\n| `small_catalogue` | 10\u201399 products. Default UX. |\n| `medium_catalogue` | 100\u2013999 products. Search nice-to-have. |\n| `large_catalogue` | 1000+ products. Search + facets required. |\n\n### Feature presence\n\n| Token | Meaning |\n| --- | --- |\n| `has_variants` | At least one product has variants \u2192 variant picker UI. |\n| `has_addons` | At least one product has add-on options \u2192 modifier UI. |\n| `has_subscriptions` | Billing plans exist \u2192 recurring pricing toggle. |\n| `has_composites` | Bundle / composite products exist. |\n| `has_recipes` | Products linked to `ProductComponent` (restaurant / manufacturing). |\n| `has_time_profiles` | Products have time-of-day windows \u2192 hours-aware UI. |\n| `has_input_fields` | Buyer-supplied inputs (engraving, message). |\n| `has_custom_attributes` | Custom attribute schemas defined. |\n| `per_location_pricing` | Prices vary by location. |\n| `multi_currency` | >1 currency configured. |\n| `multi_location` | >1 location configured. |\n\n### Merchandising state\n\n| Token | Meaning |\n| --- | --- |\n| `uses_collections` | \u22651 non-empty collection exists. |\n| `uses_categories` | \u22651 non-empty category. |\n| `has_empty_collection` | \u22651 collection has 0 products. |\n| `has_empty_category` | \u22651 category has 0 products. |\n| `untagged_majority` | >50% of products are untagged. |\n| `unmerchandised_majority` | >50% of products aren't in any collection. |\n\n### Data quality\n\n| Token | Meaning |\n| --- | --- |\n| `has_image_gaps` | \u22651 product missing image. |\n| `has_description_gaps` | \u22651 product missing description. |\n| `has_price_gaps` | \u22651 product missing price. |\n\n### Environment\n\n| Token | Meaning |\n| --- | --- |\n| `mock_data` | Reading from `cimplify-mock` (key isn't `cpk_live_*` / `cpk_test_*`). |\n"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "revalidation-tags",
|
|
157
|
+
"title": "Revalidation",
|
|
158
|
+
"description": "Canonical cimplify:* cache-tag vocabulary + revalidate helpers",
|
|
159
|
+
"source_url": "https://cimplify.dev/docs/sdk/revalidation",
|
|
160
|
+
"body": "Cimplify storefronts cache catalogue reads with Next 16's ISR (`export const revalidate = N` + `next.tags` via the SDK's `cacheOptions`). When a merchant edits a product, collection, or brand asset, Cimplify invalidates the matching caches for your storefront automatically, with no `/api/revalidate` webhook round-trip required.\n\nThis page is the canonical contract between Cimplify and your storefront. Use the typed helpers (never hand-write tag strings) so the scheme stays in one place.\n\n## Why ISR, not Cache Components\n\nCimplify templates use Next 16's [Previous Model](https://nextjs.org/docs/app/guides/caching-without-cache-components): `export const revalidate` at the page level, `cacheOptions: { revalidate, tags }` on SDK reads (forwarded as `fetch().next.{revalidate,tags}`), and `revalidateTag` for invalidation. The `'use cache'` directive (Cache Components) is still experimental and its runtime constraints don't suit hosted storefronts. The Previous Model is stable, fully supported, and what every Cimplify template ships with. Keep `cacheComponents` **off** in `next.config.ts`.\n\n## Tags\n\nAll tags are namespaced under `cimplify:` so they can't collide with consumer tags. Build them via `tags` from `@cimplify/sdk/server`:\n\n```ts\n\nexport const revalidate = 3600;\n\nasync function getProducts() {\n const r = await getServerClient().catalogue.getProducts(\n { limit: 24 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n return r.ok ? r.value.items : [];\n}\n```\n\n### Catalogue\n\n| Helper | Tag |\n| --- | --- |\n| `tags.products()` | `cimplify:products` |\n| `tags.product(id)` | `cimplify:product:{id}` |\n| `tags.categories()` | `cimplify:categories` |\n| `tags.category(id)` | `cimplify:category:{id}` |\n| `tags.categoryProducts(id)` | `cimplify:category:{id}:products` |\n| `tags.collections()` | `cimplify:collections` |\n| `tags.collection(id)` | `cimplify:collection:{id}` |\n| `tags.collectionProducts(id)` | `cimplify:collection:{id}:products` |\n| `tags.tag(name)` | `cimplify:tag:{name}` (product tag, e.g. \"vegan\") |\n| `tags.addons()` | `cimplify:addons` |\n| `tags.addon(id)` | `cimplify:addon:{id}` |\n| `tags.stock()` | `cimplify:stock` |\n| `tags.stockFor(productId)` | `cimplify:stock:{productId}` |\n\n### Brand / business\n\n| Helper | Tag |\n| --- | --- |\n| `tags.business()` | `cimplify:business` |\n| `tags.brand()` | `cimplify:brand` |\n| `tags.locations()` | `cimplify:locations` |\n| `tags.location(id)` | `cimplify:location:{id}` |\n| `tags.locale()` | `cimplify:locale` |\n\n### Pricing / subscriptions\n\n| Helper | Tag |\n| --- | --- |\n| `tags.pricing()` | `cimplify:pricing` |\n| `tags.subscriptions()` | `cimplify:subscriptions` |\n| `tags.subscription(id)` | `cimplify:subscription:{id}` |\n\n### Customer-scoped (Server Actions only)\n\n| Helper | Tag |\n| --- | --- |\n| `tags.orders(customerId)` | `cimplify:orders:{customerId}` |\n| `tags.order(id)` | `cimplify:order:{id}` |\n\n## Granularity\n\nTag with **both** a broad and a precise tag on every read so either-flavour invalidation works:\n\n```ts\ncacheOptions: { revalidate: 3600, tags: [tags.products(), tags.product(id)] }\n```\n\nThe broad tag (`products`) catches \"I changed something product-related, invalidate everything.\" The precise tag (`product:{id}`) catches \"I changed exactly this one product, invalidate only its entries.\"\n\n## Revalidating from a Server Action\n\n```ts\n\nexport async function saveProduct(id: string, data: ProductInput) {\n await client.catalogue.updateProduct(id, data);\n await revalidateProduct(id);\n}\n```\n\nHelpers:\n\n| Helper | Invalidates |\n| --- | --- |\n| `revalidateProducts()` | `products` |\n| `revalidateProduct(id)` | `product:{id}` + `products` |\n| `revalidateCategories()` | `categories` |\n| `revalidateCategory(id)` | `category:{id}` + `category:{id}:products` + `categories` |\n| `revalidateCollections()` | `collections` |\n| `revalidateCollection(id)` | `collection:{id}` + `collection:{id}:products` + `collections` |\n| `revalidateBusiness()` | `business` |\n| `revalidateBrand()` | `brand` |\n| `revalidateLocations()` | `locations` |\n| `revalidateLocation(id)` | `location:{id}` + `locations` |\n| `revalidatePricing()` | `pricing` + `products` |\n| `revalidateAddOns()` | `addons` |\n| `revalidateAddOn(id)` | `addon:{id}` + `addons` |\n| `revalidateSubscriptions()` | `subscriptions` |\n| `revalidateSubscription(id)` | `subscription:{id}` + `subscriptions` |\n| `revalidateStock(productId?)` | `stock:{productId}` + `stock` (or just `stock` if no id) |\n| `revalidateByTag(tag)` | escape hatch for a raw tag |\n\n## The Cimplify \u2192 storefront eviction contract\n\nWhen a merchant edits something in the dashboard, Cimplify automatically invalidates the matching caches for every storefront serving that business, **globally, in ~1\u20133 seconds**, with no webhook round-trip into your storefront. You don't wire anything up; tagging your reads correctly is the whole contract.\n\nThe tags Cimplify dispatches use the **exact strings** from the tables above, keyed by database ID. If you've tagged your `cacheOptions.tags` with the helpers above, your storefront stays in sync automatically.\n\n## Purging your own tags on a Cimplify-hosted site\n\nCimplify evicts its own tags for you. When a site Cimplify builds and hosts also caches data Cimplify does not own (your own content source, a third-party feed, a webhook from another service), purge those tags yourself with `purgeSiteCache` from `@cimplify/sdk/server`. It evicts the tags from every live deployment of that site through the same pipeline Cimplify uses for merchant edits.\n\n```ts title=\"app/api/content-hook/route.ts\"\n\nexport async function POST(request: Request) {\n const { slug } = (await request.json()) as { slug: string };\n try {\n const { deployments } = await purgeSiteCache([\"posts\", `post:${slug}`]);\n return Response.json({ purged: deployments });\n } catch (error) {\n if (error instanceof SitePurgeError) {\n return Response.json({ error: error.message }, { status: error.status ?? 500 });\n }\n throw error;\n }\n}\n```\n\n```ts\nfunction purgeSiteCache(\n tags: readonly string[],\n options?: PurgeSiteCacheOptions,\n): Promise<PurgeSiteCacheResult>;\n```\n\nPass the same tag strings you gave `cacheOptions.tags`. The result's `deployments` is how many live deployments of the site Cimplify queued for eviction. An empty `tags` array returns `{ deployments: 0 }` without a request. Protect the route that calls it (check your webhook's signature, for example) so strangers can't empty your cache.\n\n### Credentials\n\nEvery site Cimplify builds is deployed with these variables, so a deployed site needs no setup:\n\n| Variable | Purpose |\n| --- | --- |\n| `CIMPLIFY_PROJECT_ID` | The site's project. |\n| `CIMPLIFY_SITE_PURGE_TOKEN` | A purge credential that names this one project. It can purge this site's cache and nothing else. |\n| `CIMPLIFY_API_URL` | The API base the purge is sent to; `https://api.cimplify.io` when unset. |\n\nThe call posts the tags to `POST /v1/pub/sites/{projectId}/cache/purge` with the token as a bearer credential. Keep it in server code; never send the token to the browser. Outside a Cimplify-built site (local `next dev`, another host) the project and token are unset and the call throws unless you pass them in `options`. If you set your own `CIMPLIFY_API_URL` (to the local mock, for example), pass `apiUrl: \"https://api.cimplify.io\"` to send purges to the API.\n\n| Option | Type | Default |\n| --- | --- | --- |\n| `projectId` | `string` | `CIMPLIFY_PROJECT_ID` |\n| `token` | `string` | `CIMPLIFY_SITE_PURGE_TOKEN` |\n| `apiUrl` | `string` | `CIMPLIFY_API_URL`, then `https://api.cimplify.io` |\n| `fetch` | `typeof fetch` | The global `fetch` |\n\n### Limits and errors\n\nOne call takes at most `SITE_PURGE_MAX_TAGS` (64) tags. Each tag is printable ASCII with no spaces, at most `SITE_PURGE_MAX_TAG_BYTES` (256) bytes. Both constants are exported. The SDK checks tags the way the API does, so a bad call fails before it leaves your site. Split larger purges into several calls.\n\nEvery failure throws a `SitePurgeError`. Its `status` is the HTTP status when the API refused the purge, and is unset when the call failed before sending:\n\n| Cause | `status` |\n| --- | --- |\n| Project or token missing, more than 64 tags, or a tag that is empty, too long or not printable | unset |\n| The tags failed the API's checks | `400` |\n| The token is missing or does not name this project | `403` |\n| The purge could not be queued | `500` |\n\n## Dynamic routes: tag by **ID**, never by slug\n\nCimplify dispatches revalidation tags **keyed by database ID**. Single product edits and deletes also fire the product's slug tag, but bulk updates and every other record kind (categories, collections, add-ons, locations) fire ID tags only, so a `/products/[slug]` page that caches itself under `tags.product(slug)` alone is not refreshed by a bulk change.\n\nResolve the slug to a record first, then tag with `record.id`:\n\n```ts title=\"app/products/[slug]/page.tsx (correct)\"\n\nexport const revalidate = 3600;\n\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) return [{ slug: \"__placeholder__\" }];\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n const { slug } = await params;\n\n // First resolve slug \u2192 product. Tag the resolution read with the\n // collection-level tag so adds/removes invalidate it. This call hits the\n // SDK with a fallback-broad tag.\n const r = await getServerClient().catalogue.getProductBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) notFound();\n\n // Now re-fetch with the resolved ID so the per-product tag is keyed on the\n // stable identifier; revalidateProduct(id) from Cimplify will hit this\n // entry on the next mutation.\n const detailed = await getServerClient().catalogue.getProduct(\n r.value.id,\n { cacheOptions: { revalidate: 3600, tags: [tags.product(r.value.id), tags.products()] } },\n );\n\n if (!detailed.ok) notFound();\n return <ProductDetail product={detailed.value} />;\n}\n```\n\nThe same pattern applies to `[slug]` routes for categories, collections, addons, locations, and so on: always tag with the resolved `record.id`, never with the URL slug.\n\n### Why this matters\n\n- **Renames don't orphan.** The cache key includes the slug (path-based), so a renamed product gets a fresh cache entry under the new slug. The old slug-keyed entry is invalidated via the `tags.products()` collection tag and will 404 naturally.\n- **IDs are stable.** Every product eviction carries `tags.product(id)`. A single product edit or delete also carries `tags.product(slug)` for the product's current slug, plus its previous slug after a rename, both read in the same transaction as the change. Bulk catalogue updates carry the ID tags only, so a page tagged by slug alone can stay cached after a bulk price change. Tag by ID (or add `tags.products()`) to be refreshed by every kind of change.\n- **You get instant freshness.** With Cimplify's automatic eviction, a 1-hour `revalidate` entry can sit cached and still flip fresh within ~3s of an edit, but only if the tag actually matches.\n\n## SDK timeout\n\nThe SDK's default per-call timeout is **5 seconds**. Picked so a single slow upstream call can't exhaust the page render budget; a hung `getCollections()` would otherwise take the entire page render with it. With the 5s default, a slow call fails fast, `Result.ok` becomes `false`, the page renders with empty data, and the result is cached so subsequent hits are instant.\n\nOverride only for genuinely-large bulk reads (admin tooling, batch jobs):\n\n```ts\n\nconst adminClient = createCimplifyClient({\n secretKey: process.env.CIMPLIFY_SECRET_KEY,\n timeout: 30_000, // 30s for a 10k-row export\n});\n```\n\n## Don't cache empty responses as if they were real\n\nIf a render produces empty results because of a transient backend hiccup, that empty render gets cached for the full `revalidate` window, and every visitor sees an empty storefront until someone manually invalidates.\n\nTwo defences:\n\n**1.** The SDK's 5s timeout above stops a slow call from hanging the whole render, but the result is still `Result.ok = false`. Branch on that and short-circuit to a not-found / soft state rather than committing the empty render to cache:\n\n```ts title=\"app/categories/[slug]/page.tsx\"\nasync function getCategory(slug: string) {\n const r = await getServerClient().catalogue.getCategoryBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.categories()] } },\n );\n // Distinguish a real 404 from a transient SDK error. Only `notFound()` on\n // the real 404; for transient errors, render a soft loading state so the\n // cache doesn't lock in an incorrect \"page couldn't load\" for an hour.\n if (!r.ok) {\n if (r.error.code === \"NOT_FOUND\") notFound();\n return { soft: true as const };\n }\n return { soft: false as const, category: r.value };\n}\n```\n\n**2.** Use the eviction-pipeline path for catalogue invalidation rather than relying on `revalidate` window expiry. A real edit in the dashboard purges the cache within seconds; the `revalidate` value is a backstop, not the primary refresh mechanism.\n\n## Tag taxonomy is locked\n\nThe vocabulary above is the v1 contract. Adding new tag helpers is non-breaking; removing or renaming any of them bumps the SDK major version. Agents that bake these strings into prompts can pin to v0.x of `@cimplify/sdk`.\n"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "inspect-suggestions",
|
|
164
|
+
"title": "Inspect \u2014 `suggestions[].kind` vocabulary",
|
|
165
|
+
"description": "The canonical suggestions[].kind vocabulary for cimplify inspect",
|
|
166
|
+
"source_url": "https://cimplify.dev/docs/cli/inspect#suggestionskind-vocabulary",
|
|
167
|
+
"body": '## `suggestions[].kind` vocabulary\nEach entry is a stable `kind` + typed payload. Copy lives here (and in `cimplify explain inspect-suggestions`) so agents don\'t invent phrasing.\n\n| Kind | Payload | Implied prompt |\n| --- | --- | --- |\n| `add_first_product` | `{}` | Catalogue is empty; seed or add a product. |\n| `add_first_collection` | `{}` | Products exist but no collections; group some. |\n| `fix_no_image` | `{ count }` | N products without images; add placeholders. |\n| `fix_no_description` | `{ count }` | N products lack descriptions; generate copy. |\n| `fix_no_price` | `{ count }` | N products have no price; open the editor. |\n| `fix_no_collection` | `{ count }` | N products not in any collection; suggest "Featured". |\n| `fix_no_tags` | `{ count }` | N untagged products; suggest tags from name/category. |\n| `merchandise_empty_collection` | `{ collection_id, name }` | Collection "X" is empty; pick products to feature. |\n| `merchandise_empty_category` | `{ category_id, name }` | Category "X" has no products; fill or remove. |\n| `tag_uncategorized_products` | `{ count }` | Majority untagged; bulk-tag from category. |\n| `enable_search_ui` | `{ product_count }` | Large catalogue; add search + filters. |\n'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "agent-prompts",
|
|
171
|
+
"title": "Agent prompts",
|
|
172
|
+
"description": "Copy-paste prompts for common Cimplify workflows",
|
|
173
|
+
"source_url": "https://cimplify.dev/docs/agent-prompts",
|
|
174
|
+
"body": 'These prompts are deliberately terse. Each one names the canonical doc the agent should read first, the exact commands or [MCP tools](/docs/mcp) to call, and the exit condition. Replace `<angle-bracket>` placeholders before running.\n\nEvery prompt assumes the agent has either:\n\n- the [Cimplify CLI](/docs/cli) installed and `cimplify login` complete, **or**\n- an MCP connection to `https://api.cimplify.io/mcp` ([setup](/docs/mcp#connect))\n\n## Ship a new storefront end-to-end\n\n```text title="Scaffold \u2192 deploy \u2192 custom domain"\nSet up a Cimplify <retail> storefront called <my-store> and deploy it to <shop.example.com>.\n\nSource of truth: https://cimplify.dev/docs/tldr\n- Use the MCP tools at api.cimplify.io/mcp when available; otherwise shell `cimplify <cmd> --json`.\n- After every command, echo the JSON envelope.\n- Stop on the first non-zero exit code and name it from https://cimplify.dev/docs/cli#exit-codes.\n- Poll deployment status with cimplify_get_deployment_status (or `cimplify status --json`); never `sleep`.\n- Idempotent: re-running any step is safe.\n```\n\n## Rebrand an existing storefront\n\n```text title="Apply a new brand to a scaffolded template"\nRebrand the storefront in <./my-store> to match these tokens:\n name: <Acme Coffee>\n primary: <#0F172A>\n accent: <#F59E0B>\n fontDisplay: <"Playfair Display">\n fontBody: <"Inter">\n\nRules (https://cimplify.dev/docs/templates/brand):\n- Single source of truth is `lib/brand.ts`. Update it first.\n- Mirror color tokens into `app/globals.css` `@theme` block. Do not hard-code colors elsewhere.\n- Do not edit ejected SDK components; they pick up tokens automatically.\n- Run `bun run check:brand` after edits; fix any schema errors it reports.\n- Verify by opening `http://localhost:3000` (start `bun dev` if not running).\n- Commit with message `brand: <name>`. Do not deploy.\n```\n\n## Add a homepage section\n\n```text title="New Server Component section, cache-tagged"\nAdd a `<Stories>` section to the homepage of the storefront in <./my-store>.\n\nPattern (mirrors the homepage sections under `components/` in the scaffolded template, such as `components/collection-strip.tsx`):\n- Create `components/stories.tsx` as an async Server Component. Read through `getServerClient()` and pass `cacheOptions: { revalidate: 3600, tags: [tags.collections()] }` to the SDK read (both from `@cimplify/sdk/server`). Do NOT use `\'use cache\'` / `cacheTag` / `cacheLife`. Cimplify storefronts run on CF Workers where Cache Components is broken; use the ISR Previous Model.\n- Source data from `client.catalogue.getCollectionBySlug("stories", { cacheOptions })` and `client.catalogue.getCollectionProducts(...)`.\n- Render with the template\'s `<SectionHeading>` and the SDK\'s `<ProductCard>` (do not eject).\n- Insert into `app/page.tsx` between the hero and the featured collection.\n- Run `bun run check`. It must pass typecheck, brand, cart-flow, contract.\n- Do not modify `lib/brand.ts` or any file under `components/ui/`.\n```\n\n## Migrate to a new SDK version\n\n```text title="SDK upgrade with breaking-change scan"\nUpgrade the storefront in <./my-store> from `@cimplify/sdk@<0.44.x>` to `@cimplify/sdk@<latest>`.\n\nProcess:\n- Read the release notes for the target range (`npm view @cimplify/sdk versions --json` to list, then `npm view @cimplify/sdk@<version>` per release).\n- Bump the pin in `package.json`, run `bun install`.\n- For each breaking change called out in the changelog, grep the project for the old symbol and apply the documented replacement.\n- Run `bun run check`. If `check:contract` fails, the SDK \u2194 mock contract drifted; stop and report.\n- Do not run `cimplify deploy`. End by printing a summary of every file you changed and every breaking change you applied.\n```\n\n## Add a custom checkout payment method\n\n```text title="Wire an additional payment option"\nAdd <mobile_money> as a checkout payment option in the storefront at <./my-store>.\n\nReference: https://cimplify.dev/docs/api-reference/checkout (purchase-intent mint, immutable terms review, then exact option confirmation).\n- Use the exported `PURCHASE_INTENT_*` constants and closed tender preference types.\n- Eject the checkout page with `cimplify add checkout-page` (there is no separate payment picker entry; `cimplify list` shows every ejectable component).\n- Add the option in the ejected page\'s payment step, preparing provider input into an opaque server-issued payment option.\n- Show `fee_amount` and `customer_debit_amount`, then confirm the reviewed option ID.\n- Validate mint and confirmation bodies against the purchase-intent schemas from `@cimplify/sdk/testing`.\n- Run `bun run check:cart`; the add\u2192checkout flow must still pass.\n```\n\n## Provision a CI deploy key\n\n```text title="One-time setup for headless deploys"\nCreate a CI-scoped API key for the Cimplify project linked at <./my-store>, and write the GitHub Actions snippet that uses it.\n\nSteps:\n- Confirm `.cimplify/project.json` exists; if not, fail with NOT_LINKED (exit 4).\n- Print the dashboard URL the human needs to visit to create the key (https://app.cimplify.io/settings/developer).\n- Once the user provides a `cak_live_\u2026` Personal key, store it as a repo secret named `CIMPLIFY_API_KEY` (instruct, do not perform).\n- Write `.github/workflows/deploy.yml` that runs on push to main: install CLI via the curl one-liner, `cimplify login --api-key "$CIMPLIFY_API_KEY" --json`, then `cimplify deploy --prod --yes --json`.\n- Print the exit-code legend from https://cimplify.dev/docs/cli#exit-codes so the workflow can map failures.\n```\n\n## Diagnose a deploy that didn\'t go live\n\n```text title="Triage a failed or stuck deployment"\nThe latest deploy of the project at <./my-store> isn\'t live. Find out why.\n\nTriage path:\n- `cimplify status --json`: what\'s the latest deployment_id and status?\n- If status is `failed`, `cimplify logs --deployment <id> --json | tail -200`. Quote the first error line you see.\n- If status is `queued` or `building` for >5 minutes, the build is stuck; try `cimplify rollback <previous-deploy-id>`.\n- If status is `superseded` (exit 2), a newer push raced it; usually fine, so confirm with `cimplify status` again.\n- If domains misroute, `cimplify domains ls --json`: confirm the verified+attached domain.\n- Report: deployment_id, status, the failing log line (if any), and the recommended next action. Do not auto-rollback without confirming.\n```\n\n## Add a hero image and reference it in the homepage\n\n```text title="Upload brand assets \u2192 use them in source"\nAdd the image at <./Downloads/hero.jpg> as the homepage hero for the storefront in <./my-store>.\n\nReference: https://cimplify.dev/docs/cli/assets\n- `cp <./Downloads/hero.jpg> public/hero/main.jpg`\n- `cimplify assets upload public/hero/ --folder hero --json`: captures the manifest entry for `hero/main.jpg`.\n- Edit `app/page.tsx`: replace the hero `<Image>` `src` with `assetUrl("hero/main.jpg")` from `@cimplify/sdk`.\n- Do NOT touch existing `<Image>` calls that point at Cloudinary or other external hosts; the loader passes them through unchanged.\n- Run `bun run check`. Open `http://localhost:3000`; verify the hero image renders.\n- Commit `cimplify-assets.json` along with the source change so the manifest is reviewable.\n```\n\n## Audit caching and asset delivery\n\n```text title="Find caching and asset regressions on a live storefront"\nAudit the live storefront at <https://my-store.mycimplify.com> for caching and asset-delivery regressions.\n\nSources of truth:\n- https://cimplify.dev/docs/sdk/optimization (page + asset caching)\n- https://cimplify.dev/docs/templates/seo-and-resource-hints (responsive images)\n\n- Page cache: curl /, /shop, and a PDP 2\u20133\xD7 each; `X-Cimplify-Edge-Cache` must warm to HIT and `Cache-Control` must carry `s-maxage`. A BYPASS or missing `s-maxage` means the page lost its `revalidate` or carries a Cookie.\n- Asset cache: pull a `/_next/static/*` chunk; it must be `public, max-age=31536000, immutable`.\n- Images: flag raw `<img>` from a third-party CDN, and Cloudinary URLs with no `/upload/<transform>/` segment (full-resolution originals).\n- Report a table (route/asset, observed header, expected, severity). Do not change source unless asked.\n```\n\n## Reset a sandbox storefront to a clean state\n\n```text title="Wipe and re-seed for repeatable demos"\nReset the local mock state for the storefront in <./my-store>.\n\n- With `bun dev` running, `curl -X POST localhost:8787/_mock/reset -H \'content-type: application/json\' -d \'{"seed":"<retail>"}\'` (clears the mock\'s in-memory state and re-seeds with the chosen industry fixture).\n- Or stop `bun dev` and start it again: the mock boots fresh from the seed named in `scripts.dev`.\n- Verify: `curl localhost:8787/api/v1/catalogue/products` returns the seed\'s products.\n- Do not touch the linked cloud project; this is local-only.\n```\n\n## How these are built\n\nEvery prompt above is structured the same way so you can adapt them:\n\n1. **Goal**: one sentence, with `<angle-bracket>` placeholders for the moving parts.\n2. **Source of truth**: the canonical doc URL the agent reads first.\n3. **Rules**: operational guardrails for which tools to call, what to echo, and when to stop.\n4. **Exit condition**: what counts as success, what counts as a halt.\n\nIf you build a prompt that should live here, open a PR against `content/docs/agent-prompts.mdx`. Keep them under ~12 lines; if a workflow needs more, it belongs in a dedicated doc page and the prompt should link to it.\n'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "seeds",
|
|
178
|
+
"title": "seeds",
|
|
179
|
+
"description": "Available mock seeds and how to switch between them",
|
|
180
|
+
"source_url": "https://cimplify.dev/docs/cli/mock",
|
|
181
|
+
"body": '## Mock seeds\n\nEvery Cimplify storefront ships with a local Hono mock backed by one of\nthese seeds. Switch with `cimplify mock seed <name>` (or hand-edit the\n`--seed` flag in your `package.json` `scripts.dev`).\n\n| Seed | Notes |\n| --- | --- |\n| `auto` | (no description) |\n| `context` | (no description) |\n| `default` | Generic small business \u2014 minimal fixtures |\n| `empty` | No data \u2014 for blank-slate testing |\n| `fashion` | Apparel with multi-axis variants (size, color) |\n| `grocery` | High-SKU produce + perishables |\n| `names` | (no description) |\n| `pharmacy` | Wellspring Apothecary \u2014 health products with regulated metadata |\n| `reesa-storefront` | Reesa-specific demo storefront |\n| `restaurant` | Menu-driven with reservations |\n| `retail` | Electronics-focused storefront with variants and add-ons |\n| `services` | Bookable services (e.g. salon, spa) |\n| `source` | (no description) |\n\n### Finding fixture IDs\n\nEach seed is a TypeScript file at `packages/sdk/src/mock/seeds/<name>.ts`\nin the cimplify monorepo. Product slugs, categories, and variant axes\nare defined inline. Grep that file when you need a real slug for a\nhard-coded demo:\n\n```bash\ngrep "slug:" node_modules/@cimplify/sdk/dist/mock/seeds/<name>.* | head\n```\n\nA future `cimplify mock products ls --json` will surface these IDs\ndirectly without grep.\n'
|
|
182
|
+
}
|
|
183
|
+
];
|
|
184
|
+
var TOPICS_BY_NAME = Object.fromEntries(
|
|
185
|
+
TOPICS.map((t) => [t.name, t])
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// src/commands/explain.ts
|
|
189
|
+
var CLI_VERSION = package_default.version ?? "unknown";
|
|
190
|
+
var NAME_COL_WIDTH = 14;
|
|
191
|
+
function listTopics(topics = TOPICS) {
|
|
192
|
+
const out = [];
|
|
193
|
+
out.push(bold("cimplify explain"));
|
|
194
|
+
out.push(dim(`\u2014 canonical guidance, bundled in cli-v${CLI_VERSION}`));
|
|
195
|
+
out.push("");
|
|
196
|
+
for (const t of topics) {
|
|
197
|
+
out.push(` ${bold(t.name.padEnd(NAME_COL_WIDTH))}${t.description}`);
|
|
198
|
+
}
|
|
199
|
+
out.push("");
|
|
200
|
+
out.push(dim(`Run \`cimplify explain <topic>\` to print the guide.`));
|
|
201
|
+
out.push(dim(`Add --json for a machine-readable envelope.`));
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
function renderTopic(topic) {
|
|
205
|
+
const out = [];
|
|
206
|
+
out.push(bold(`cimplify explain ${topic.name}`));
|
|
207
|
+
out.push("");
|
|
208
|
+
out.push(topic.body.trimEnd());
|
|
209
|
+
out.push("");
|
|
210
|
+
out.push(dim(`Source: ${topic.source_url} (snapshot in cli-v${CLI_VERSION})`));
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
function getTopic(name) {
|
|
214
|
+
return TOPICS_BY_NAME[name] ?? null;
|
|
215
|
+
}
|
|
216
|
+
function knownTopicNames() {
|
|
217
|
+
return TOPICS.map((t) => t.name);
|
|
218
|
+
}
|
|
219
|
+
async function run(argv) {
|
|
220
|
+
const args = parseArgs(argv);
|
|
221
|
+
const name = args.positional[0];
|
|
222
|
+
if (!name) {
|
|
223
|
+
for (const line of listTopics()) info(line);
|
|
224
|
+
result({
|
|
225
|
+
topics: TOPICS.map((t) => ({
|
|
226
|
+
name: t.name,
|
|
227
|
+
description: t.description,
|
|
228
|
+
source_url: t.source_url
|
|
229
|
+
}))
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const topic = getTopic(name);
|
|
234
|
+
if (!topic) {
|
|
235
|
+
throw new CliError(
|
|
236
|
+
CLI_ERROR_CODE.NOT_FOUND,
|
|
237
|
+
`Unknown topic "${name}".`,
|
|
238
|
+
{ remediation: `Run \`cimplify explain\` to see all topics (${knownTopicNames().length} available).` }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
for (const line of renderTopic(topic)) info(line);
|
|
242
|
+
result({
|
|
243
|
+
topic: topic.name,
|
|
244
|
+
title: topic.title,
|
|
245
|
+
body: topic.body,
|
|
246
|
+
source_url: topic.source_url,
|
|
247
|
+
snapshot_version: CLI_VERSION
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { run as default, getTopic, listTopics, renderTopic };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { commonOptions } from './chunk-PVW5D5AW.mjs';
|
|
3
|
+
import { prepareSession, runSession, FINAL_CAPTURE } from './chunk-JTYVGORV.mjs';
|
|
4
|
+
import { SessionError } from './chunk-SZXO6YSX.mjs';
|
|
5
|
+
import './chunk-XF3MGADK.mjs';
|
|
6
|
+
import './chunk-74QZQSCL.mjs';
|
|
7
|
+
import './chunk-NEB372WD.mjs';
|
|
8
|
+
import './chunk-TYR4Q37L.mjs';
|
|
9
|
+
import './chunk-XZG3DTXV.mjs';
|
|
10
|
+
import './chunk-JNCEKW2Y.mjs';
|
|
11
|
+
import { requireInteractiveProject, workPath } from './chunk-BTNVYAC4.mjs';
|
|
12
|
+
import './chunk-VHK7WBO3.mjs';
|
|
13
|
+
import { readLoopProfile, LOOP_PROFILE_FILE, HOT_MS_PER_FRAME, HOT_ROWS_PER_CALL } from './chunk-454RQ6NR.mjs';
|
|
14
|
+
import './chunk-JKECUAAV.mjs';
|
|
15
|
+
import { compactReport, formatFinding } from './chunk-THATLMEX.mjs';
|
|
16
|
+
import './chunk-HQPZQIWW.mjs';
|
|
17
|
+
import './chunk-KQNZKV6E.mjs';
|
|
18
|
+
import './chunk-QVR6V6QP.mjs';
|
|
19
|
+
import './chunk-N2WZJX5P.mjs';
|
|
20
|
+
import './chunk-EYEMPIMP.mjs';
|
|
21
|
+
import './chunk-BGM7CH5G.mjs';
|
|
22
|
+
import './chunk-25ZPFHLF.mjs';
|
|
23
|
+
import './chunk-SGT5IJVW.mjs';
|
|
24
|
+
import { parseArgs, flagBool } from './chunk-XQAHREKO.mjs';
|
|
25
|
+
import './chunk-XBQ4LO2N.mjs';
|
|
26
|
+
import './chunk-GGPN4QYQ.mjs';
|
|
27
|
+
import { isJsonMode, result, CliError, CLI_ERROR_CODE } from './chunk-I7GEHLDD.mjs';
|
|
28
|
+
|
|
29
|
+
// src/interactives/kernels/loops/report.ts
|
|
30
|
+
var MS_DIGITS = 3;
|
|
31
|
+
function summarizeLoops(reports, profile) {
|
|
32
|
+
const out = reports.map((r) => {
|
|
33
|
+
const sample = profile?.loops[r.key];
|
|
34
|
+
const frames = profile?.frames ?? 0;
|
|
35
|
+
const msPerFrame = sample !== void 0 && frames > 0 ? sample.ms / frames : r.hot.msPerFrame;
|
|
36
|
+
const rowsPerCall = sample !== void 0 && sample.calls > 0 && sample.rows >= 0 ? sample.rows / sample.calls : r.hot.rowsPerCall;
|
|
37
|
+
const callsPerFrame = sample !== void 0 && frames > 0 ? sample.calls / frames : 0;
|
|
38
|
+
const hot = sample !== void 0 ? (msPerFrame ?? 0) >= HOT_MS_PER_FRAME || (rowsPerCall ?? 0) >= HOT_ROWS_PER_CALL && callsPerFrame >= 0.5 : r.hot.hot;
|
|
39
|
+
return { where: `${r.path}:${r.line}:${r.column}`, status: r.status, msPerFrame, rowsPerCall, hot, message: r.message, estimate: r.estimate };
|
|
40
|
+
});
|
|
41
|
+
return out.sort((a, b) => (b.msPerFrame ?? -1) - (a.msPerFrame ?? -1));
|
|
42
|
+
}
|
|
43
|
+
function formatLoops(summary) {
|
|
44
|
+
const hot = summary.filter((s) => s.hot);
|
|
45
|
+
if (hot.length === 0) return "";
|
|
46
|
+
const lines = hot.map((s) => {
|
|
47
|
+
const time = s.msPerFrame === null ? "no capture time" : `${s.msPerFrame.toFixed(MS_DIGITS)} ms per frame`;
|
|
48
|
+
const rows = s.rowsPerCall === null ? "" : `, ${Math.round(s.rowsPerCall)} rows per call`;
|
|
49
|
+
const verdict = s.status === "missed" ? `stays in JavaScript: ${s.message}${s.estimate === null ? "" : ` (estimated ${s.estimate}x faster without a JIT once fixed)`}` : s.status === "excluded" ? "stays in JavaScript: listed in no_lower" : s.status === "refused" ? `stays in JavaScript: ${s.message}` : `lowers to a kernel${s.estimate === null ? "" : ` (estimated ${s.estimate}x faster without a JIT)`}`;
|
|
50
|
+
return ` ${s.where} ${time}${rows} ${verdict}`;
|
|
51
|
+
});
|
|
52
|
+
return `Hot loops:
|
|
53
|
+
${lines.join("\n")}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/interactives/commands/inspect.ts
|
|
57
|
+
async function run(argv) {
|
|
58
|
+
const args = parseArgs(argv);
|
|
59
|
+
const root = process.cwd();
|
|
60
|
+
requireInteractiveProject(root, "inspect");
|
|
61
|
+
const options = await commonOptions(args, root);
|
|
62
|
+
try {
|
|
63
|
+
const setup = await prepareSession(root);
|
|
64
|
+
const frames = await runSession(setup, { ...options, screenshots: true, inspect: true });
|
|
65
|
+
const final = frames.find((f) => f.sidecar.name === FINAL_CAPTURE) ?? frames[frames.length - 1];
|
|
66
|
+
const report = final.inspect;
|
|
67
|
+
if (report === null) throw new SessionError("the inspection did not run");
|
|
68
|
+
const loops = summarizeLoops(setup.build.result.loops ?? [], readLoopProfile(workPath(root, LOOP_PROFILE_FILE)));
|
|
69
|
+
if (isJsonMode()) {
|
|
70
|
+
result({ ...report, loops });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(`${compactReport(report)}
|
|
74
|
+
`);
|
|
75
|
+
const hotLoops = formatLoops(loops);
|
|
76
|
+
if (hotLoops.length > 0) process.stdout.write(`${hotLoops}
|
|
77
|
+
`);
|
|
78
|
+
if (!flagBool(args, "text")) for (const finding of report.checks.findings) process.stderr.write(`${formatFinding(finding)}
|
|
79
|
+
`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof SessionError) throw new CliError(CLI_ERROR_CODE.BUILD_FAILED, error.message);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { run as default };
|