@greatstore/cli 0.1.1 → 0.1.3
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/CHANGELOG.md +15 -0
- package/README.md +15 -16
- package/dist/cli.js +178 -136
- package/dist/gs-skill/SKILL.md +83 -0
- package/dist/gs-skill/recipes/ask-about-this.md +31 -0
- package/dist/gs-skill/recipes/campaign-hero.md +43 -0
- package/dist/gs-skill/recipes/comparison-table.md +80 -0
- package/dist/gs-skill/recipes/complete-the-look.md +59 -0
- package/dist/gs-skill/recipes/custom-chat-button.md +25 -0
- package/dist/gs-skill/recipes/gift-finder-funnel.md +97 -0
- package/dist/gs-skill/recipes/launchers.md +109 -0
- package/dist/gs-skill/recipes/page-action-suite.md +90 -0
- package/dist/gs-skill/recipes/product-faq.md +58 -0
- package/dist/gs-skill/references/agents-and-mcp.md +35 -0
- package/dist/gs-skill/references/chat-components.md +224 -0
- package/dist/gs-skill/references/embed-api.md +381 -0
- package/dist/gs-skill/references/push-notifications.md +66 -0
- package/dist/gs-skill/references/store-admin.md +121 -0
- package/dist/gs-skill/references/structured-content.md +252 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -635,11 +635,89 @@ async function switchCommand(positional) {
|
|
|
635
635
|
// src/commands/skill.ts
|
|
636
636
|
import * as fs3 from "fs";
|
|
637
637
|
import * as os2 from "os";
|
|
638
|
+
import * as path4 from "path";
|
|
639
|
+
|
|
640
|
+
// src/skill.ts
|
|
641
|
+
import { existsSync as existsSync2 } from "fs";
|
|
638
642
|
import * as path3 from "path";
|
|
643
|
+
import { fileURLToPath } from "url";
|
|
644
|
+
var SKILL_DIR_NAME = "greatstore";
|
|
645
|
+
function skillSourceDir() {
|
|
646
|
+
for (const candidate of ["./gs-skill/", "../../gs-skill/"]) {
|
|
647
|
+
const dir = path3.resolve(fileURLToPath(new URL(candidate, import.meta.url)));
|
|
648
|
+
if (existsSync2(path3.join(dir, "SKILL.md"))) return dir;
|
|
649
|
+
}
|
|
650
|
+
throw new Error(
|
|
651
|
+
"internal: this install of the CLI is missing the agent skill files"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/commands/skill.ts
|
|
656
|
+
function skillCommand() {
|
|
657
|
+
process.stdout.write(installInstructions(skillSourceDir()));
|
|
658
|
+
}
|
|
659
|
+
function installInstructions(source) {
|
|
660
|
+
if (looksEphemeral(source)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
[
|
|
663
|
+
"This copy of the CLI is running from a temporary cache, so the agent",
|
|
664
|
+
"skill has no stable location to install from. Install the CLI first,",
|
|
665
|
+
"then ask again:",
|
|
666
|
+
"",
|
|
667
|
+
" npm install -g @greatstore/cli",
|
|
668
|
+
" gs skill"
|
|
669
|
+
].join("\n")
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return [
|
|
673
|
+
"GreatStore agent skill \u2014 installation instructions for an AI coding agent.",
|
|
674
|
+
"",
|
|
675
|
+
"Skill source on this machine:",
|
|
676
|
+
` ${source}`,
|
|
677
|
+
"",
|
|
678
|
+
`Symlink that folder into your skills directory as "${SKILL_DIR_NAME}":`,
|
|
679
|
+
"",
|
|
680
|
+
" mkdir -p <SKILLS_DIR>",
|
|
681
|
+
` ln -sfn "${source}" "<SKILLS_DIR>/${SKILL_DIR_NAME}"`,
|
|
682
|
+
"",
|
|
683
|
+
"Symlink it, do not copy it: the link keeps the skill current every",
|
|
684
|
+
"time @greatstore/cli is upgraded, with nothing to re-run.",
|
|
685
|
+
"",
|
|
686
|
+
"<SKILLS_DIR> is wherever you load skills from \u2014 for Claude Code that",
|
|
687
|
+
"is ~/.claude/skills for every project, or ./.claude/skills for this",
|
|
688
|
+
"one. Use your own location if it differs, and create it if missing.",
|
|
689
|
+
`Then read <SKILLS_DIR>/${SKILL_DIR_NAME}/SKILL.md to confirm the install.`,
|
|
690
|
+
"",
|
|
691
|
+
`On Windows: mklink /D "<SKILLS_DIR>\\${SKILL_DIR_NAME}" "${source}"`,
|
|
692
|
+
""
|
|
693
|
+
].join("\n");
|
|
694
|
+
}
|
|
695
|
+
function looksEphemeral(dir) {
|
|
696
|
+
const posix = dir.split(path4.sep).join("/");
|
|
697
|
+
if (/\/(?:_npx|_cacache|\.bun\/install\/cache)\//.test(posix)) return true;
|
|
698
|
+
if (/\/(?:dlx-|bunx-|\.npx-)/.test(posix)) return true;
|
|
699
|
+
const tmp = os2.tmpdir();
|
|
700
|
+
return isInside(dir, tmp) || isInside(dir, realPath(tmp));
|
|
701
|
+
}
|
|
702
|
+
function isInside(child, parent) {
|
|
703
|
+
const rel = path4.relative(parent, child);
|
|
704
|
+
return rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
705
|
+
}
|
|
706
|
+
function realPath(p) {
|
|
707
|
+
try {
|
|
708
|
+
return fs3.realpathSync(p);
|
|
709
|
+
} catch {
|
|
710
|
+
return p;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/commands/apps/init.ts
|
|
715
|
+
import * as fs4 from "fs";
|
|
716
|
+
import * as path5 from "path";
|
|
639
717
|
|
|
640
718
|
// src/template.ts
|
|
641
719
|
import { readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
642
|
-
import { fileURLToPath } from "url";
|
|
720
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
643
721
|
var cache = null;
|
|
644
722
|
function loadTemplate() {
|
|
645
723
|
if (cache) return cache;
|
|
@@ -668,58 +746,8 @@ function applyTemplate(content, vars) {
|
|
|
668
746
|
return out;
|
|
669
747
|
}
|
|
670
748
|
|
|
671
|
-
// src/skill.ts
|
|
672
|
-
var SKILL_DIR_NAME = "greatstore";
|
|
673
|
-
var cache2 = null;
|
|
674
|
-
function loadSkill() {
|
|
675
|
-
if (cache2) return cache2;
|
|
676
|
-
cache2 = true ? JSON.parse('{"SKILL.md":"---\\nname: greatstore\\ndescription: Build AI-powered shopping experiences with GreatStore on a merchant\'s website and store. Use when installing the GreatStore chat widget on a site, generating AI content for custom UI with generateStructuredContent, adding chat entry points (sendMessage), letting the assistant act on the page via WebMCP page tools (document.modelContext.registerTool), authoring custom in-chat React components with the gs CLI, administering the store from the CLI (gs configure, gs connectors \u2014 origin allowlists, CSP hosts, MCP connectors), setting up web push re-engagement, or connecting AI agents to a store\'s MCP endpoints. Covers setup, schema design, caching behavior, component authoring, store administration, and ready-made recipes.\\n---\\n\\n# Building with GreatStore\\n\\nGreatStore gives a store an AI shopping assistant on two surfaces: a hosted\\nstorefront at `https://<slug>.greatstore.ai/`, and an embedded chat widget on\\nthe merchant\'s own site, installed with one script tag:\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nEverything else a site can do with GreatStore is documented in the\\nreferences below. Read the one that matches the task before writing code \u2014\\neach facet has non-obvious rules (caching, grounding, result shapes, design\\nconstraints) that the references spell out.\\n\\n## Index\\n\\n| Goal | Use | Read |\\n|---|---|---|\\n| Install the widget; control the panel; readiness, events, troubleshooting | `window.GreatStore` SDK | [references/embed-api.md](references/embed-api.md) |\\n| AI-generated, catalog-grounded content rendered in **your own HTML/CSS** (highlights, comparisons, FAQs, gift guides) | `generateStructuredContent(schema, prompt)` | [references/structured-content.md](references/structured-content.md) |\\n| Copy-paste on-page experiences | recipes built on the SDK | the [Recipes](#recipes) table below |\\n| Contextual **conversation entry points** anywhere on the page | `sendMessage(text)`, `open()`, `?gs_chat=open` | [references/embed-api.md](references/embed-api.md) |\\n| Feed the assistant **invisible page/component state** (current product, cart, selected variant) | `updateModelContext(text)`; component `onUpdateModelContext` | [references/embed-api.md](references/embed-api.md), [references/chat-components.md](references/chat-components.md) |\\n| Let the assistant **act on the page** (cart, navigation, filters) | WebMCP: `document.modelContext.registerTool(...)` | [references/embed-api.md](references/embed-api.md) |\\n| Custom **interactive UI inside the chat** (configurators, quizzes, size guides, booking forms) | remote components shipped with the `gs` CLI | [references/chat-components.md](references/chat-components.md) |\\n| Expand a component\'s image in an **on-brand full-screen lightbox** | component `onShowLightbox({ src, originRect })` | [references/chat-components.md](references/chat-components.md) |\\n| **Re-engage shoppers** with browser notifications | merchant-hosted `gs.js` + `enableNotifications()` | [references/push-notifications.md](references/push-notifications.md) |\\n| Connect **AI agents** to the store (shopping tools over MCP, CLI docs for coding agents) | the store\'s MCP endpoints | [references/agents-and-mcp.md](references/agents-and-mcp.md) |\\n| **Administer the store** \u2014 read config/connectors to ground decisions; self-serve origin allowlists and CSP hosts | `gs configure`, `gs connectors` | [references/store-admin.md](references/store-admin.md) |\\n\\n## Three things to know before any of it\\n\\n- **Every code sample in this skill is a reference implementation, not a\\n drop-in.** Samples are framework-free vanilla JS so they stay portable \u2014\\n re-express the same logic in the conventions of the repo you\'re working\\n in (React, Vue, Shopify Liquid sections, Svelte, \u2026); never retrofit the\\n sample as-is into a codebase with its own framework.\\n- The page\'s domain **must be in the store\'s allowed domains**. If it isn\'t,\\n nothing works and the console shows\\n `[GreatStore] Chat is unavailable on <origin>\u2026` \u2014 check this first whenever\\n the embed appears dead. You can verify and fix it yourself with the `gs`\\n CLI (read-merge-write on `extraOrigins` \u2014 see\\n [references/store-admin.md](references/store-admin.md)).\\n- Every SDK call is safe immediately after the script tag \u2014 pre-mount calls\\n queue and replay in order, and the SDK pre-warms itself in the background.\\n\\n## Recipes\\n\\nComplete, framework-free implementations, one per file. Shared conventions:\\ncontainers start `hidden` and reveal only on success (a failed generation\\nchanges nothing); generated strings render via `textContent`, never\\n`innerHTML`; every recipe guards on `window.GreatStore`; prompts stay\\ndeterministic per page so repeat renders hit the cache.\\nInline real product/page data into prompts where the platform exposes it.\\n\\n| Recipe | What it builds | Use it for |\\n|---|---|---|\\n| [GreatStore launchers](recipes/launchers.md) | Horizontally scrollable AI-generated chips \u2014 engaging first-person questions about the current page; tap to ask the assistant. | Instant engagement on any page type \u2014 product, collection, blog, home. Simple yet effective; start here. |\\n| [Ask-about-this entry points](recipes/ask-about-this.md) | One-line `sendMessage` buttons wired to existing page elements. | Size guides, shipping rows, out-of-stock badges \u2014 anywhere a shopper hesitates. |\\n| [Product FAQ accordion](recipes/product-faq.md) | Grounded pre-purchase Q&A with an \\"ask us\\" handoff into chat. | Product pages; answering objections before they cost the sale. |\\n| [Comparison table](recipes/comparison-table.md) | AI-picked representative products compared on category-relevant criteria. | Collection pages where shoppers weigh options. |\\n| [Complete the look](recipes/complete-the-look.md) | Catalog-grounded cross-sell strip with a reason per pick. | Product pages; raising order value with genuine pairings. |\\n| [Campaign hero](recipes/campaign-hero.md) | Seasonal homepage hero copy, cache-keyed to the ISO week. | Fresh homepage/campaign copy without manual rewrites. |\\n| [Gift finder funnel](recipes/gift-finder-funnel.md) | Quiz teaser \u2192 chat handoff \u2192 page-tool navigation; the full funnel. | Gifting seasons, guided discovery, homepage engagement. |\\n| [Page-action suite](recipes/page-action-suite.md) | WebMCP cart/page tools every conversation can use. | Any site where the assistant should act, not just advise. |\\n| [Custom chat button](recipes/custom-chat-button.md) | Branded launcher synced via `ready` + `open`/`close` events. | Replacing the default launcher with the site\'s own UI. |\\n\\n## How the facets combine\\n\\nThe strongest pattern is the **teaser \u2192 conversation \u2192 action** funnel:\\n`generateStructuredContent` renders a grounded teaser in the merchant\'s\\ndesign; each option\'s click handler calls `sendMessage` with the shopper\'s\\nchoice, dropping them into a conversation with momentum; WebMCP page tools\\nand custom chat components let that conversation actually do things \u2014 add to\\ncart, configure a product, book a slot \u2014 so it ends in a conversion, not a\\ncopy-paste. The [gift finder funnel](recipes/gift-finder-funnel.md)\\nrecipe is this funnel end to end.\\n","recipes/ask-about-this.md":"# \\"Ask about this\\" entry points (`sendMessage` only)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nZero-generation, instant, and often the biggest engagement win per line of\\ncode. Sprinkle context-aware buttons wherever a shopper hesitates:\\n\\n```js\\nconst gs = window.GreatStore;\\nif (gs) {\\n sizeGuideLink.addEventListener(\\"click\\", (e) => {\\n e.preventDefault();\\n gs.sendMessage(`How does the sizing run on \\"${productName}\\"? I usually wear a medium.`);\\n });\\n\\n shippingRow.querySelector(\\".ask\\").addEventListener(\\"click\\", () => {\\n gs.sendMessage(`What are the shipping options and times for \\"${productName}\\"?`);\\n });\\n\\n outOfStockBadge?.addEventListener(\\"click\\", () => {\\n gs.sendMessage(`\\"${productName}\\" looks out of stock \u2014 is there anything similar in stock?`);\\n });\\n}\\n```\\n\\nWrite each message as something the shopper would plausibly say \u2014 it appears\\nin the transcript as their message.\\n","recipes/campaign-hero.md":"# Campaign hero with deliberate variation\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nCache-friendly variation: key the prompt to a low-cardinality period, not to\\ntime itself.\\n\\n```js\\n// ISO week number \u2192 one generation per store per week, shared by everyone.\\nconst week = (d => {\\n const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\\n t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));\\n return `${t.getUTCFullYear()}-W${Math.ceil((((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5) + 1) / 7)}`;\\n})(new Date());\\n\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n headline: { type: \\"string\\", maxLength: 60 },\\n subline: { type: \\"string\\", maxLength: 120 },\\n featuredProductName: { type: \\"string\\", nullable: true },\\n ctaChatMessage: { type: \\"string\\", maxLength: 120 },\\n },\\n required: [\\"headline\\", \\"subline\\", \\"ctaChatMessage\\"],\\n },\\n `Variant ${week}. Write a homepage hero for this store: a headline and ` +\\n `subline spotlighting a real product or category that fits the current ` +\\n `season, plus ctaChatMessage \u2014 the first-person message a shopper ` +\\n `would send to start shopping for it.`\\n);\\n\\nheroHeadline.textContent = data.headline;\\nheroSubline.textContent = data.subline;\\nheroCta.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(data.ctaChatMessage)\\n);\\nhero.hidden = false;\\n```\\n","recipes/comparison-table.md":"# Collection-page comparison table\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```html\\n<section id=\\"gs-compare\\" hidden>\\n <h3>Quick comparison</h3>\\n <table><thead id=\\"gsc-head\\"></thead><tbody id=\\"gsc-body\\"></tbody></table>\\n</section>\\n\\n<script>\\n (async () => {\\n if (!window.GreatStore?.generateStructuredContent) return;\\n const collection = \\"winter jackets\\"; // \u2190 your collection name\\n try {\\n const data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n criteria: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: { type: \\"string\\", maxLength: 25 },\\n },\\n rows: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n bestFor: { type: \\"string\\", maxLength: 60 },\\n values: {\\n type: \\"array\\",\\n items: { type: \\"string\\", maxLength: 60 },\\n },\\n },\\n required: [\\"productName\\", \\"bestFor\\", \\"values\\"],\\n },\\n },\\n },\\n required: [\\"criteria\\", \\"rows\\"],\\n },\\n `The shopper is browsing the \\"${collection}\\" collection. Pick the ` +\\n `3-4 most representative products and compare them. Choose the ` +\\n `criteria a shopper actually decides on for this category. ` +\\n `\\"values\\" must align with \\"criteria\\" by index. Add a one-line ` +\\n `\\"bestFor\\" verdict per product. Only use real products and facts.`\\n );\\n\\n const head = document.getElementById(\\"gsc-head\\");\\n const hr = document.createElement(\\"tr\\");\\n for (const h of [\\"Product\\", ...data.criteria, \\"Best for\\"]) {\\n const th = document.createElement(\\"th\\");\\n th.textContent = h;\\n hr.append(th);\\n }\\n head.append(hr);\\n\\n const body = document.getElementById(\\"gsc-body\\");\\n for (const row of data.rows) {\\n const tr = document.createElement(\\"tr\\");\\n const cells = [row.productName, ...(row.values ?? []), row.bestFor];\\n for (let i = 0; i < data.criteria.length + 2; i++) {\\n const td = document.createElement(\\"td\\");\\n td.textContent = cells[i] ?? \\"\u2014\\";\\n tr.append(td);\\n }\\n body.append(tr);\\n }\\n document.getElementById(\\"gs-compare\\").hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nNote the index-aligned `values`/`criteria` trick and the `?? \\"\u2014\\"` guard \u2014\\ngrounding means a value the catalog can\'t support may be missing.\\n","recipes/complete-the-look.md":"# \\"Complete the look\\" cross-sell strip\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n intro: { type: \\"string\\", maxLength: 90 },\\n picks: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n reason: { type: \\"string\\", maxLength: 90 },\\n imageUrl: { type: \\"string\\", nullable: true },\\n productUrl: { type: \\"string\\", nullable: true },\\n },\\n required: [\\"productName\\", \\"reason\\"],\\n },\\n },\\n },\\n required: [\\"picks\\"],\\n },\\n `The shopper is viewing \\"${productName}\\". From the store\'s real catalog, ` +\\n `pick 2-4 products that genuinely pair with it and say why each one ` +\\n `completes the look or use-case. Include image and product URLs only ` +\\n `if known.`\\n);\\n\\nfor (const pick of data.picks) {\\n const card = document.createElement(\\"a\\");\\n if (pick.productUrl) card.href = pick.productUrl;\\n if (pick.imageUrl) {\\n const img = document.createElement(\\"img\\");\\n img.src = pick.imageUrl;\\n img.alt = pick.productName;\\n img.loading = \\"lazy\\";\\n card.append(img);\\n }\\n const name = document.createElement(\\"strong\\");\\n name.textContent = pick.productName;\\n const why = document.createElement(\\"p\\");\\n why.textContent = pick.reason;\\n card.append(name, why);\\n strip.append(card);\\n}\\nstrip.hidden = false;\\n```\\n\\n`imageUrl`/`productUrl` are `nullable` and optional in the render \u2014 the\\ngrounding contract means they\'re only present when the catalog actually has\\nthem. Never `require` URLs.\\n","recipes/custom-chat-button.md":"# Custom chat button synced to panel state\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nReplace the default launcher with your own UI using the lifecycle surface:\\n\\n```js\\nconst gs = window.GreatStore;\\nconst btn = document.getElementById(\\"my-chat-button\\");\\n\\ngs.ready.then(() => { btn.hidden = false; });\\nbtn.addEventListener(\\"click\\", () => gs.toggle());\\n\\ngs.on(\\"open\\", () => btn.setAttribute(\\"aria-expanded\\", \\"true\\"));\\ngs.on(\\"close\\", () => btn.setAttribute(\\"aria-expanded\\", \\"false\\"));\\n```\\n\\n`ready` resolves even when `.then()` is attached after mount, so script\\nordering doesn\'t matter. The `open`/`close` events also fire for opens the\\nSDK triggers itself (`sendMessage`, `?gs_chat=open`), keeping your button\\nstate honest.\\n","recipes/gift-finder-funnel.md":"# Gift finder funnel (teaser \u2192 conversation \u2192 action)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nThe flagship pattern: an AI-generated quiz card in your design, whose answers\\ndrop the shopper into a chat that can act on the page. Uses the\\n`onGreatStoreReady` helper from the\\n[embed API reference](../references/embed-api.md#availability) \u2014 define\\nthat once and it works for every use on the page.\\n\\n```html\\n<section id=\\"gift-finder\\" hidden>\\n <h3 id=\\"gf-question\\"></h3>\\n <div id=\\"gf-options\\"></div>\\n</section>\\n\\n<script>\\n // Depending on where this script tag sits relative to the embed script\\n // tag, this inline script could run either before embed.js has executed\\n // at all, or after it\'s already mounted \u2014 checking `window.GreatStore`\\n // synchronously and bailing if it\'s not there yet would silently skip\\n // the whole funnel in the first case, and a bare\\n // `addEventListener(\\"greatstore:ready\\", ...)` would silently miss the\\n // (one-shot, already-fired) event in the second.\\n // onGreatStoreReady (see the embed API reference) handles both.\\n onGreatStoreReady(async (gs) => {\\n // Tools the resulting conversation can use \u2014 discovered on its next\\n // turn automatically.\\n document.modelContext.registerTool({\\n name: \\"go_to_product\\",\\n description:\\n \\"Navigate the shopper to a product page on this site. Use when \\" +\\n \\"the shopper picks a product they want to see.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: { url: { type: \\"string\\" } },\\n required: [\\"url\\"],\\n },\\n execute({ url }) {\\n const u = new URL(String(url), location.origin);\\n if (u.origin !== location.origin) throw new Error(\\"Only same-site URLs allowed\\");\\n location.assign(u.href);\\n return { content: [{ type: \\"text\\", text: \\"Navigating.\\" }] };\\n },\\n });\\n\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 80 },\\n options: {\\n type: \\"array\\",\\n minItems: 3,\\n maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n label: { type: \\"string\\", maxLength: 30 },\\n chatMessage: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"label\\", \\"chatMessage\\"],\\n },\\n },\\n },\\n required: [\\"question\\", \\"options\\"],\\n },\\n \\"Create one engaging gift-finder opening question for this store, \\" +\\n \\"with 3-4 answer options that map to real areas of the catalog. \\" +\\n \\"For each option also write chatMessage: the message a shopper \\" +\\n \\"would send to a shopping assistant after picking it, phrased in \\" +\\n \\"first person (e.g. \\\\\\"I\'m shopping for my dad who loves hiking\\\\\\").\\"\\n );\\n\\n document.getElementById(\\"gf-question\\").textContent = data.question;\\n const wrap = document.getElementById(\\"gf-options\\");\\n for (const opt of data.options) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = opt.label;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(opt.chatMessage));\\n wrap.append(btn);\\n }\\n document.getElementById(\\"gift-finder\\").hidden = false;\\n } catch {}\\n });\\n</script>\\n```\\n\\nWhy it works: the teaser costs one cached generation per page, each\\nclick opens a conversation that already has direction, and `go_to_product`\\nlets the conversation end on a product page instead of in a dead end.\\n","recipes/launchers.md":"# GreatStore launchers \u2014 AI question chips\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nA horizontally scrollable row of chips, each a highly engaging first-person\\nquestion about the current page. Tapping a chip sends that question to the\\nassistant \u2014 `generateStructuredContent` writes the questions, `sendMessage`\\nfires them. Simple yet effective: it works on every page type, costs one\\ncached generation per page, and every tap starts a conversation that already\\nhas a great opening line.\\n\\n```html\\n<div id=\\"gs-launchers\\" hidden></div>\\n\\n<style>\\n #gs-launchers {\\n display: flex;\\n gap: 0.5em;\\n overflow-x: auto;\\n -webkit-overflow-scrolling: touch;\\n scrollbar-width: none;\\n padding: 0.5em 1em;\\n }\\n #gs-launchers::-webkit-scrollbar { display: none; }\\n #gs-launchers button {\\n flex: 0 0 auto;\\n white-space: nowrap;\\n border: 1px solid #ddd;\\n border-radius: 999px;\\n padding: 0.5em 0.9em;\\n background: #fff;\\n cursor: pointer;\\n }\\n</style>\\n\\n<script>\\n (async () => {\\n const gs = window.GreatStore;\\n if (!gs?.generateStructuredContent) return;\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n launchers: {\\n type: \\"array\\", minItems: 4, maxItems: 6,\\n items: {\\n type: \\"object\\",\\n properties: {\\n chip: { type: \\"string\\", maxLength: 32 },\\n question: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"chip\\", \\"question\\"],\\n },\\n },\\n },\\n required: [\\"launchers\\"],\\n },\\n `The shopper is on the page \\"${document.title}\\". Write 4-6 launcher ` +\\n `chips for a shopping assistant. For each, \\"question\\" is a highly ` +\\n `engaging first-person question this shopper would genuinely want ` +\\n `answered on this page \u2014 specific to its product, category, or ` +\\n `content, never generic \u2014 and \\"chip\\" is a 2-4 word teaser of it. ` +\\n `Vary the angles: fit and use, comparisons, gifting, care, what\'s ` +\\n `popular.`\\n );\\n\\n const row = document.getElementById(\\"gs-launchers\\");\\n for (const { chip, question } of data.launchers) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = chip;\\n btn.title = question;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(question));\\n row.append(btn);\\n }\\n row.hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nWhy it works:\\n\\n- **The chip is the teaser, the question is the payload.** A 2-4 word chip\\n scans instantly; the full first-person question lands in the transcript\\n reading like something the shopper typed, and gives the assistant a\\n well-formed prompt. The `title` attribute previews the full question on\\n hover.\\n- **Per-page for free.** The page URL is part of the generation context and\\n the cache key, so one site-wide snippet yields different chips on every\\n page \u2014 each served from cache on repeat renders.\\n- **Placement is the lever.** Under the product title, above the grid on\\n collections, at the end of a blog post \u2014 wherever a shopper pauses to\\n wonder, the chips name the question for them.\\n\\nTips:\\n\\n- Inline the product or collection name into the prompt when the platform\\n exposes it \u2014 it beats relying on `document.title`.\\n- Restyle the chips to the site\'s design system; the CSS above is just the\\n scroll mechanics (flex row, `overflow-x: auto`, hidden scrollbars,\\n `white-space: nowrap`).\\n- Resist adding more than ~6 chips \u2014 a launcher row is an invitation, not a\\n sitemap.\\n","recipes/page-action-suite.md":"# Page-action suite (WebMCP)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nGive every conversation on the site real capabilities. Register once in a\\nshared snippet, after the SDK is ready (which guarantees\\n`document.modelContext` exists) \u2014 using the `onGreatStoreReady` helper from\\nthe [embed API reference](../references/embed-api.md#availability), which\\nhandles both load orderings (embed script not run yet vs. already mounted)\\nthat a plain `window.GreatStore?.ready.then()` or a bare\\n`addEventListener(\\"greatstore:ready\\", ...)` each only cover one side of:\\n\\n```js\\nonGreatStoreReady(() => {\\n const text = (value) => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(value) }],\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_cart\\",\\n description:\\n \\"Read the shopper\'s current cart on this site: items, quantities, \\" +\\n \\"and totals. Use before answering any cart question.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n async execute() {\\n return text(await (await fetch(\\"/cart.js\\")).json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the cart on this site. Use when the \\" +\\n \\"shopper asks to add or buy something. Confirm the variant with \\" +\\n \\"the shopper first if ambiguous.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1, maximum: 10 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n if (!variantId) throw new Error(\\"variantId is required\\");\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Could not add to cart (${res.status})`);\\n document.dispatchEvent(new CustomEvent(\\"cart:refresh\\"));\\n return text(await res.json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_current_page\\",\\n description:\\n \\"Read what page the shopper is currently on, including structured \\" +\\n \\"product data when on a product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute() {\\n return text({\\n url: location.href,\\n title: document.title,\\n productJson: document.querySelector(\\"#product-json\\")?.textContent ?? null,\\n });\\n },\\n });\\n});\\n```\\n\\nPrinciples at work: throw on failure (the assistant explains and recovers),\\nreturn fresh state after mutations (the assistant confirms accurately), cap\\nquantities in the schema, and notify your own UI (`cart:refresh`) so the\\npage reflects what the AI did.\\n\\nFor a product-page-only tool, register with an `AbortSignal` and abort on\\nSPA navigation:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(reviewsTool, { signal: ac.signal });\\nrouter.onLeave(\\"/products/:handle\\", () => ac.abort());\\n```\\n","recipes/product-faq.md":"# Product FAQ accordion\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n faqs: {\\n type: \\"array\\", minItems: 3, maxItems: 5,\\n items: {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 90 },\\n answer: { type: \\"string\\", maxLength: 300 },\\n },\\n required: [\\"question\\", \\"answer\\"],\\n },\\n },\\n },\\n required: [\\"faqs\\"],\\n },\\n `Generate the questions shoppers most plausibly have before buying the ` +\\n `product \\"${productName}\\", with accurate answers grounded in the real ` +\\n `product details and store policies. Skip any question the store data ` +\\n `can\'t answer confidently.`\\n);\\n\\nconst wrap = document.getElementById(\\"gs-faq\\");\\nfor (const { question, answer } of data.faqs) {\\n const details = document.createElement(\\"details\\");\\n const summary = document.createElement(\\"summary\\");\\n summary.textContent = question;\\n const p = document.createElement(\\"p\\");\\n p.textContent = answer;\\n details.append(summary, p);\\n wrap.append(details);\\n}\\nwrap.hidden = false;\\n```\\n\\nEngagement bonus \u2014 append a hand-off row so unanswered questions become\\nconversations:\\n\\n```js\\nconst ask = document.createElement(\\"button\\");\\nask.type = \\"button\\";\\nask.textContent = \\"Have a different question? Ask us\\";\\nask.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(`I have a question about \\"${productName}\\".`)\\n);\\nwrap.append(ask);\\n```\\n","references/agents-and-mcp.md":"# AI agents and the store\'s MCP endpoint\\n\\nEvery GreatStore store publishes a standard MCP server card, and the\\nplatform runs one MCP endpoint for coding agents \u2014 no authentication\\nrequired for either:\\n\\n| URL | What it is |\\n|---|---|\\n| `https://<slug>.greatstore.ai/.well-known/mcp/server-card.json` | Standard MCP server card \u2014 machine-readable discovery document for the store. |\\n| `https://admin.greatstore.ai/mcp` | A **documentation server for coding agents**: its tools return usage docs for the `gs` CLI. Same URL for every store. |\\n\\n## CLI docs for coding agents \u2014 `/mcp`\\n\\nA stateless HTTP MCP whose tools hand back markdown documentation for `gs`\\nCLI commands.\\n\\n```\\nclaude mcp add --transport http greatstore-admin https://admin.greatstore.ai/mcp\\n```\\n\\nUseful when a coding agent is shipping chat components\\n([chat-components.md](chat-components.md)) and needs the exact command for\\nthe next step. If this skill is installed, the agent already has the\\nworkflow \u2014 the MCP is the self-serve alternative for agents that don\'t.\\n\\n## What to use when\\n\\n- **Building the merchant\'s site** \u2192 this skill\'s other references (the SDK,\\n structured content, page tools).\\n- **A coding agent shipping chat components** \u2192 the `gs` CLI, with `/mcp`\\n as its built-in documentation.\\n- **Reading or changing store settings** (origin allowlists, CSP hosts, MCP\\n connectors, brand config) \u2192 the `gs` CLI\'s admin commands \u2014\\n [store-admin.md](store-admin.md), including which changes are safe to make\\n without asking the merchant.\\n","references/chat-components.md":"# Custom chat components \u2014 authoring with the `gs` CLI\\n\\nRemote components are React components the assistant renders **inside the\\nconversation** \u2014 product configurators, quizzes, size guides, booking forms,\\nanything richer than text. Each component is an AI-callable tool: the\\nmanifest\'s `description` tells the assistant *when* to show it, its\\n`inputSchema` declares the props the assistant fills in, and a `displayMode`\\npicks where it appears.\\n\\nAuthoring requires store-owner access (`gs login` signs in with the store\\nowner\'s account).\\n\\n## Workflow\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # browser sign-in\\ngs apps init --store my-store # scaffold a project root\\ncd <project> && npm install\\ngs apps init size_guide # scaffold components/size_guide/\\n# \u2026 edit components/size_guide/{component.tsx,manifest.json} \u2026\\ngs apps build # bundle every component\\ngs apps push # upload changed components as drafts\\ngs apps publish size_guide # promote to live\\n```\\n\\n`gs apps list` shows what\'s deployed (with dashboard links); `gs apps pull`\\nround-trips remote components back to disk. `gs apps push` hashes components\\nand only uploads what changed. (The subcommands also work at the top level \u2014\\n`gs push` == `gs apps push`.)\\n\\nThe scaffold writes an `AGENTS.md` into the project (with `CLAUDE.md` /\\n`GEMINI.md` symlinked) containing the complete design rules and brand\\nvariable table \u2014 your coding agent picks it up automatically when working in\\nthe project. `https://admin.greatstore.ai/mcp` serves the same CLI docs to\\nagents over MCP (see [agents-and-mcp.md](agents-and-mcp.md)).\\n\\n## `manifest.json`\\n\\n```json\\n{\\n \\"name\\": \\"size_guide\\",\\n \\"displayName\\": \\"Size guide\\",\\n \\"description\\": \\"Interactive size guide. Show when the shopper asks about sizing or fit for apparel.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {\\n \\"productName\\": { \\"type\\": \\"string\\" },\\n \\"category\\": { \\"type\\": \\"string\\" }\\n },\\n \\"required\\": [\\"productName\\"]\\n }\\n}\\n```\\n\\n| Field | Meaning |\\n|---|---|\\n| `name` | Tool name, snake_case (`^[a-z][a-z0-9_]*$`), matches the folder under `components/`. |\\n| `displayName` | Friendly label shown in chat UI. |\\n| `description` | **Load-bearing** \u2014 how the assistant decides when to render the component. Say what it shows *and* when to use it, like any good tool description. |\\n| `displayMode` | Where it renders \u2014 see below. |\\n| `inputSchema` | JSON Schema for the props the assistant fills. Keep it tight; required fields the AI can\'t infer cause bad calls. |\\n| `async` | Set `true` for backend-backed components (see Async below). |\\n\\n### Display modes\\n\\n- `inline` \u2014 a bubble inside the chat transcript; persists with the message\\n log.\\n- `over-input` \u2014 floats above the chat input (like a question overlay);\\n cleared by the next user turn or an explicit close.\\n- `fullscreen` \u2014 takes over the full preview surface; persists until the\\n next widget-emitting tool call or an explicit close.\\n\\n## The component contract\\n\\n`component.tsx` default-exports a React component. Its props are the\\n`inputSchema` fields the assistant filled, plus six GreatStore-injected\\nlifecycle props (always present):\\n\\n| Prop | What it does |\\n|---|---|\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it \u2014 lets the component drive the conversation (\\"Selected size M, what\'s the return policy?\\"). |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject **invisible** background context for the model \u2014 the variant the shopper selected, the options they configured, the step they\'re on. Replaces the prior value (never appends); pass `\\"\\"` to clear. Read on the next chat turn. Use this instead of `onSendMessage` when the assistant should *know* state without a message appearing in the transcript. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay you can\'t render yourself (your component is boxed inside its own bounds and shadow root). Pass the image `src`; for a smooth zoom, also pass the clicked element\'s `getBoundingClientRect()` as `originRect` (omit it and the image grows from the viewport centre). |\\n| `onGenerateStructuredContent(schema, prompt, fallback)` | Ask the store\'s assistant for content matching a JSON Schema (or Zod schema) and get back the generated object. Personalized to the shopper. Works the same in the conversation on the storefront and every embed \u2014 no `window.GreatStore` needed. Best for async components that build their render from a generated payload. `fallback` is **required**: a schema-shaped object you provide that the component preview renders (no live store there), validated against the schema \u2014 a mismatch throws. |\\n| `onClose()` | Dismiss the host slot. `over-input` clears the overlay, `fullscreen` reverts the pane, `inline` is a no-op. |\\n\\nReach for `onUpdateModelContext` when the shopper changes something inside\\nthe component (picks a size, configures a build, advances a quiz) and you\\nwant the assistant to factor it into the *next* thing they ask \u2014 without\\nspamming the chat with a visible \\"I selected M\\" message. Use `onSendMessage`\\nwhen you actually want a turn to happen now.\\n\\nReach for `onShowLightbox` whenever your component shows imagery the shopper\\nmight want to inspect closely \u2014 product photos, swatches, a size chart. A\\nthumbnail\'s `onClick` handler is the natural place to call it. Don\'t build\\nyour own full-screen modal: a remote component is sandboxed inside its own\\nbounds and shadow root, so a self-rendered overlay can\'t cover the chat. The\\nshared lightbox escapes those bounds and themes itself from the store\'s CSS\\nvariables.\\n\\n```tsx\\nimport React from \\"react\\";\\n\\ninterface Props {\\n productName: string;\\n category?: string;\\n onSendMessage: (text: string) => void;\\n onCallTool: (name: string, args: Record<string, unknown>) => void;\\n onUpdateModelContext: (context: string) => void;\\n onShowLightbox: (options: { src: string; originRect?: DOMRect }) => void;\\n onGenerateStructuredContent: <T = unknown>(\\n schema: object,\\n prompt: string,\\n fallback: T,\\n ) => Promise<T>;\\n onClose: () => void;\\n}\\n\\nexport default function SizeGuide({\\n productName,\\n onSendMessage,\\n onUpdateModelContext,\\n onShowLightbox,\\n}: Props) {\\n return (\\n <div\\n style={{\\n padding: \\"1em\\",\\n border: \\"1px solid var(--color-border-default)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n background: \\"var(--color-surface)\\",\\n color: \\"var(--color-foreground)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n >\\n {/* \u2026 sizes for {productName} \u2026 */}\\n <img\\n src={`/size-charts/${productName}.png`}\\n alt={`${productName} size chart`}\\n style={{ cursor: \\"zoom-in\\", width: \\"100%\\" }}\\n onClick={(e) =>\\n onShowLightbox({\\n src: `/size-charts/${productName}.png`,\\n originRect: e.currentTarget.getBoundingClientRect(),\\n })\\n }\\n />\\n <button\\n onClick={() =>\\n // Silent: the assistant now knows the pick for the shopper\'s\\n // next question, with nothing added to the transcript.\\n onUpdateModelContext(`Shopper selected size M of \\"${productName}\\".`)\\n }\\n >\\n Select size M\\n </button>\\n <button onClick={() => onSendMessage(`Size M of \\"${productName}\\" \u2014 is it in stock?`)}>\\n Ask about size M\\n </button>\\n </div>\\n );\\n}\\n```\\n\\n## Design rules (non-negotiable)\\n\\nComponents render inside arbitrary publisher pages *and* the GreatStore\\nstorefront; you control neither the host\'s root font size nor its colors.\\n\\n1. **Size in `em`, never `rem`** \u2014 `rem` resolves against the host page\'s\\n root font size, which is arbitrary (`html { font-size: 8px }` breaks every\\n `rem` dimension). `em` stays self-consistent anywhere. Borders may stay\\n `px`.\\n2. **Never hardcode colors, fonts, or radii** \u2014 read the brand CSS variables\\n GreatStore injects (`--color-primary`, `--color-surface`,\\n `--color-foreground`, `--color-border-default`, `--font-primary`,\\n `--radius-lg`, \u2026) so the component restyles itself with the store\'s\\n theme. The scaffolded `AGENTS.md` has the full variable table.\\n\\n## Async components (backend-backed data)\\n\\nIf a component must load data before it can render correctly, don\'t render a\\nshell and fetch in `useEffect` \u2014 set `\\"async\\": true` in the manifest and\\nexport an **async** default. GreatStore shows its own loading state, awaits\\nyour promise, and renders what it resolves to.\\n\\nA thrown error is a **retry signal**: the assistant sees it and usually\\nre-calls the tool. So only throw when a *different* call could help:\\n\\n1. Validate the AI-passed props first and throw on bad input \u2014 the AI can\\n fix the args and retry. (Don\'t validate the API\'s *output* and throw: the\\n AI can\'t fix your backend, it\'ll just loop.)\\n2. Throw on failures where retrying differently could succeed, and say what\\n to change (e.g. empty search \u2192 `\\"no results for X \u2014 try a broader keyword\\"`).\\n3. For idempotent failures (500, timeout, missing record) render a graceful\\n fallback instead of throwing \u2014 re-running the same call changes nothing.\\n\\n```tsx\\nexport default async function Results(props: Props) {\\n if (!props.query?.trim()) throw new Error(\\"missing required prop: query\\");\\n const res = await fetch(`/api/search?q=${encodeURIComponent(props.query)}`);\\n if (res.ok) {\\n const { results } = await res.json();\\n if (results.length === 0)\\n throw new Error(`no results for \\"${props.query}\\" \u2014 try a broader keyword`);\\n return <ul>{/* render results */}</ul>;\\n }\\n return <p>Couldn\'t load results right now.</p>; // idempotent: don\'t throw\\n}\\n```\\n\\n## When to build a component vs. the other facets\\n\\n- Content for the **merchant\'s page** \u2192 `window.GreatStore.generateStructuredContent`\\n ([structured-content.md](structured-content.md)). Inside a chat component, use\\n the injected `onGenerateStructuredContent` prop instead \u2014 same idea, but\\n personalized to the shopper and available on every surface without the global.\\n- Letting the assistant **act on the page** \u2192 WebMCP page tools\\n ([embed-api.md](embed-api.md)).\\n- Rich, interactive UI **inside the conversation itself**, available on the\\n storefront and every embed without page changes \u2192 a chat component.\\n","references/embed-api.md":"# `window.GreatStore` API reference\\n\\n## Setup\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nOne script tag, anywhere on the page (end of `<body>` preferred), with the\\nstore\'s slug in the host. The `window.GreatStore` object exists synchronously\\nonce the script executes; every method below is safe to call before the chat\\nUI has finished loading \u2014 pre-mount calls are queued and replayed in order\\nonce it mounts. The SDK pre-warms its chat bundle in the background\\nautomatically; the panel stays closed until `open()` / `toggle()` /\\n`sendMessage()` is called or the shopper clicks the launcher.\\n\\nRequirements:\\n\\n- The page\'s domain must be in the store\'s **allowed domains** (GreatStore\\n store settings) \u2014 see Troubleshooting below for the failure signature.\\n- If the store also wants push notifications, host `gs.js` at the site root\\n and load that instead of `embed.js` \u2014 it injects the embed for you. See\\n [push-notifications.md](push-notifications.md).\\n\\nCode samples throughout are framework-free reference implementations \u2014\\nre-express them in the host repo\'s framework (React, Vue, Shopify Liquid,\\n\u2026) rather than retrofitting them as-is. **Using React?** `@greatstore/react`\\nwraps all of this in a `<GreatStore>` component with typed props (no\\n`data-*` attribute quoting) and clean mount/unmount for SPA routing \u2014\\n`npm install @greatstore/react`.\\n\\n## Appearance overrides\\n\\nThe store\'s configured appearance (theme colors, fonts, roundedness, panel\\nposition, mobile bar, AI disclaimer) is the default everywhere. To make the\\nembed look or behave differently on a specific page \u2014 e.g. matching a\\ncampaign landing page\'s palette \u2014 set `data-*` attributes on the embed\\nscript tag. Every attribute is optional; anything you don\'t set falls back\\nto the store\'s configured value.\\n\\n```html\\n<script\\n src=\\"https://my-store.greatstore.ai/embed.js\\"\\n data-theme-mode=\\"dark\\"\\n data-theme-radius=\\"rounded\\"\\n data-theme-panel-position=\\"right\\"\\n data-theme-mobile-bar=\\"false\\"\\n data-z-index=\\"9999\\"\\n data-theme-brand-color=\\"#1a1a2e\\"\\n data-theme-surface-color=\\"#ffffff\\"\\n data-theme-text-color=\\"#111111\\"\\n data-theme-font=\\"Inter, sans-serif\\"\\n data-ai-disclaimer=\\"false\\"\\n></script>\\n```\\n\\n| Attribute | Values | Overrides |\\n|---|---|---|\\n| `data-theme-mode` | `auto` \\\\| `light` \\\\| `dark` \\\\| `custom` | Color scheme. |\\n| `data-theme-radius` | `sharp` \\\\| `default` \\\\| `rounded` | Corner roundedness. |\\n| `data-theme-panel-position` | `left` \\\\| `right` \\\\| `middle` | Desktop panel placement. |\\n| `data-theme-mobile-bar` | `true` \\\\| `false` | Whether the collapsed mobile bar shows. |\\n| `data-z-index` | integer, `0`\u2013`2147483647` | Stacking order of the chat overlay on your page. Lower it if something on the page must stay above the chat. |\\n| `data-theme-font` | CSS font-family string | Primary font. |\\n| `data-theme-font-secondary` | CSS font-family string | Secondary font. |\\n| `data-theme-brand-color` | any CSS color | Brand/primary color (only used in `custom` mode). |\\n| `data-theme-surface-color` | any CSS color | Surface/background color (only used in `custom` mode). |\\n| `data-theme-text-color` | any CSS color | Text color (only used in `custom` mode). |\\n| `data-ai-disclaimer` | `true` \\\\| `false` | Shows or hides the AI disclaimer line. |\\n| `data-ai-disclaimer-text` | string, up to 200 chars | Custom disclaimer message (implies shown, unless `data-ai-disclaimer=\\"false\\"` is also set). |\\n\\nNot overridable this way: display name, assistant name, logo, and icons \u2014\\nthose stay whatever\'s configured in the store\'s admin.\\n\\nInvalid values (unrecognized enum, malformed color, disallowed font\\ncharacters) are silently ignored and fall back to the configured default.\\n\\n## Properties\\n\\n| Property | Type | Description |\\n|---|---|---|\\n| `slug` | `string` | The store identifier the script was loaded for. |\\n| `host` | `string` | `\\"greatstore.ai\\"`. |\\n| `embedHost` | `string` | Origin the embed assets load from, e.g. `https://<slug>.greatstore.ai`. |\\n| `ready` | `Promise<void>` | Resolves when the chat UI has mounted and `open()` would render instantly. Resolved promises replay, so `.then()` works no matter when it\'s attached. The readiness signal to gate on. |\\n\\nA `greatstore:ready` `CustomEvent` (with the SDK object as `detail`) is also\\ndispatched on `window` at the moment `ready` resolves, for declarative\\ntooling. Unlike the promise, the listener must be attached before mount\\ncompletes \u2014 attach it before (or immediately after) the embed script tag.\\n\\n## Methods\\n\\n### `load(): void`\\n\\nPre-warms the chat bundle and identity in the background without opening the\\npanel. Called automatically when `embed.js` runs, so you rarely need it.\\nIdempotent.\\n\\n### `open(): void` / `close(): void` / `toggle(): void`\\n\\nOpen, close, or toggle the chat panel. On desktop the panel is a floating\\nside panel; under 768px viewport width it\'s a full-height drawer. All three\\nqueue if called before mount.\\n\\n### `sendMessage(text: string): void`\\n\\nSends `text` as the shopper\'s own visible chat message and **opens the panel\\nif it\'s closed**. The text is trimmed; empty or whitespace-only strings are\\nsilently dropped. Queues if called before mount.\\n\\nThis is the highest-leverage one-liner in the SDK: any element on the page\\ncan become a conversation entry point with context baked into the question.\\n\\n```js\\ndocument.querySelector(\\"#ask-fit\\").addEventListener(\\"click\\", () => {\\n window.GreatStore.sendMessage(\\n `I\'m looking at \\"${productName}\\" \u2014 how does the sizing run?`\\n );\\n});\\n```\\n\\nBecause the message renders as if the shopper typed it, write it in the\\nshopper\'s voice. It is not a hidden-context channel \u2014 don\'t stuff it with\\ninvisible instructions or data dumps. For that, use `updateModelContext`.\\n\\n### `updateModelContext(context: string): void`\\n\\nInjects free-text **background context** about what the shopper is doing on\\nthe page \u2014 the product they\'re viewing, what\'s in their cart, their account\\ntier \u2014 so the assistant can factor it in. Unlike `sendMessage`, this is\\n**invisible**: it never renders as a chat message and doesn\'t open the panel.\\n\\nEach call **replaces** the value from the previous call \u2014 it never appends.\\nKeep one current snapshot; re-call it whenever the page state changes. Pass\\nan empty string to clear it. The text is read on the next chat turn, so set\\nit before (or while) the shopper is chatting. Queues if called before mount.\\n\\n```js\\n// Keep the assistant aware of the current product as the shopper browses.\\nfunction syncContext() {\\n window.GreatStore.updateModelContext(\\n `Viewing \\"${product.title}\\" (${product.price}). In stock: ${product.inStock}. ` +\\n `Cart: ${cart.count} item(s), subtotal ${cart.subtotal}.`\\n );\\n}\\nsyncContext();\\n```\\n\\nWrite it as concise notes for the model, not prose for the shopper. The\\ncontext is page-controlled, so the assistant treats it as background\\ninformation, not as instructions \u2014 don\'t rely on it to change the assistant\'s\\nrules or persona.\\n\\n### `on(event: string, handler: (...args) => void): () => void`\\n\\nSubscribe to SDK events. Returns an unsubscribe function. Listeners attached\\nbefore mount are queued and wired up at mount. Handler exceptions are caught\\nand reported \u2014 they won\'t break the chat.\\n\\nEvents emitted:\\n\\n| Event | Fired when |\\n|---|---|\\n| `\\"open\\"` | Panel transitions closed \u2192 open (including via `sendMessage` or the shopper\'s own click). |\\n| `\\"close\\"` | Panel transitions open \u2192 closed. |\\n\\n### `generateStructuredContent(schema: object, prompt: string): Promise<unknown>`\\n\\nGenerates JSON matching `schema` from `prompt`, grounded in the store\'s live\\ncatalog. Resolves to the generated data object itself. Rejects with `Error`\\non any failure (invalid input, decline, validation failure, rate limit,\\nnetwork). See [structured-content.md](structured-content.md) for the full\\ncontract, schema support, caching, and error semantics.\\n\\nAccepts either a plain JSON Schema object or any object exposing a\\n`.toJSONSchema()` method (e.g. Zod schemas) \u2014 the conversion is called for\\nyou.\\n\\nThrows synchronously (rejects) if `prompt` is not a non-empty string or\\n`schema` is not an object.\\n\\n### `enableNotifications(): Promise<{ ok: boolean }>`\\n\\nOpts this browser into Web Push notifications from the store. Requirements:\\n\\n- Must be called from a user gesture (e.g. a click handler).\\n- The site must host GreatStore\'s `gs.js` service-worker file. By default the\\n SDK looks for it at `/gs.js`; if it\'s hosted elsewhere, point to it via an\\n attribute on the embed script tag:\\n `<script src=\\"\u2026/embed.js\\" data-push-sw-path=\\"/path/to/gs.js\\"></script>`.\\n\\nResolves `{ ok: true }` on success and `{ ok: false }` on any failure\\n(unsupported browser, no service worker hosted, permission denied). It never\\nrejects.\\n\\n### `destroy(): void`\\n\\nFully tears down a mounted panel: unmounts the chat UI, closes any open voice\\nconnection, and removes the embed\'s DOM/listeners from the page. Use this\\nwhen your page is done with the embed for good \u2014 e.g. a single-page app\\nnavigating away from the only route that should show it.\\n\\nNo-ops (with a console warning) if nothing is mounted. The cached identity\\nand downloaded chat bundle are kept, so a later `load()` (or any call that\\ntriggers a mount, like `open()`) mounts a fresh panel without a network\\nround-trip for either. `ready` becomes a new pending promise at the moment\\n`destroy()` is called, resolving again once the next mount completes:\\n\\n```js\\nwindow.GreatStore.destroy();\\n// ...later, on the page/route where the embed should come back:\\nwindow.GreatStore.load();\\nawait window.GreatStore.ready; // resolves once the fresh mount is done\\n```\\n\\n## Page tools \u2014 WebMCP (`document.modelContext`)\\n\\nThe recommended way to expose page capabilities to the assistant is the\\nWebMCP standard. The GreatStore assistant discovers every tool registered on\\n`document.modelContext`, re-reading the list on each conversational turn \u2014\\nso tools registered mid-session appear on the next message without a reload.\\n\\n### Availability\\n\\nIf the browser implements WebMCP natively, `document.modelContext` is just\\nthere. Otherwise the SDK installs a minimal fallback the instant `embed.js`\\nstarts running \u2014 synchronously, no async gap \u2014 so `document.modelContext`\\nis normally available immediately. The embed script tag deliberately has no\\n`async`/`defer` \u2014 a script with neither blocks parsing and runs at its own\\nposition, which matters if the page also has another script (a nav widget,\\nan analytics tag) registering its own WebMCP tools: whichever executes\\nfirst wins that registration, and only a synchronous, unconditionally-first\\nscript gives GreatStore\'s fallback shim a real chance of installing before\\none of those calls happens. That said, your own code can still end up on\\neither side of two different races relative to `embed.js`, depending on\\nwhere your script tag sits and whether it uses `async`/`defer` itself:\\n\\n- **Your script runs before `embed.js` has executed at all** \u2014 `window.GreatStore`\\n doesn\'t exist yet. `window.GreatStore?.ready` silently evaluates to\\n `undefined` here (optional chaining swallows it), so accessing `.then()`\\n on it throws or (written more defensively) just does nothing.\\n- **Your script runs after `embed.js` has already mounted** \u2014 the\\n `greatstore:ready` event already fired once, in the past.\\n `window.addEventListener(\\"greatstore:ready\\", ...)` attached now will\\n never see it: the event isn\'t replayed for late listeners, unlike a\\n resolved Promise (`.then()` on an already-resolved Promise still fires).\\n\\nNeither `.ready.then(...)` alone nor `addEventListener(\\"greatstore:ready\\", ...)`\\nalone is safe against both orderings. Use both, picking whichever is valid\\nat the moment your code runs:\\n\\n```js\\nfunction onGreatStoreReady(callback) {\\n if (window.GreatStore?.ready) {\\n // embed.js has already run \u2014 .then() on its ready Promise fires\\n // immediately if it already resolved, or once it does.\\n window.GreatStore.ready.then(() => callback(window.GreatStore));\\n } else {\\n // embed.js hasn\'t run yet \u2014 wait for the one-shot event it\'ll dispatch\\n // once it has. Safe to attach now: nothing can fire it between this\\n // check and the listener attaching, since JS execution isn\'t preemptible.\\n window.addEventListener(\\n \\"greatstore:ready\\",\\n (event) => callback(event.detail),\\n { once: true },\\n );\\n }\\n}\\n\\nonGreatStoreReady(() => {\\n document.modelContext.registerTool(/* \u2026 */);\\n});\\n```\\n\\n(`navigator.modelContext` is a deprecated alias for the same object; use\\n`document.modelContext` in new code.)\\n\\n### `registerTool(tool, options?)`\\n\\n```ts\\ndocument.modelContext.registerTool(\\n {\\n name: string, // required, non-empty, unique on the page\\n description: string, // required \u2014 how the AI decides when to call it\\n inputSchema?: object, // JSON Schema for execute\'s args;\\n // defaults to { type: \\"object\\", properties: {} }\\n execute(args): Result | Promise<Result>,\\n },\\n options?: { signal?: AbortSignal }, // abort to unregister\\n);\\n```\\n\\n- **Result shape**: `execute` returns MCP content blocks \u2014\\n `{ content: [{ type: \\"text\\", text: \\"\u2026\\" }] }`. For structured data,\\n `JSON.stringify` it into `text`. Add `isError: true` to mark a handled\\n failure.\\n- **Errors**: a thrown error or rejected promise is delivered to the\\n assistant as a *failed* tool call carrying the error message \u2014 the\\n assistant can explain or adapt. Errors never escape into your page.\\n- **Duplicate names throw.** To replace a tool, abort its registration first.\\n- **Unregistration is `AbortSignal`-driven**: pass `{ signal }` and call\\n `abort()` when the tool\'s context goes away (SPA navigation, modal close).\\n A pre-aborted signal skips registration. (A legacy\\n `unregisterTool(name)` exists but is deprecated in the spec.)\\n- **Treat `args` as untrusted input**: values are AI-generated. Validate\\n before passing to your own APIs, and never `eval` anything from them.\\n\\nA complete tool, registered once GreatStore is ready (using the\\n`onGreatStoreReady` helper defined above):\\n\\n```js\\nonGreatStoreReady(() => {\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the shopper\'s cart on this site. \\" +\\n \\"Use when the shopper asks to add, buy, or get a product.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Cart add failed (${res.status})`);\\n const cart = await res.json();\\n return { content: [{ type: \\"text\\", text: JSON.stringify(cart) }] };\\n },\\n });\\n});\\n```\\n\\nReturning the fresh cart state after the mutation lets the assistant confirm\\naccurately. Good tool families: cart (`get_cart`, `add_to_cart`), navigation\\n(`go_to_page`), page state (`get_current_product`, `apply_filters`), UI\\n(`highlight_section`, `scroll_to_reviews`).\\n\\nAnd a context-scoped tool, unregistered via `AbortSignal`:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(\\n {\\n name: \\"get_product_reviews\\",\\n description: \\"Read the reviews shown on the current product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute: () => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(collectReviews()) }],\\n }),\\n },\\n { signal: ac.signal },\\n);\\n\\n// On SPA route change away from the product page:\\nac.abort();\\n```\\n\\n## URL parameter: `?gs_chat=open`\\n\\nWhen the page URL carries `gs_chat=open`, the panel opens automatically once\\nthe embed mounts. The param is consumed and stripped from the URL via\\n`history.replaceState`, so a manual reload doesn\'t re-open the panel. Use it\\nin campaign links, emails, and post-login redirects.\\n\\n## Troubleshooting\\n\\n| Symptom | Likely cause |\\n|---|---|\\n| Console: `[GreatStore] Chat is unavailable on <origin>\u2026 this domain isn\'t in the store\'s allowed domains` | The page\'s origin isn\'t in the store\'s allowed domains. Add it in store settings. Until then every SDK network call fails. |\\n| Console: `[GreatStore] Embed script must be loaded from <slug>.greatstore.ai/embed.js` | The script was copied/self-hosted instead of loaded from the store\'s embed URL. Always load it from `https://<slug>.greatstore.ai/embed.js`. |\\n| `generateStructuredContent` rejects with a rate-limit message | More than ~20 requests/minute from one visitor. Consolidate calls into fewer, richer schemas. |\\n| Panel won\'t auto-open on mobile after returning to the page | Intentional: the mobile drawer never auto-opens on resume \u2014 it would cover the content the shopper is reading. The transcript is preserved; they\'ll see it when they tap the launcher. |\\n| Tools registered but the assistant doesn\'t use them | Check the `description` \u2014 it\'s the only signal for *when* to call. Also confirm the registration ran (`document.modelContext` exists after `ready`) on the same page the conversation is on. |\\n","references/push-notifications.md":"# Web push re-engagement\\n\\nShoppers who opt in receive browser notifications from the store \u2014 under the\\nmerchant\'s own domain and branding, with the permission prompt shown inline\\non the merchant\'s page. Setup is two pieces: a single file hosted at the site\\nroot, and an opt-in button.\\n\\n## 1. Host `gs.js` at the site root\\n\\nDownload the store\'s loader and serve it at `/gs.js` on the merchant\'s\\ndomain:\\n\\n```\\nhttps://<slug>.greatstore.ai/gs.js \u2192 https://www.merchant-site.com/gs.js\\n```\\n\\nAlways download it from the **store\'s own subdomain** \u2014 the file is built for\\nthat store; don\'t copy one from elsewhere.\\n\\nThen load it with one tag (replacing the `embed.js` tag \u2014 `gs.js` injects the\\nembed for you and registers itself as the service worker):\\n\\n```html\\n<script src=\\"/gs.js\\"></script>\\n```\\n\\nHosting this file is what enables push. Without it, push is simply off \u2014\\n`enableNotifications()` returns `{ ok: false }` and nothing else changes.\\n\\n### Non-root hosting\\n\\nIf the platform can\'t serve files at the site root (e.g. Shopify themes\\nserve assets under a path), keep the regular `embed.js` tag and point it at\\nwhere the file lives \u2014 the path must be on the merchant\'s own origin:\\n\\n```html\\n<script\\n src=\\"https://my-store.greatstore.ai/embed.js\\"\\n data-push-sw-path=\\"/cdn/shop/files/gs.js\\"\\n></script>\\n```\\n\\n## 2. Offer the opt-in from a user gesture\\n\\n```js\\noptInButton.addEventListener(\\"click\\", async () => {\\n const { ok } = await window.GreatStore.enableNotifications();\\n optInButton.hidden = ok; // done \u2014 or quietly keep the button\\n});\\n```\\n\\nRules that make this work well:\\n\\n- **Always call it from a click** \u2014 browsers ignore or penalize permission\\n prompts that aren\'t user-initiated, and the call is designed for gesture\\n context.\\n- **Never prompt on page load.** Tie the button to a moment where\\n notifications have obvious value (\\"Notify me when this is back in stock\\",\\n post-purchase, after a chat conversation).\\n- `{ ok: false }` covers every failure the same way \u2014 unsupported browser, no\\n `gs.js` hosted, permission denied. It never rejects, and there\'s no popup\\n fallback. Design the button so a decline just leaves the page as it was;\\n don\'t show an error.\\n- The promise resolving `{ ok: true }` means this browser is subscribed.\\n There\'s nothing else to wire \u2014 notification delivery is handled by\\n GreatStore.\\n","references/store-admin.md":"# Store administration from the CLI\\n\\nThe `gs` CLI is a full admin surface for a GreatStore store, mirroring the\\nmerchant\'s admin dashboard one-to-one: `gs configure` \u2194 the Configure panel,\\n`gs connectors` \u2194 the Connectors panel, `gs apps` \u2194 the Apps panel. Same\\nfields, same behaviour \u2014 anything you change is what the merchant sees in\\ntheir dashboard.\\n\\nThat makes the CLI the way a coding agent grounds and unblocks its own work:\\nread the store\'s configuration to make better decisions, and make the narrow\\nclass of additive, integration-enabling changes yourself instead of telling\\nthe user to go click through a dashboard.\\n\\n## Setup\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # one-time browser sign-in (needs a human)\\n```\\n\\nCredentials persist across runs. Admin commands take `--store <slug>`\\ndirectly, or read the nearest `.gsrc` (`{\\"store\\":\\"my-store\\"}`) \u2014 so they work\\nfrom any repo, not just a scaffolded component project. Every read supports\\n`--json` for machine-readable output.\\n\\n## Read freely \u2014 always safe\\n\\nReading store state is never destructive. Do it whenever it would improve a\\ndecision:\\n\\n```\\ngs configure --store my-store --json # brand config\\ngs connectors --store my-store --json # MCP connectors feeding the assistant\\ngs connectors health --store my-store # live-probe them (reports tool counts)\\ngs apps list --store my-store --json # deployed custom chat components\\n```\\n\\nWhat each read is good for:\\n\\n- **`gs configure`** \u2014 the store\'s `extraOrigins` (is the site you\'re\\n integrating actually allowlisted? \u2014 see the embed troubleshooting note in\\n [embed-api.md](embed-api.md)), `theme` (match generated on-page UI to the\\n brand\'s colors/radius), `storeLink`, and CSP host lists.\\n- **`gs connectors`** \u2014 which MCP connectors the assistant has, and whether\\n the Maker MCP is on. This tells you what the assistant can actually *do*:\\n check it before building features that lean on catalog data (e.g.\\n [structured content](structured-content.md) grounding), and before\\n registering WebMCP page tools that would duplicate a capability a\\n server-side connector already provides.\\n- **`gs connectors health`** \u2014 a connector being listed doesn\'t mean it\'s\\n reachable; this probes each one live. Run it when assistant behaviour\\n suggests a tool isn\'t responding.\\n\\n## Safe to change without asking\\n\\nWhen the task the user gave you requires it, these changes are yours to\\nmake: they\'re additive, easy to undo, and invisible to shoppers.\\n\\n- **`extraOrigins`** \u2014 the origin allowlist for the embed. Installing the\\n widget on a new site/staging domain? Add that origin yourself instead of\\n reporting \\"ask the merchant to allowlist it\\".\\n- **`cspScriptHosts` / `cspConnectHosts`** \u2014 hosts the storefront accepts\\n for scripts/connections. Add a host when a custom chat component\\n legitimately needs a third-party SDK or API.\\n\\n**List flags replace the stored list \u2014 always read-merge-write.** Fetch the\\ncurrent value, append yours, write the union:\\n\\n```\\ngs configure --store my-store --json\\n# extraOrigins is [\\"https://shop.example.com\\"] \u2192 write both, comma-separated:\\ngs configure set --store my-store \\\\\\n --extraOrigins \\"https://shop.example.com,https://staging.example.com\\"\\n```\\n\\nNever drop an entry you didn\'t add, and say what you changed (and why) when\\nyou report back to the user.\\n\\n## Ask the merchant first\\n\\nEverything below is shopper-visible or changes what the live assistant can\\ndo for every shopper. Propose it, don\'t do it unprompted:\\n\\n- **Brand-visible config** \u2014 `displayName`, `assistantName`, `storeLink`,\\n `theme`, and asset uploads\\n (`gs configure upload|clear icon|logoLight|logoDark`).\\n- **Connector mutations** \u2014 `gs connectors add|remove|enable|disable` and\\n `gs connectors maker on|off`. (`add` probes the connector for tool\\n discovery before saving and refuses if it doesn\'t answer; `--force`\\n overrides. Still: adding capabilities to the merchant\'s assistant is the\\n merchant\'s call.)\\n- **Anything that clears** \u2014 passing `\\"\\"` to empty a field, removing list\\n entries, `clear`ing assets.\\n\\nWhen the user has *explicitly asked* for one of these (\\"set the assistant\'s\\nname to Voyager\\", \\"connect this MCP server\\"), that\'s the go-ahead \u2014 do it\\nand confirm the result with a read.\\n\\n## Command reference\\n\\n```\\ngs configure [--json] show configuration\\ngs configure set --<field> <value> displayName, assistantName,\\n storeLink, extraOrigins a,b,\\n theme \'<json>\' (--themeFile <path>),\\n cspScriptHosts a,b, cspConnectHosts a,b\\n (\\"\\" clears a field)\\ngs configure upload <kind> <file> icon | logoLight | logoDark (.png/.jpg/.webp)\\ngs configure clear <kind> remove an uploaded asset\\n\\ngs connectors [--json] list Maker toggle + custom connectors\\ngs connectors add <name> --url <url> [--token <t>] [--profileUrl <u>]\\n [--disabled] [--force]\\ngs connectors remove <name|id>\\ngs connectors enable|disable <name|id>\\ngs connectors maker on|off\\ngs connectors health [<name|id>] [--json]\\n\\ngs apps <init|build|list|pull|push|publish|unpublish|delete>\\n see chat-components.md for the workflow\\n```\\n","references/structured-content.md":"# `generateStructuredContent` deep dive\\n\\nAI-generated, catalog-grounded JSON for UI you render yourself \u2014 the chat\\npanel is not involved.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(schema, prompt);\\n```\\n\\n- `schema` \u2014 a JSON Schema describing the output (or a Zod schema exposing\\n `.toJSONSchema()`).\\n- `prompt` \u2014 what to generate.\\n- Resolves to **the generated JSON object itself**, matching the schema.\\n Rejects with an `Error` on any failure.\\n\\nCode samples here are framework-free reference implementations \u2014 re-express\\nthem in the host repo\'s framework (React, Vue, Shopify Liquid, \u2026) rather\\nthan retrofitting them as-is.\\n\\n## The rules that make it work well\\n\\nEach is unpacked in the sections below; this is the checklist.\\n\\n1. **Top level must be an object.** Want a list? Wrap it:\\n `{ type: \\"object\\", properties: { items: { type: \\"array\\", \u2026 } }, required: [\\"items\\"] }`.\\n2. **Steer with the prompt, not schema `description`s** \u2014 free-text schema\\n fields are stripped before the AI sees them. Use self-explanatory\\n property names (`benefitHeadline`, not `text1`).\\n3. **Only `require` what\'s guaranteed.** `required` is strictly enforced; if\\n the catalog can\'t ground a required field the whole call can fail. Require\\n structural fields, keep per-product details (image URLs, prices) optional,\\n and make rendering tolerate missing values.\\n4. **Point, don\'t paste.** GreatStore researches the store\'s live catalog on\\n its own \u2014 name the entity (`` `\u2026the product \\"${productName}\\" (SKU ${sku})` ``)\\n and let it look the facts up. Don\'t fetch specs/prices/descriptions\\n yourself and paste them into the prompt. The one thing it *can\'t* see is\\n your page, so page-only context (which page the shopper is on, what the\\n section is for) does belong in the prompt.\\n5. **Research is bounded by the store\'s connectors \u2014 validate before you\\n build.** The AI can only look up what the store\'s MCP connectors actually\\n provide. Ask for something outside them \u2014 currency conversion, live\\n shipping rates, review data the store never wired up \u2014 and it has nothing\\n to ground on, so it will decline, omit\u2026 or hallucinate. Before writing a\\n prompt or schema that depends on a data capability, run `gs connectors`\\n (and `gs connectors health`, [store-admin.md](store-admin.md)) and confirm\\n a connector for it exists; if it doesn\'t, don\'t ask for it.\\n6. **Responses are cached** for up to ~24h per page + prompt + schema \u2014\\n shared across anonymous visitors, per-shopper for identified ones. Keep\\n prompts deterministic per page \u2014 no timestamps, random values, or\\n per-visitor data (GreatStore already knows who the shopper is; see below).\\n7. **Progressive enhancement, always.** Generate after the page renders into\\n a hidden container, reveal on success, leave the fallback on error. Render\\n generated strings via `textContent`, never `innerHTML`.\\n8. **One rich call beats many small ones** \u2014 there\'s a per-visitor rate limit\\n (~20 requests/minute); fetch multiple surfaces with one combined schema.\\n\\n## What actually happens\\n\\n1. The SDK posts your schema + prompt to the store\'s GreatStore endpoint,\\n along with the current **page URL and page title** (sent automatically \u2014\\n you don\'t pass them, and you can\'t override them).\\n2. GreatStore first **researches**: it looks up real data from the store\'s\\n live catalog (products, prices, availability, store info) using read-only\\n lookups. The page URL/title serve as hints about which product or category\\n to look up \u2014 they are *not* treated as a source of product data, and the\\n page\'s DOM is never read. Research happens on GreatStore\'s side \u2014 your\\n prompt only needs to *point* it at the right SKU, product, or collection,\\n not carry the material. Its reach is exactly the store\'s MCP connectors:\\n validate with `gs connectors` ([store-admin.md](store-admin.md)) that a\\n connector for the data you want actually exists before you build on it \u2014\\n research can\'t exceed the wired-up connectors, and prompts that assume\\n otherwise invite hallucinated filler.\\n3. The AI then fills your schema from the researched data, under a strict\\n grounding contract: it must not invent product names, prices, images, IDs,\\n or descriptions. Fields it can\'t ground are omitted or `null`. For an\\n identified shopper this step also sees their shopper profile \u2014 the same\\n identity the chat assistant has \u2014 so the output can be subtly personalized\\n without you passing anything about the visitor.\\n4. The output is validated against your schema (with internal retries) before\\n being returned and cached.\\n\\nGreatStore already knows who\'s reading: identity rides the request the same\\nway it does for chat, and responses for identified shoppers are cached just\\nfor them (anonymous visitors share one entry). The practical consequence:\\nnever put shopper data in the prompt \u2014 it\'s redundant, and it poisons the\\ncache key.\\n\\n## Schema support\\n\\nTop level **must describe an object**: `type: \\"object\\"` (or a bare\\n`properties` / `anyOf`). To get a list, wrap it in an object property.\\n\\nSupported keywords (anything else is tolerated but ignored):\\n\\n- Types: `object`, `array`, `string`, `number`, `integer`, `boolean`, `null`\\n- Structure: `properties`, `required`, `items`, `additionalProperties`\\n- Choice: `enum`, `const`, `anyOf`, `nullable`\\n- Constraints: `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`,\\n `maxItems`, `pattern`, `format`, `default`\\n\\nValidation of the output is real: `required` is enforced, `enum`/`const`\\nmust match, numeric and length bounds are checked, and\\n`additionalProperties: false` rejects extra keys. Constraints are therefore a\\n*tool* \u2014 `maxItems: 4` reliably caps a list, `enum` reliably restricts a\\nfield \u2014 but every constraint is also a way for generation to fail, so apply\\nthem only where you\'d rather have no content than non-conforming content.\\n\\nZod schemas (or anything with a `.toJSONSchema()` method) are accepted and\\nconverted automatically.\\n\\n### Free-text schema fields are stripped\\n\\n`description`, `title`, and `example` are removed from the schema before the\\nAI sees it (they\'re a prompt-injection surface, so they\'re filtered\\nserver-side). Consequences:\\n\\n- Schema descriptions **cannot** steer generation. All steering lives in the\\n prompt string.\\n- Property *names* are the only in-schema signal of intent \u2014 make them\\n self-documenting: `ctaLabel`, `warmthRating`, `priceJustification`.\\n\\n## Prompting guide\\n\\nThe prompt is the entire instruction channel. A good prompt states, in order:\\n\\n1. **Context** \u2014 what page/situation the shopper is in, naming the entity so\\n research targets the right thing (the AI can\'t see your DOM, so identify\\n it explicitly):\\n `The shopper is viewing the product \\"Aurora Down Parka\\" (SKU AUR-021) on its product page.`\\n2. **Task** \u2014 what to generate, mapped loosely onto your schema\'s fields:\\n `Write a heading and 3 reasons to love it; each reason has a short title and one supporting sentence.`\\n3. **Grounding expectations** \u2014 what store data to draw on:\\n `Base every claim on the product\'s real materials, features, and price.`\\n4. **Voice** \u2014 tone and constraints:\\n `Warm and concrete. No exclamation marks, no generic marketing filler.`\\n\\nNote what\'s *not* in that prompt: no pasted specs, prices, or descriptions.\\nPointing at the SKU is enough \u2014 GreatStore researches the rest itself, from\\ndata that\'s live rather than whatever was true when you wrote the prompt.\\n\\nAnti-patterns:\\n\\n- **Pasting researched material into the prompt** (specs, prices,\\n descriptions you fetched from your platform\'s API). GreatStore researches\\n the live catalog itself \u2014 point it at the SKU/product/collection and let\\n it look things up. Pasted facts go stale, bloat the cache key, and compete\\n with the fresher data research returns.\\n- **Per-visitor or per-moment data in the prompt** (names, cart contents,\\n timestamps, `Math.random()`): destroys caching, so every visitor pays full\\n generation latency and the store pays for every call \u2014 and it\'s redundant,\\n because GreatStore already knows the shopper and personalizes for\\n identified ones server-side. If you need per-shopper *interaction*, that\'s\\n what `sendMessage` and the chat panel are for.\\n- **Asking for data no connector provides** (\\"convert the price to EUR\\",\\n \\"estimate delivery to the shopper\'s city\\"): research can\'t exceed the\\n store\'s MCP connectors, and the AI may hallucinate plausible-looking\\n values rather than leave the field empty. Validate first \u2014 `gs connectors`\\n shows what\'s wired up ([store-admin.md](store-admin.md)); if there\'s no\\n connector for it, don\'t ask for it.\\n- **Asking it to read the page** (\\"summarize the reviews shown below\\") \u2014 it\\n can\'t. Page-only data (review snippets, UGC, things that exist nowhere but\\n the DOM) is the one kind worth inlining \u2014 keep it stable per page so\\n caching still works.\\n- **Asking for minute-fresh operational data** (exact live stock counts,\\n delivery countdowns). Even when a connector could answer, responses cache\\n for up to ~24h \u2014 display operational data from your own platform APIs and\\n use GreatStore for *editorial intelligence over the catalog*.\\n- **Burying instructions in schema descriptions** \u2014 stripped, see above.\\n\\n## Caching: design for it\\n\\nResponses are cached server-side for up to **24 hours**, keyed by the\\ncombination of page URL + page title + prompt + schema. Anonymous visitors\\nshare one entry per key; identified shoppers each get their own (their\\noutput may be personalized, so it\'s only ever served back to them).\\n(Tracking query params like `utm_*`/`gclid` and the URL fragment are\\nignored, so ad-tagged visits share the campaign-free page\'s cache entry.\\nMeaningful params like `?product=123` are part of the key.)\\n\\nPractical consequences:\\n\\n- **The first render pays, repeats fly.** Expect a few seconds on a cache\\n miss and near-instant responses after \u2014 shared across all anonymous\\n traffic, per-shopper for identified traffic (the research underneath is\\n cached briefly and shared, so even those misses are cheaper than cold).\\n Design loading states for the miss case.\\n- **Same call on different pages = different content**, automatically \u2014 the\\n page URL is in the key and in the AI\'s hints. A single site-wide snippet\\n with a constant prompt yields per-page content for free.\\n- **Content refreshes roughly daily.** Don\'t build experiences that assume\\n minute-level freshness.\\n- **To force different content, change the prompt or schema** (e.g. a\\n campaign variant string that changes weekly \u2014 deliberate, low-cardinality\\n variation is fine; per-visitor cardinality is not).\\n\\nThe catalog research underneath is also cached briefly, so several distinct\\nsurfaces on the same page (different prompts/schemas) stay cheap even on\\ncold cache.\\n\\n## Errors and how to handle them\\n\\nThe promise rejects with `new Error(message)`. The message is\\ndeveloper-facing \u2014 never render it to shoppers. Cases:\\n\\n| Case | Message you\'ll see | Retry? |\\n|---|---|---|\\n| Bad input (empty prompt, non-object schema) | thrown immediately by the SDK | Fix the call |\\n| Invalid schema shape | `Invalid schema: \u2026` | Fix the schema |\\n| AI declined the request | `The assistant declined to generate content for this request.` | No \u2014 permanent for that prompt/schema. Rework the prompt. |\\n| Output couldn\'t satisfy the schema | `Failed to produce valid structured content` | No \u2014 usually `required`/constraints demand data the catalog lacks. Loosen the schema. |\\n| Rate limit (~20/min per visitor) | rate-limit message | Later \u2014 and consolidate calls |\\n| Network / server | varies | Next page load |\\n\\nThe uniform shopper-facing strategy: render into a hidden-by-default\\ncontainer, reveal on success, leave hidden (or show your static fallback) on\\nany rejection. One `try/catch`, no case analysis needed unless you\'re\\nlogging.\\n\\n## Performance pattern\\n\\nFire generation as early as possible without blocking render \u2014 top of your\\ndeferred script, before other work:\\n\\n```js\\nconst highlightsPromise = window.GreatStore?.generateStructuredContent\\n ? window.GreatStore.generateStructuredContent(schema, prompt).catch(() => null)\\n : Promise.resolve(null);\\n\\n// \u2026rest of page setup\u2026\\n\\nconst data = await highlightsPromise;\\nif (data) renderHighlights(data);\\n```\\n\\nThe `.catch(() => null)` attached immediately avoids unhandled-rejection\\nnoise while keeping a single render path.\\n\\nFor multiple surfaces on one page, prefer **one call with a combined\\nschema** over parallel calls \u2014 it\'s one research pass, one cache entry, and\\nno rate-limit pressure:\\n\\n```js\\nconst schema = {\\n type: \\"object\\",\\n properties: {\\n highlights: { /* \u2026 */ },\\n faq: { /* \u2026 */ },\\n crossSell: { /* \u2026 */ },\\n },\\n required: [\\"highlights\\"],\\n};\\n```\\n"}') : readTreeFromDisk(new URL("../../gs-skill/", import.meta.url));
|
|
677
|
-
if (!cache2["SKILL.md"]) {
|
|
678
|
-
throw new Error("internal: agent skill tree is missing SKILL.md");
|
|
679
|
-
}
|
|
680
|
-
return cache2;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// src/commands/skill.ts
|
|
684
|
-
function skillCommand(args) {
|
|
685
|
-
const global = flagBool(args.flags, "global");
|
|
686
|
-
const dirFlag = flagString(args.flags, "dir");
|
|
687
|
-
if (global && dirFlag !== void 0) {
|
|
688
|
-
throw new Error("Pass either --global or --dir <path>, not both.");
|
|
689
|
-
}
|
|
690
|
-
const skillsDir = dirFlag !== void 0 ? path3.resolve(dirFlag) : global ? path3.join(os2.homedir(), ".claude", "skills") : path3.resolve(".claude", "skills");
|
|
691
|
-
const dest = path3.join(skillsDir, SKILL_DIR_NAME);
|
|
692
|
-
const existed = fs3.existsSync(path3.join(dest, "SKILL.md"));
|
|
693
|
-
for (const [rel, content] of Object.entries(loadSkill())) {
|
|
694
|
-
const full = path3.join(dest, ...rel.split("/"));
|
|
695
|
-
fs3.mkdirSync(path3.dirname(full), { recursive: true });
|
|
696
|
-
fs3.writeFileSync(full, content);
|
|
697
|
-
}
|
|
698
|
-
process.stdout.write(
|
|
699
|
-
[
|
|
700
|
-
`${existed ? "Updated" : "Installed"} the GreatStore agent skill at ${displayPath(dest)}.`,
|
|
701
|
-
"",
|
|
702
|
-
"Your coding agent picks it up automatically. Try asking it:",
|
|
703
|
-
` "Add an AI-powered gift finder to my product page with GreatStore."`,
|
|
704
|
-
""
|
|
705
|
-
].join("\n")
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
function displayPath(dest) {
|
|
709
|
-
const home = os2.homedir();
|
|
710
|
-
if (dest.startsWith(home + path3.sep)) {
|
|
711
|
-
return `~${dest.slice(home.length)}`;
|
|
712
|
-
}
|
|
713
|
-
const rel = path3.relative(process.cwd(), dest);
|
|
714
|
-
return rel && !rel.startsWith("..") ? rel : dest;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// src/commands/apps/init.ts
|
|
718
|
-
import * as fs4 from "fs";
|
|
719
|
-
import * as path4 from "path";
|
|
720
|
-
|
|
721
749
|
// src/version.ts
|
|
722
|
-
var CLI_VERSION = true ? "0.1.
|
|
750
|
+
var CLI_VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
723
751
|
|
|
724
752
|
// src/commands/apps/init.ts
|
|
725
753
|
var NAME_REGEX = /^[a-z][a-z0-9_]*$/;
|
|
@@ -731,7 +759,7 @@ function initCommand(args) {
|
|
|
731
759
|
const force = flagBool(args.flags, "force");
|
|
732
760
|
const storeFlag = flagString(args.flags, "store");
|
|
733
761
|
const outRel = flagString(args.flags, "out") ?? ".";
|
|
734
|
-
const root =
|
|
762
|
+
const root = path5.resolve(outRel);
|
|
735
763
|
const rootExisted = hasRootScaffold(root);
|
|
736
764
|
let store;
|
|
737
765
|
if (rootExisted) {
|
|
@@ -766,7 +794,7 @@ function initCommand(args) {
|
|
|
766
794
|
lines.push(
|
|
767
795
|
"",
|
|
768
796
|
"Next steps:",
|
|
769
|
-
` cd ${
|
|
797
|
+
` cd ${path5.relative(process.cwd(), root) || "."}`,
|
|
770
798
|
" npm install",
|
|
771
799
|
" gs apps init <component_name> # add your first component",
|
|
772
800
|
""
|
|
@@ -774,9 +802,9 @@ function initCommand(args) {
|
|
|
774
802
|
process.stdout.write(lines.join("\n"));
|
|
775
803
|
return;
|
|
776
804
|
}
|
|
777
|
-
const componentDir =
|
|
805
|
+
const componentDir = path5.join(root, "components", name);
|
|
778
806
|
ensureComponent(componentDir, name, force);
|
|
779
|
-
const projectLabel =
|
|
807
|
+
const projectLabel = path5.relative(process.cwd(), root) || ".";
|
|
780
808
|
process.stdout.write(
|
|
781
809
|
[
|
|
782
810
|
`Added component "${name}" at components/${name}/.`,
|
|
@@ -792,7 +820,7 @@ function initCommand(args) {
|
|
|
792
820
|
);
|
|
793
821
|
}
|
|
794
822
|
function hasRootScaffold(dir) {
|
|
795
|
-
return fs4.existsSync(
|
|
823
|
+
return fs4.existsSync(path5.join(dir, "package.json")) && fs4.existsSync(path5.join(dir, ".gsrc"));
|
|
796
824
|
}
|
|
797
825
|
function ensureRoot(root, rootExisted, force, opts) {
|
|
798
826
|
fs4.mkdirSync(root, { recursive: true });
|
|
@@ -806,15 +834,15 @@ function ensureRoot(root, rootExisted, force, opts) {
|
|
|
806
834
|
}
|
|
807
835
|
const files = rootExisted ? existingRootFiles() : rootFiles({ store: opts.store });
|
|
808
836
|
const written = writeRootFiles(root, files, force);
|
|
809
|
-
fs4.mkdirSync(
|
|
837
|
+
fs4.mkdirSync(path5.join(root, "components"), { recursive: true });
|
|
810
838
|
return written;
|
|
811
839
|
}
|
|
812
840
|
var CLI_OWNED_ROOT_FILES = /* @__PURE__ */ new Set(["AGENTS.md"]);
|
|
813
841
|
function writeRootFiles(root, files, force) {
|
|
814
842
|
const written = [];
|
|
815
843
|
for (const [relPath, content] of files) {
|
|
816
|
-
const full =
|
|
817
|
-
fs4.mkdirSync(
|
|
844
|
+
const full = path5.join(root, relPath);
|
|
845
|
+
fs4.mkdirSync(path5.dirname(full), { recursive: true });
|
|
818
846
|
const exists = fs4.existsSync(full);
|
|
819
847
|
if (!force && exists && !CLI_OWNED_ROOT_FILES.has(relPath)) continue;
|
|
820
848
|
const changed = !exists || fs4.readFileSync(full, "utf8") !== content;
|
|
@@ -822,7 +850,7 @@ function writeRootFiles(root, files, force) {
|
|
|
822
850
|
if (changed) written.push(relPath);
|
|
823
851
|
}
|
|
824
852
|
for (const link of ["CLAUDE.md", "GEMINI.md"]) {
|
|
825
|
-
const linkPath =
|
|
853
|
+
const linkPath = path5.join(root, link);
|
|
826
854
|
if (force && pathExists(linkPath)) fs4.rmSync(linkPath);
|
|
827
855
|
if (!pathExists(linkPath)) {
|
|
828
856
|
fs4.symlinkSync("AGENTS.md", linkPath);
|
|
@@ -850,7 +878,7 @@ function ensureComponent(componentDir, name, force) {
|
|
|
850
878
|
}
|
|
851
879
|
fs4.mkdirSync(componentDir, { recursive: true });
|
|
852
880
|
for (const [relPath, content] of componentFiles(name)) {
|
|
853
|
-
fs4.writeFileSync(
|
|
881
|
+
fs4.writeFileSync(path5.join(componentDir, relPath), content);
|
|
854
882
|
}
|
|
855
883
|
}
|
|
856
884
|
function templateFiles(prefix, vars, exclude = /* @__PURE__ */ new Set()) {
|
|
@@ -885,16 +913,16 @@ function pascal(name) {
|
|
|
885
913
|
|
|
886
914
|
// src/commands/apps/build.ts
|
|
887
915
|
import * as fs6 from "fs";
|
|
888
|
-
import * as
|
|
916
|
+
import * as path8 from "path";
|
|
889
917
|
import { createRequire as createRequire2 } from "module";
|
|
890
918
|
|
|
891
919
|
// src/validation/context.ts
|
|
892
920
|
import * as fs5 from "fs";
|
|
893
|
-
import * as
|
|
921
|
+
import * as path7 from "path";
|
|
894
922
|
|
|
895
923
|
// src/validation/ast.ts
|
|
896
924
|
import { createRequire } from "module";
|
|
897
|
-
import * as
|
|
925
|
+
import * as path6 from "path";
|
|
898
926
|
function analyzeComponentProps(root, sourcePath, sourceText) {
|
|
899
927
|
const ts = loadTypeScript(root);
|
|
900
928
|
if (!ts) {
|
|
@@ -922,7 +950,7 @@ function analyzeComponentProps(root, sourcePath, sourceText) {
|
|
|
922
950
|
} catch (err) {
|
|
923
951
|
return {
|
|
924
952
|
ok: false,
|
|
925
|
-
reason: `couldn't check props against the manifest \u2014 ${
|
|
953
|
+
reason: `couldn't check props against the manifest \u2014 ${path6.basename(sourcePath)} could not be parsed (${err.message})`
|
|
926
954
|
};
|
|
927
955
|
}
|
|
928
956
|
}
|
|
@@ -966,7 +994,7 @@ function isGlobalObject(ts, node) {
|
|
|
966
994
|
return ts.isIdentifier(node) && (node.text === "window" || node.text === "globalThis");
|
|
967
995
|
}
|
|
968
996
|
function loadTypeScript(root) {
|
|
969
|
-
const localRequire = createRequire(
|
|
997
|
+
const localRequire = createRequire(path6.join(root, "package.json"));
|
|
970
998
|
try {
|
|
971
999
|
return localRequire("typescript");
|
|
972
1000
|
} catch {
|
|
@@ -1124,9 +1152,9 @@ function mergeProps(parts) {
|
|
|
1124
1152
|
// src/validation/context.ts
|
|
1125
1153
|
function buildContext(paths) {
|
|
1126
1154
|
const { root, name } = paths;
|
|
1127
|
-
const componentDir =
|
|
1128
|
-
const manifestPath = paths.manifestPath ??
|
|
1129
|
-
const sourcePath = paths.sourcePath ??
|
|
1155
|
+
const componentDir = path7.join(root, "components", name);
|
|
1156
|
+
const manifestPath = paths.manifestPath ?? path7.join(componentDir, "manifest.json");
|
|
1157
|
+
const sourcePath = paths.sourcePath ?? path7.join(componentDir, "component.tsx");
|
|
1130
1158
|
const manifestFile = relative4(root, manifestPath);
|
|
1131
1159
|
const sourceFile = relative4(root, sourcePath);
|
|
1132
1160
|
const diagnostics = [];
|
|
@@ -1184,8 +1212,8 @@ function readIfPresent(file) {
|
|
|
1184
1212
|
}
|
|
1185
1213
|
}
|
|
1186
1214
|
function relative4(root, target) {
|
|
1187
|
-
const rel =
|
|
1188
|
-
return rel.startsWith("..") ? target : rel.split(
|
|
1215
|
+
const rel = path7.relative(root, target);
|
|
1216
|
+
return rel.startsWith("..") ? target : rel.split(path7.sep).join("/");
|
|
1189
1217
|
}
|
|
1190
1218
|
|
|
1191
1219
|
// src/validation/injected-props.ts
|
|
@@ -1282,24 +1310,24 @@ function schemaFieldNames(inputSchema) {
|
|
|
1282
1310
|
var MAX_SCHEMA_BYTES = 8 * 1024;
|
|
1283
1311
|
var MAX_SCHEMA_FIELDS = 50;
|
|
1284
1312
|
var UNION_KEYWORDS = ["anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
1285
|
-
function measure(node,
|
|
1313
|
+
function measure(node, path15, acc) {
|
|
1286
1314
|
if (Array.isArray(node)) {
|
|
1287
|
-
node.forEach((entry, i) => measure(entry, `${
|
|
1315
|
+
node.forEach((entry, i) => measure(entry, `${path15}[${i}]`, acc));
|
|
1288
1316
|
return;
|
|
1289
1317
|
}
|
|
1290
1318
|
if (!node || typeof node !== "object") return;
|
|
1291
1319
|
const record = node;
|
|
1292
1320
|
for (const keyword of UNION_KEYWORDS) {
|
|
1293
|
-
if (keyword in record) acc.unionPaths.push(`${
|
|
1321
|
+
if (keyword in record) acc.unionPaths.push(`${path15}.${keyword}`);
|
|
1294
1322
|
}
|
|
1295
1323
|
const properties = record["properties"];
|
|
1296
1324
|
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
1297
1325
|
for (const [name, sub] of Object.entries(properties)) {
|
|
1298
1326
|
acc.fields += 1;
|
|
1299
|
-
measure(sub, `${
|
|
1327
|
+
measure(sub, `${path15}.${name}`, acc);
|
|
1300
1328
|
}
|
|
1301
1329
|
}
|
|
1302
|
-
if ("items" in record) measure(record["items"], `${
|
|
1330
|
+
if ("items" in record) measure(record["items"], `${path15}[]`, acc);
|
|
1303
1331
|
}
|
|
1304
1332
|
function inputSchemaComplexityRule(ctx) {
|
|
1305
1333
|
const inputSchema = ctx.manifest?.inputSchema;
|
|
@@ -1393,7 +1421,7 @@ function validateComponent(paths) {
|
|
|
1393
1421
|
var COMPONENTS_DIR = "components";
|
|
1394
1422
|
async function buildCommand(args) {
|
|
1395
1423
|
const root = process.cwd();
|
|
1396
|
-
if (!fs6.existsSync(
|
|
1424
|
+
if (!fs6.existsSync(path8.join(root, "package.json")) || !fs6.existsSync(path8.join(root, COMPONENTS_DIR))) {
|
|
1397
1425
|
throw new Error(
|
|
1398
1426
|
`\`gs apps build\` must run from a project root (contains \`package.json\` and \`${COMPONENTS_DIR}/\`). Current dir: ${root}`
|
|
1399
1427
|
);
|
|
@@ -1430,14 +1458,14 @@ Nothing built \u2014 fix the errors above.
|
|
|
1430
1458
|
}
|
|
1431
1459
|
const { build, reactPlugin, transformWithEsbuild } = await loadVite(root);
|
|
1432
1460
|
for (const name of buildable) {
|
|
1433
|
-
const dir =
|
|
1434
|
-
const bundlePath =
|
|
1461
|
+
const dir = path8.join(root, COMPONENTS_DIR, name);
|
|
1462
|
+
const bundlePath = path8.join(dir, "bundle.js");
|
|
1435
1463
|
await build({
|
|
1436
1464
|
plugins: [reactPlugin()],
|
|
1437
1465
|
logLevel: "warn",
|
|
1438
1466
|
build: {
|
|
1439
1467
|
lib: {
|
|
1440
|
-
entry:
|
|
1468
|
+
entry: path8.join(dir, "component.tsx"),
|
|
1441
1469
|
formats: ["es"],
|
|
1442
1470
|
fileName: () => "bundle.js"
|
|
1443
1471
|
},
|
|
@@ -1490,10 +1518,10 @@ Built locally \u2014 nothing uploaded yet. Next: \`gs apps push\` to upload, the
|
|
|
1490
1518
|
);
|
|
1491
1519
|
}
|
|
1492
1520
|
function listComponents(root) {
|
|
1493
|
-
const dir =
|
|
1521
|
+
const dir = path8.join(root, COMPONENTS_DIR);
|
|
1494
1522
|
return fs6.readdirSync(dir).filter((entry) => {
|
|
1495
|
-
const candidate =
|
|
1496
|
-
return fs6.statSync(candidate).isDirectory() && fs6.existsSync(
|
|
1523
|
+
const candidate = path8.join(dir, entry);
|
|
1524
|
+
return fs6.statSync(candidate).isDirectory() && fs6.existsSync(path8.join(candidate, "component.tsx"));
|
|
1497
1525
|
}).sort();
|
|
1498
1526
|
}
|
|
1499
1527
|
function formatBytes(n) {
|
|
@@ -1505,7 +1533,7 @@ function pctSmaller(before, after) {
|
|
|
1505
1533
|
return `${Math.round((1 - after / before) * 100)}%`;
|
|
1506
1534
|
}
|
|
1507
1535
|
async function loadVite(root) {
|
|
1508
|
-
const localRequire = createRequire2(
|
|
1536
|
+
const localRequire = createRequire2(path8.join(root, "package.json"));
|
|
1509
1537
|
const vitePath = resolveEsmEntry(localRequire, "vite");
|
|
1510
1538
|
if (!vitePath) {
|
|
1511
1539
|
throw new Error(
|
|
@@ -1553,7 +1581,7 @@ function resolveEsmEntry(req, specifier) {
|
|
|
1553
1581
|
}
|
|
1554
1582
|
const pkgJsonPath = findOwningPackageJson(anchor, specifier);
|
|
1555
1583
|
if (!pkgJsonPath) return null;
|
|
1556
|
-
const pkgDir =
|
|
1584
|
+
const pkgDir = path8.dirname(pkgJsonPath);
|
|
1557
1585
|
let pkg;
|
|
1558
1586
|
try {
|
|
1559
1587
|
pkg = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf8"));
|
|
@@ -1563,12 +1591,12 @@ function resolveEsmEntry(req, specifier) {
|
|
|
1563
1591
|
const fromExports = pickImportEntry(pkg.exports);
|
|
1564
1592
|
const entry = fromExports ?? (typeof pkg.module === "string" ? pkg.module : null) ?? (typeof pkg.main === "string" ? pkg.main : null);
|
|
1565
1593
|
if (!entry) return null;
|
|
1566
|
-
return
|
|
1594
|
+
return path8.resolve(pkgDir, entry);
|
|
1567
1595
|
}
|
|
1568
1596
|
function findOwningPackageJson(start, specifier) {
|
|
1569
|
-
let dir =
|
|
1597
|
+
let dir = path8.dirname(start);
|
|
1570
1598
|
while (true) {
|
|
1571
|
-
const candidate =
|
|
1599
|
+
const candidate = path8.join(dir, "package.json");
|
|
1572
1600
|
if (fs6.existsSync(candidate)) {
|
|
1573
1601
|
try {
|
|
1574
1602
|
const parsed = JSON.parse(fs6.readFileSync(candidate, "utf8"));
|
|
@@ -1576,7 +1604,7 @@ function findOwningPackageJson(start, specifier) {
|
|
|
1576
1604
|
} catch {
|
|
1577
1605
|
}
|
|
1578
1606
|
}
|
|
1579
|
-
const parent =
|
|
1607
|
+
const parent = path8.dirname(dir);
|
|
1580
1608
|
if (parent === dir) return null;
|
|
1581
1609
|
dir = parent;
|
|
1582
1610
|
}
|
|
@@ -1637,12 +1665,12 @@ function pad(s, width) {
|
|
|
1637
1665
|
|
|
1638
1666
|
// src/commands/apps/pull.ts
|
|
1639
1667
|
import * as fs8 from "fs";
|
|
1640
|
-
import * as
|
|
1668
|
+
import * as path10 from "path";
|
|
1641
1669
|
|
|
1642
1670
|
// src/sync.ts
|
|
1643
1671
|
import * as crypto2 from "crypto";
|
|
1644
1672
|
import * as fs7 from "fs";
|
|
1645
|
-
import * as
|
|
1673
|
+
import * as path9 from "path";
|
|
1646
1674
|
var SYNC_FILE = ".gssync.json";
|
|
1647
1675
|
function hashString(text) {
|
|
1648
1676
|
return crypto2.createHash("sha256").update(text).digest("hex");
|
|
@@ -1652,17 +1680,17 @@ function hashFile(filePath) {
|
|
|
1652
1680
|
return hashString(fs7.readFileSync(filePath, "utf8"));
|
|
1653
1681
|
}
|
|
1654
1682
|
function computeComponentHashes(componentDir) {
|
|
1655
|
-
const manifestHash = hashFile(
|
|
1683
|
+
const manifestHash = hashFile(path9.join(componentDir, "manifest.json"));
|
|
1656
1684
|
if (manifestHash === null) {
|
|
1657
1685
|
throw new Error(`Missing manifest.json in ${componentDir}`);
|
|
1658
1686
|
}
|
|
1659
1687
|
return {
|
|
1660
1688
|
manifestHash,
|
|
1661
|
-
sourceHash: hashFile(
|
|
1689
|
+
sourceHash: hashFile(path9.join(componentDir, "component.tsx"))
|
|
1662
1690
|
};
|
|
1663
1691
|
}
|
|
1664
1692
|
function readSyncState(componentDir) {
|
|
1665
|
-
const file =
|
|
1693
|
+
const file = path9.join(componentDir, SYNC_FILE);
|
|
1666
1694
|
if (!fs7.existsSync(file)) return null;
|
|
1667
1695
|
try {
|
|
1668
1696
|
const parsed = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
@@ -1680,7 +1708,7 @@ function readSyncState(componentDir) {
|
|
|
1680
1708
|
}
|
|
1681
1709
|
function writeSyncState(componentDir, state) {
|
|
1682
1710
|
fs7.writeFileSync(
|
|
1683
|
-
|
|
1711
|
+
path9.join(componentDir, SYNC_FILE),
|
|
1684
1712
|
JSON.stringify(state, null, 2) + "\n"
|
|
1685
1713
|
);
|
|
1686
1714
|
}
|
|
@@ -1721,7 +1749,7 @@ async function pullCommand(args) {
|
|
|
1721
1749
|
for (const name of targets) {
|
|
1722
1750
|
const outcome = await pullOne({
|
|
1723
1751
|
slug,
|
|
1724
|
-
componentDir:
|
|
1752
|
+
componentDir: path10.join(root, "components", name),
|
|
1725
1753
|
name,
|
|
1726
1754
|
revisionQuery,
|
|
1727
1755
|
force
|
|
@@ -1750,12 +1778,12 @@ async function pullAll(opts) {
|
|
|
1750
1778
|
}
|
|
1751
1779
|
const outcomes = [];
|
|
1752
1780
|
for (const entry of list.components) {
|
|
1753
|
-
const componentDir =
|
|
1781
|
+
const componentDir = path10.join(root, "components", entry.name);
|
|
1754
1782
|
const outcome = await pullOne({
|
|
1755
1783
|
slug,
|
|
1756
1784
|
componentDir,
|
|
1757
1785
|
name: entry.name,
|
|
1758
|
-
revisionQuery,
|
|
1786
|
+
revisionQuery: revisionQuery === "" && entry.live === null ? "?revision=draft" : revisionQuery,
|
|
1759
1787
|
force
|
|
1760
1788
|
});
|
|
1761
1789
|
outcomes.push(outcome);
|
|
@@ -1788,25 +1816,37 @@ async function pullOne(opts) {
|
|
|
1788
1816
|
};
|
|
1789
1817
|
}
|
|
1790
1818
|
}
|
|
1791
|
-
const
|
|
1819
|
+
const base = `${apiBaseFor(slug)}/api/apps/components/${encodeURIComponent(name)}`;
|
|
1792
1820
|
let data;
|
|
1793
1821
|
try {
|
|
1794
|
-
data = await request(
|
|
1822
|
+
data = await request(`${base}${revisionQuery}`);
|
|
1795
1823
|
} catch (err) {
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1824
|
+
if (revisionQuery === "" && isNotFound(err)) {
|
|
1825
|
+
try {
|
|
1826
|
+
data = await request(`${base}?revision=draft`);
|
|
1827
|
+
} catch (draftErr) {
|
|
1828
|
+
return {
|
|
1829
|
+
name,
|
|
1830
|
+
status: "failed",
|
|
1831
|
+
message: draftErr instanceof Error ? draftErr.message : String(draftErr)
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
} else {
|
|
1835
|
+
return {
|
|
1836
|
+
name,
|
|
1837
|
+
status: "failed",
|
|
1838
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1801
1841
|
}
|
|
1802
1842
|
fs8.mkdirSync(componentDir, { recursive: true });
|
|
1803
1843
|
const manifestText = JSON.stringify(data.manifest, null, 2) + "\n";
|
|
1804
|
-
fs8.writeFileSync(
|
|
1805
|
-
fs8.writeFileSync(
|
|
1844
|
+
fs8.writeFileSync(path10.join(componentDir, "manifest.json"), manifestText);
|
|
1845
|
+
fs8.writeFileSync(path10.join(componentDir, "bundle.js"), data.bundle);
|
|
1806
1846
|
const wrote = ["manifest.json", "bundle.js"];
|
|
1807
1847
|
let sourceHash = "";
|
|
1808
1848
|
if (data.source !== null) {
|
|
1809
|
-
fs8.writeFileSync(
|
|
1849
|
+
fs8.writeFileSync(path10.join(componentDir, "component.tsx"), data.source);
|
|
1810
1850
|
wrote.push("component.tsx");
|
|
1811
1851
|
sourceHash = hashString(data.source);
|
|
1812
1852
|
}
|
|
@@ -1824,7 +1864,10 @@ async function pullOne(opts) {
|
|
|
1824
1864
|
return { name, status: "pulled", version: data.version, wrote };
|
|
1825
1865
|
}
|
|
1826
1866
|
function ensureComponentsDir(root) {
|
|
1827
|
-
fs8.mkdirSync(
|
|
1867
|
+
fs8.mkdirSync(path10.join(root, "components"), { recursive: true });
|
|
1868
|
+
}
|
|
1869
|
+
function isNotFound(err) {
|
|
1870
|
+
return err instanceof HttpError && err.status === 404;
|
|
1828
1871
|
}
|
|
1829
1872
|
function buildRevisionQuery(args) {
|
|
1830
1873
|
const version = flagString(args.flags, "version");
|
|
@@ -1859,12 +1902,12 @@ function printOutcome(outcome, force) {
|
|
|
1859
1902
|
|
|
1860
1903
|
// src/commands/apps/push.ts
|
|
1861
1904
|
import * as fs9 from "fs";
|
|
1862
|
-
import * as
|
|
1905
|
+
import * as path11 from "path";
|
|
1863
1906
|
async function pushCommand(args) {
|
|
1864
1907
|
const slug = requireProjectStore();
|
|
1865
1908
|
const root = process.cwd();
|
|
1866
1909
|
rejectLegacyLayout(root);
|
|
1867
|
-
const componentsDir =
|
|
1910
|
+
const componentsDir = path11.join(root, "components");
|
|
1868
1911
|
if (!fs9.existsSync(componentsDir)) {
|
|
1869
1912
|
throw new Error(
|
|
1870
1913
|
"No components/ directory here. Run `gs apps init <name>` to scaffold the project root and your first component."
|
|
@@ -1924,7 +1967,7 @@ ${summary.join(", ")} (of ${outcomes.length}) \u2192 ${slug}
|
|
|
1924
1967
|
}
|
|
1925
1968
|
async function pushOne(opts) {
|
|
1926
1969
|
const { slug, root, name, force } = opts;
|
|
1927
|
-
const componentDir =
|
|
1970
|
+
const componentDir = path11.join(root, "components", name);
|
|
1928
1971
|
if (!fs9.existsSync(componentDir)) {
|
|
1929
1972
|
return {
|
|
1930
1973
|
name,
|
|
@@ -1932,9 +1975,9 @@ async function pushOne(opts) {
|
|
|
1932
1975
|
message: `components/${name}/ does not exist`
|
|
1933
1976
|
};
|
|
1934
1977
|
}
|
|
1935
|
-
const manifestPath = opts.manifestPath ?
|
|
1936
|
-
const bundlePath = opts.bundlePath ?
|
|
1937
|
-
const sourcePath =
|
|
1978
|
+
const manifestPath = opts.manifestPath ? path11.resolve(opts.manifestPath) : path11.join(componentDir, "manifest.json");
|
|
1979
|
+
const bundlePath = opts.bundlePath ? path11.resolve(opts.bundlePath) : path11.join(componentDir, "bundle.js");
|
|
1980
|
+
const sourcePath = path11.join(componentDir, "component.tsx");
|
|
1938
1981
|
if (!fs9.existsSync(bundlePath)) {
|
|
1939
1982
|
return {
|
|
1940
1983
|
name,
|
|
@@ -2015,13 +2058,13 @@ ${OUTCOME_INDENT}`)
|
|
|
2015
2058
|
}
|
|
2016
2059
|
function listLocalComponents(componentsDir) {
|
|
2017
2060
|
return fs9.readdirSync(componentsDir).filter((entry) => {
|
|
2018
|
-
const dir =
|
|
2019
|
-
return fs9.statSync(dir).isDirectory() && fs9.existsSync(
|
|
2061
|
+
const dir = path11.join(componentsDir, entry);
|
|
2062
|
+
return fs9.statSync(dir).isDirectory() && fs9.existsSync(path11.join(dir, "manifest.json"));
|
|
2020
2063
|
}).sort();
|
|
2021
2064
|
}
|
|
2022
2065
|
function rejectLegacyLayout(root) {
|
|
2023
|
-
const rootManifest =
|
|
2024
|
-
const componentsDir =
|
|
2066
|
+
const rootManifest = path11.join(root, "manifest.json");
|
|
2067
|
+
const componentsDir = path11.join(root, "components");
|
|
2025
2068
|
if (fs9.existsSync(rootManifest) && !fs9.existsSync(componentsDir)) {
|
|
2026
2069
|
throw new Error(
|
|
2027
2070
|
[
|
|
@@ -2265,7 +2308,7 @@ function formatList(list) {
|
|
|
2265
2308
|
|
|
2266
2309
|
// src/commands/configure/set.ts
|
|
2267
2310
|
import * as fs10 from "fs";
|
|
2268
|
-
import * as
|
|
2311
|
+
import * as path12 from "path";
|
|
2269
2312
|
var TEXT_FIELDS = [
|
|
2270
2313
|
"displayName",
|
|
2271
2314
|
"assistantName",
|
|
@@ -2325,7 +2368,7 @@ async function setConfig(args) {
|
|
|
2325
2368
|
`);
|
|
2326
2369
|
}
|
|
2327
2370
|
function readTextFile(filePath, flag) {
|
|
2328
|
-
const resolved =
|
|
2371
|
+
const resolved = path12.resolve(filePath);
|
|
2329
2372
|
if (!fs10.existsSync(resolved)) {
|
|
2330
2373
|
throw new Error(`${flag}: file not found: ${resolved}`);
|
|
2331
2374
|
}
|
|
@@ -2346,7 +2389,7 @@ function parseThemeJson(raw, flag) {
|
|
|
2346
2389
|
|
|
2347
2390
|
// src/commands/configure/upload.ts
|
|
2348
2391
|
import * as fs11 from "fs";
|
|
2349
|
-
import * as
|
|
2392
|
+
import * as path13 from "path";
|
|
2350
2393
|
|
|
2351
2394
|
// src/commands/configure/shared.ts
|
|
2352
2395
|
var ASSET_KINDS = ["icon", "logoLight", "logoDark"];
|
|
@@ -2371,11 +2414,11 @@ async function uploadAsset(args) {
|
|
|
2371
2414
|
if (!filePath) {
|
|
2372
2415
|
throw new Error(`Usage: gs configure upload <${ASSET_KINDS.join("|")}> <file>`);
|
|
2373
2416
|
}
|
|
2374
|
-
const resolved =
|
|
2417
|
+
const resolved = path13.resolve(filePath);
|
|
2375
2418
|
if (!fs11.existsSync(resolved)) {
|
|
2376
2419
|
throw new Error(`File not found: ${resolved}`);
|
|
2377
2420
|
}
|
|
2378
|
-
const mime = MIME_BY_EXT[
|
|
2421
|
+
const mime = MIME_BY_EXT[path13.extname(resolved).toLowerCase()];
|
|
2379
2422
|
if (!mime) {
|
|
2380
2423
|
throw new Error("Unsupported image type. Use .png, .jpg, or .webp.");
|
|
2381
2424
|
}
|
|
@@ -2384,7 +2427,7 @@ async function uploadAsset(args) {
|
|
|
2384
2427
|
form.append(
|
|
2385
2428
|
"file",
|
|
2386
2429
|
new Blob([new Uint8Array(bytes)], { type: mime }),
|
|
2387
|
-
|
|
2430
|
+
path13.basename(resolved)
|
|
2388
2431
|
);
|
|
2389
2432
|
const url = `${adminApiBase(slug)}/configure/upload/${kind}`;
|
|
2390
2433
|
const data = await request(url, {
|
|
@@ -2704,7 +2747,7 @@ function recentChangelog(text, minItems = 15) {
|
|
|
2704
2747
|
// src/version-check.ts
|
|
2705
2748
|
import * as fs12 from "fs";
|
|
2706
2749
|
import * as os3 from "os";
|
|
2707
|
-
import * as
|
|
2750
|
+
import * as path14 from "path";
|
|
2708
2751
|
var REFRESH_COMMAND = "__refresh-version-cache";
|
|
2709
2752
|
var PKG = "@greatstore/cli";
|
|
2710
2753
|
var REGISTRY_URL = `https://registry.npmjs.org/${PKG}/latest`;
|
|
@@ -2765,7 +2808,7 @@ async function fetchLatest() {
|
|
|
2765
2808
|
}
|
|
2766
2809
|
}
|
|
2767
2810
|
function cachePath(home) {
|
|
2768
|
-
return
|
|
2811
|
+
return path14.join(home, ".greatstore", "version-check.json");
|
|
2769
2812
|
}
|
|
2770
2813
|
function readCache(home) {
|
|
2771
2814
|
try {
|
|
@@ -2778,11 +2821,11 @@ function readCache(home) {
|
|
|
2778
2821
|
}
|
|
2779
2822
|
return null;
|
|
2780
2823
|
}
|
|
2781
|
-
function writeCache(home,
|
|
2824
|
+
function writeCache(home, cache2) {
|
|
2782
2825
|
try {
|
|
2783
2826
|
const file = cachePath(home);
|
|
2784
|
-
fs12.mkdirSync(
|
|
2785
|
-
fs12.writeFileSync(file, JSON.stringify(
|
|
2827
|
+
fs12.mkdirSync(path14.dirname(file), { recursive: true });
|
|
2828
|
+
fs12.writeFileSync(file, JSON.stringify(cache2));
|
|
2786
2829
|
} catch {
|
|
2787
2830
|
}
|
|
2788
2831
|
}
|
|
@@ -2804,7 +2847,7 @@ function parseVer(v) {
|
|
|
2804
2847
|
}
|
|
2805
2848
|
|
|
2806
2849
|
// src/index.ts
|
|
2807
|
-
var CHANGELOG = true ? "# Changelog\n\nAll notable changes to `@greatstore/cli` are recorded here. The format\nfollows [Keep a Changelog](https://keepachangelog.com/).\n\n## 0.1.1 \u2014 2026-08-20\n\n### Changed\n- Signing in now asks you to authorize the store you picked before the CLI\n gets access to it, and that access can be ended anytime from **Team \u2192 CLI\n access** in the dashboard. Ending it there signs this computer out on its\n next command.\n- For each store, you're signed in on one computer at a time. Signing in to\n that store again from another machine replaces the previous one, which then\n has to sign in again; your other stores are unaffected.\n- This release is required: earlier versions no longer work. Run\n `npm install -g @greatstore/cli` to upgrade.\n\n## 0.1.0 \u2014 2026-08-19\n\n### Changed\n- When a component's `inputSchema` declares fields the component doesn't\n accept, validation now reports them on a single line \u2014 naming the fields\n (capped, with a `(+N more)` count past that) \u2014 instead of one error per\n field. A schema/component mismatch stays readable instead of burying the\n other diagnostics.\n\n## 0.0.48 \u2014 2026-08-19\n\n### Changed\n- Component validation now warns when a component reaches for the page-level\n `window.GreatStore` API. Components should interact with GreatStore through\n the lifecycle props passed into them (`onSendMessage`, `onCallTool`,\n `onGenerateStructuredContent`, \u2026), which are wired to the surface the\n component is mounted in; the warning points you there. It's a nudge, not a\n build failure.\n\n## 0.0.47 \u2014 2026-08-18\n\n### Changed\n- `onGenerateStructuredContent` now takes a required third argument,\n `fallback` \u2014 a schema-shaped object you supply. The component preview\n renders it (there's no live store to generate against there), so a\n generation-driven component previews as it would live. It's validated\n against the schema and throws if the two don't line up.\n\n## 0.0.46 \u2014 2026-08-18\n\n### Added\n- Components now receive an `onGenerateStructuredContent(schema, prompt)`\n prop: ask the store's assistant for content matching a JSON Schema (or a\n Zod schema) and render what it returns, personalized to the shopper. It\n works the same in the conversation on the storefront and every embed.\n Added to the component template, the scaffolded `AGENTS.md` reference, and\n prop validation.\n\n## 0.0.45 \u2014 2026-08-18\n\n### Changed\n- The auto-generated `AGENTS.md` banner now records the CLI version that\n wrote it, so you can tell at a glance when it trails your installed CLI\n and a `gs apps init` refresh is due.\n\n## 0.0.44 \u2014 2026-08-18\n\n### Added\n- New check: a component that still declares `onError` gets a warning\n pointing at the async-component pattern that replaced it, with the\n throw-vs-fallback caveat (a throw asks the assistant to retry, so\n permanent failures should render a fallback rather than throw). Earlier\n this surfaced as the generic \"prop isn't declared in the schema\"\n warning, whose suggested fix was wrong for a former lifecycle callback.\n\n### Changed\n- `gs apps init` now always refreshes `AGENTS.md` (the agent guidance\n file) so it tracks the installed CLI version instead of going stale.\n The file carries a \"do not edit \u2014 auto-generated\" banner; your own\n project files are still left untouched.\n\n## 0.0.43 \u2014 2026-08-18\n\n### Added\n- `gs apps build` and `gs apps push` now check each component before\n building or uploading it. Findings are reported as **errors** (a\n blocker \u2014 the component isn't built or uploaded) or **warnings** (it\n builds and is ready to publish, but something is worth improving),\n and a single run reports everything it found rather than stopping at\n the first problem.\n- New check: a component's props and its `inputSchema.properties` must\n agree. A schema field the component doesn't accept is an error; a prop\n the schema doesn't declare is a warning, since nothing will ever pass\n it. The props GreatStore injects (`onSendMessage`, `onCallTool`,\n `onUpdateModelContext`, `onShowLightbox`, `onClose`, `storeData`,\n `Image`) are exempt.\n\n## 0.0.42 \u2014 2026-08-15\n\n### Changed\n- `gs apps list` now works outside a project. With no `.gsrc` it lists the\n components of the store you're signed in to, instead of erroring. The\n commands that write files or change the store \u2014 `push`, `pull`, `publish`,\n `unpublish`, `delete` \u2014 still require a project.\n\n## 0.0.41 \u2014 2026-08-14\n\n### Changed\n- Internal authentication rework. Re-run `gs login` after updating.\n\n## 0.0.40 \u2014 2026-07-24\n\n### Removed\n- `gs configure set` no longer accepts `--salesGuide` or\n `--salesGuideFile`, and `gs configure show` no longer lists the field.\n\n## 0.0.39 \u2014 2026-07-21\n\n### Added\n- Components now receive an `Image` prop \u2014 a drop-in for `<img>` that\n serves images at the size they're displayed. Render `<Image src=\u2026 />`\n instead of `<img>`; pass `Image={\"img\"}` to preview a component\n outside a store. Scaffolded into `gs apps init` and documented in\n `AGENTS.md`.\n\n## 0.0.38 \u2014 2026-07-13\n\n### Added\n- `gs login --store <slug>` skips the store picker and signs in\n directly to that store \u2014 fails immediately if your account doesn't\n have access to it, instead of falling back to the picker.\n\n## 0.0.37 \u2014 2026-07-10\n\n### Added\n- Signing in now ends by choosing which store to work on \u2014 skipped\n automatically when your account has exactly one. Commands default to\n that store, so `--store` is rarely needed anymore.\n- `gs switch [<slug>]` changes the working store without signing in\n again.\n- `gs apps init` no longer requires `--store` when your sign-in already\n selected a store.\n\n### Changed\n- A `--store` flag or project `.gsrc` that names a different store than\n the one you signed in to is now an error, so work can't accidentally\n target the wrong store. Run `gs switch` to change stores.\n\n## 0.0.33 \u2014 2026-07-10\n\n### Changed\n- `gs apps push` now rejects a component whose `inputSchema` is too\n complex for the assistant to call reliably: union keywords\n (`anyOf`/`oneOf`/`allOf`/`$ref`/`not`), more than 8 KB serialized, or\n more than 50 declared fields. Keep schemas to a small set of flat,\n single-type fields \u2014 one canonical name per concept \u2014 and handle\n aliases or edge cases in component code instead.\n\n## 0.0.32 \u2014 2026-06-18\n\n### Added\n- Chat components can expand an image into a full-screen, on-brand\n lightbox via a new `onShowLightbox({ src, originRect })` prop. Wire it\n to an image's `onClick` \u2014 pass the image URL and, for a smooth zoom,\n the clicked element's `getBoundingClientRect()`. Use it for product\n photos, swatches, or size charts the shopper may want to inspect up\n close, instead of building your own overlay.\n\n## 0.0.31 \u2014 2026-06-18\n\n### Added\n- Components can read a secondary brand font from `--font-secondary`,\n for a second layer of typography. Falls back to the primary font.\n\n### Changed\n- The brand font variable is now `--font-primary` (was `--font-sans`).\n\n## 0.0.30 \u2014 2026-06-17\n\n### Added\n- The agent skill documents a new way to give the assistant background\n context without sending a visible message:\n `window.GreatStore.updateModelContext(text)` on the page, and the\n matching `onUpdateModelContext(text)` prop inside a chat component. Use\n it to keep the assistant aware of what the shopper is doing \u2014 the\n product they're viewing, what's in their cart, the variant they just\n selected \u2014 so its replies stay on point. Each call replaces the previous\n value, and nothing renders in the chat.\n\n## 0.0.29 \u2014 2026-06-12\n\n### Fixed\n- The \"update available\" notice actually fires now. It previously raced\n a 1-second timeout against the npm registry and usually lost, so most\n installs never saw it. The notice is now served instantly from a local\n cache, refreshed in the background after each day's first invocation \u2014\n it can lag one run behind a release, but it no longer adds latency or\n goes silent on slow networks.\n\n## 0.0.28 \u2014 2026-06-12\n\n### Added\n- `gs configure` \u2014 view and edit the store configuration from the CLI:\n display name, assistant name, sales guide, store link, extra origins,\n theme, CSP host lists, and icon/logo uploads. Same fields and\n behaviour as the dashboard's Configure panel.\n- `gs connectors` \u2014 manage the store's MCP connectors: list, add (with\n a discovery probe before saving), remove, enable/disable, toggle the\n Maker MCP, and health-check. Same behaviour as the dashboard's\n Connectors panel.\n- `--store <slug>` on the new admin commands, so they work outside a\n scaffolded component project (a `.gsrc` is still used when present).\n- The agent skill gains a store-administration reference: coding agents\n can read the store's configuration and connectors to ground their\n work, self-serve additive changes like origin allowlists and CSP\n hosts (read-merge-write), and are told which changes need the\n merchant's go-ahead first.\n\n### Changed\n- Component commands now live under `gs apps` (`gs apps push`,\n `gs apps build`, \u2026), matching the dashboard's Apps panel. The old\n top-level forms keep working as aliases, so existing scripts and\n scaffolded projects are unaffected.\n- The skill's structured-content guide now teaches \"point, don't\n paste\": name the SKU/product/collection and let GreatStore research\n the catalog itself instead of inlining fetched specs; validate with\n `gs connectors` that a connector exists for the data a prompt or\n schema depends on (research can't exceed the wired-up connectors);\n and never put shopper data in prompts \u2014 GreatStore already knows the\n shopper, and identified shoppers get per-shopper cached responses.\n\n## 0.0.27 \u2014 2026-06-11\n\n### Changed\n- Skill code samples now carry an explicit reference-only disclaimer:\n coding agents are told to re-express the logic in the host repo's\n framework (React, Vue, Shopify Liquid, Svelte, \u2026) instead of\n retrofitting the framework-free samples as-is.\n\n## 0.0.26 \u2014 2026-06-11\n\n### Added\n- The agent skill gains a \"GreatStore launchers\" recipe: a horizontally\n scrollable row of AI-generated chips, each an engaging first-person\n question about the current page that's sent to the assistant on tap.\n\n### Changed\n- Skill recipes are now one file each under `recipes/`, indexed from\n SKILL.md by a table with description and use-case columns.\n\n## 0.0.25 \u2014 2026-06-11\n\n### Added\n- `gs skill` installs the GreatStore agent skill \u2014 a guide AI coding\n agents use to build with GreatStore: AI content for your own UI, chat\n entry points, page tools, custom in-chat components, push\n notifications, and the store's MCP endpoints. Installs into\n `./.claude/skills/`; use `--global` for `~/.claude/skills/`, or\n `--dir <path>` for agents that read skills from somewhere else. Run\n it again any time to update an installed copy.\n\n## 0.0.24 \u2014 2026-06-03\n\n### Changed\n- `gs init` in an existing project now fills in any scaffold files that\n are missing (for example, the `AGENTS.md` design guide added in\n 0.0.23) and leaves your own files alone. Pass `--force` to refresh\n every scaffold file to the latest version. Your pinned store\n (`.gsrc`) is never rewritten either way.\n\n## 0.0.23 \u2014 2026-06-03\n\n### Added\n- `gs init` now scaffolds an `AGENTS.md` (with `CLAUDE.md` and\n `GEMINI.md` symlinked to it) documenting the design rules every\n component should follow \u2014 use `em` rather than `rem` for sizing, and\n style from the provided brand CSS variables so components match the\n store's theme. It doubles as guidance for AI coding agents.\n\n## 0.0.22 \u2014 2026-06-02\n\n### Added\n- `gs list` now shows a link to each component's page in the dashboard,\n so you can jump straight to a component to preview or publish it. The\n link is also included in `gs list --json`.\n\n## 0.0.21 \u2014 2026-05-31\n\n### Changed\n- The `gs init` component scaffold now shows how to write **async**\n components that load data before they render \u2014 including validating\n inputs up front and signalling a failure by throwing. The scaffolded\n component no longer includes an `onError` prop; throw from an async\n component to report a failure instead.\n\n## 0.0.20 \u2014 2026-05-29\n\n### Added\n- `gs pull`, `gs push`, and `gs publish` now accept several component\n names at once (e.g. `gs publish header footer cart`). Each component\n is reported on its own line and one failure no longer stops the rest.\n\n## 0.0.19 \u2014 2026-05-28\n\n### Fixed\n- `gs login` on Windows no longer opens a sign-in URL with parameters\n stripped, which surfaced as a \"Missing redirect_uri or state\n parameter\" page in the browser.\n\n## 0.0.18 \u2014 2026-05-28\n\n### Changed\n- Push and publish errors now name the specific reason \u2014 including\n every failing field in `manifest.json` \u2014 instead of the previous\n generic message.\n\n## 0.0.17 \u2014 2026-05-28\n\n### Added\n- Each command now prints a one-line upgrade notice when a newer\n `@greatstore/cli` is available on npm.\n\n## 0.0.16 \u2014 2026-05-28\n\n### Changed\n- Simplified error messages.\n\n## 0.0.15 \u2014 2026-05-24\n\n### Added\n- Scaffolded `component.tsx` now declares the four injected lifecycle\n props (`onSendMessage`, `onCallTool`, `onClose`, `onError`) on\n `Props`. Use `onError(message)` to report expected failures (failed\n fetch, host action rejected, invalid host state) so the AI can\n recover on its next turn. Render-time crashes are reported for you.\n\n## 0.0.14 \u2014 2026-05-24\n\n### Changed\n- `gs build` output is now whitespace-minified \u2014 typically ~50% smaller.\n\n## 0.0.13 \u2014 2026-05-24\n\n### Changed\n- `gs build` prints the next-step hint (`gs push`, then `gs publish`).\n\n## 0.0.12 \u2014 2026-05-24\n\n### Fixed\n- `gs build` failing to load Vite in some setups.\n\n## 0.0.11 \u2014 2026-05-24\n\n### Added\n- Multi-component projects. `gs init` (no args) scaffolds the project\n root; `gs init <name>` adds a component under `components/<name>/`.\n- `gs build [<name>]` \u2014 compiles every `components/<name>/bundle.js`.\n- `gs push` (no args) uploads only the components that changed.\n- `gs pull` (no args, or `*`) downloads every component. Locally\n edited components are skipped; pass `--force` to overwrite.\n- Public `CHANGELOG.md`; `gs --version` prints recent entries.\n\n### Changed\n- A project folder ships to exactly one store. Only `gs init` accepts\n `--store`; every other command reads the slug from `.gsrc`. The old\n single-component layout is rejected with a migration hint.\n- `gs init` requires `--store <slug>` for a fresh root, and rejects\n `--store` on an existing root.\n- `gs init` no longer writes `build.mjs` \u2014 scripts call `gs build`.\n\n## 0.0.10 \u2014 2026-05-23\n\n### Changed\n- The sign-in browser tab auto-closes once `gs login` finishes.\n\n## 0.0.9 \u2014 2026-05-23\n\n### Changed\n- Scaffolded manifests include a `displayName` so the admin UI has a\n friendlier label.\n\n## 0.0.8 \u2014 2026-05-23\n\n### Changed\n- `gs push` and `gs publish` print a link to view the component.\n\n## 0.0.6 \u2014 2026-05-23\n\n### Changed\n- `gs --version` reads from the published package version.\n\n## 0.0.4 \u2014 2026-05-23\n\n### Changed\n- Scaffolded projects produce browser-ready bundles out of the box.\n\n## 0.0.3 \u2014 2026-05-23\n\n### Changed\n- Trimmed public README to the essentials.\n\n## 0.0.2 \u2014 2026-05-23\n\n### Fixed\n- Sign-in callback parameter handling.\n" : "";
|
|
2850
|
+
var CHANGELOG = true ? "# Changelog\n\nAll notable changes to `@greatstore/cli` are recorded here. The format\nfollows [Keep a Changelog](https://keepachangelog.com/).\n\n## 0.1.3 \u2014 2026-09-12\n\n### Changed\n- `gs skill` prints the steps for installing the GreatStore agent skill\n instead of installing it itself, so any AI coding agent can set it up in\n whichever skills directory it uses \u2014 no `--global` or `--dir` to pick.\n- The installed skill is a link to the copy that ships with the CLI, so\n upgrading `@greatstore/cli` keeps it current with nothing to re-run.\n\n## 0.1.2 \u2014 2026-09-04\n\n### Fixed\n- `gs apps pull` now downloads components that aren't published yet. Pulling\n one used to report that it wasn't found on the server.\n\n## 0.1.1 \u2014 2026-08-20\n\n### Changed\n- Signing in now asks you to authorize the store you picked before the CLI\n gets access to it, and that access can be ended anytime from **Team \u2192 CLI\n access** in the dashboard. Ending it there signs this computer out on its\n next command.\n- For each store, you're signed in on one computer at a time. Signing in to\n that store again from another machine replaces the previous one, which then\n has to sign in again; your other stores are unaffected.\n- This release is required: earlier versions no longer work. Run\n `npm install -g @greatstore/cli` to upgrade.\n\n## 0.1.0 \u2014 2026-08-19\n\n### Changed\n- When a component's `inputSchema` declares fields the component doesn't\n accept, validation now reports them on a single line \u2014 naming the fields\n (capped, with a `(+N more)` count past that) \u2014 instead of one error per\n field. A schema/component mismatch stays readable instead of burying the\n other diagnostics.\n\n## 0.0.48 \u2014 2026-08-19\n\n### Changed\n- Component validation now warns when a component reaches for the page-level\n `window.GreatStore` API. Components should interact with GreatStore through\n the lifecycle props passed into them (`onSendMessage`, `onCallTool`,\n `onGenerateStructuredContent`, \u2026), which are wired to the surface the\n component is mounted in; the warning points you there. It's a nudge, not a\n build failure.\n\n## 0.0.47 \u2014 2026-08-18\n\n### Changed\n- `onGenerateStructuredContent` now takes a required third argument,\n `fallback` \u2014 a schema-shaped object you supply. The component preview\n renders it (there's no live store to generate against there), so a\n generation-driven component previews as it would live. It's validated\n against the schema and throws if the two don't line up.\n\n## 0.0.46 \u2014 2026-08-18\n\n### Added\n- Components now receive an `onGenerateStructuredContent(schema, prompt)`\n prop: ask the store's assistant for content matching a JSON Schema (or a\n Zod schema) and render what it returns, personalized to the shopper. It\n works the same in the conversation on the storefront and every embed.\n Added to the component template, the scaffolded `AGENTS.md` reference, and\n prop validation.\n\n## 0.0.45 \u2014 2026-08-18\n\n### Changed\n- The auto-generated `AGENTS.md` banner now records the CLI version that\n wrote it, so you can tell at a glance when it trails your installed CLI\n and a `gs apps init` refresh is due.\n\n## 0.0.44 \u2014 2026-08-18\n\n### Added\n- New check: a component that still declares `onError` gets a warning\n pointing at the async-component pattern that replaced it, with the\n throw-vs-fallback caveat (a throw asks the assistant to retry, so\n permanent failures should render a fallback rather than throw). Earlier\n this surfaced as the generic \"prop isn't declared in the schema\"\n warning, whose suggested fix was wrong for a former lifecycle callback.\n\n### Changed\n- `gs apps init` now always refreshes `AGENTS.md` (the agent guidance\n file) so it tracks the installed CLI version instead of going stale.\n The file carries a \"do not edit \u2014 auto-generated\" banner; your own\n project files are still left untouched.\n\n## 0.0.43 \u2014 2026-08-18\n\n### Added\n- `gs apps build` and `gs apps push` now check each component before\n building or uploading it. Findings are reported as **errors** (a\n blocker \u2014 the component isn't built or uploaded) or **warnings** (it\n builds and is ready to publish, but something is worth improving),\n and a single run reports everything it found rather than stopping at\n the first problem.\n- New check: a component's props and its `inputSchema.properties` must\n agree. A schema field the component doesn't accept is an error; a prop\n the schema doesn't declare is a warning, since nothing will ever pass\n it. The props GreatStore injects (`onSendMessage`, `onCallTool`,\n `onUpdateModelContext`, `onShowLightbox`, `onClose`, `storeData`,\n `Image`) are exempt.\n\n## 0.0.42 \u2014 2026-08-15\n\n### Changed\n- `gs apps list` now works outside a project. With no `.gsrc` it lists the\n components of the store you're signed in to, instead of erroring. The\n commands that write files or change the store \u2014 `push`, `pull`, `publish`,\n `unpublish`, `delete` \u2014 still require a project.\n\n## 0.0.41 \u2014 2026-08-14\n\n### Changed\n- Internal authentication rework. Re-run `gs login` after updating.\n\n## 0.0.40 \u2014 2026-07-24\n\n### Removed\n- `gs configure set` no longer accepts `--salesGuide` or\n `--salesGuideFile`, and `gs configure show` no longer lists the field.\n\n## 0.0.39 \u2014 2026-07-21\n\n### Added\n- Components now receive an `Image` prop \u2014 a drop-in for `<img>` that\n serves images at the size they're displayed. Render `<Image src=\u2026 />`\n instead of `<img>`; pass `Image={\"img\"}` to preview a component\n outside a store. Scaffolded into `gs apps init` and documented in\n `AGENTS.md`.\n\n## 0.0.38 \u2014 2026-07-13\n\n### Added\n- `gs login --store <slug>` skips the store picker and signs in\n directly to that store \u2014 fails immediately if your account doesn't\n have access to it, instead of falling back to the picker.\n\n## 0.0.37 \u2014 2026-07-10\n\n### Added\n- Signing in now ends by choosing which store to work on \u2014 skipped\n automatically when your account has exactly one. Commands default to\n that store, so `--store` is rarely needed anymore.\n- `gs switch [<slug>]` changes the working store without signing in\n again.\n- `gs apps init` no longer requires `--store` when your sign-in already\n selected a store.\n\n### Changed\n- A `--store` flag or project `.gsrc` that names a different store than\n the one you signed in to is now an error, so work can't accidentally\n target the wrong store. Run `gs switch` to change stores.\n\n## 0.0.33 \u2014 2026-07-10\n\n### Changed\n- `gs apps push` now rejects a component whose `inputSchema` is too\n complex for the assistant to call reliably: union keywords\n (`anyOf`/`oneOf`/`allOf`/`$ref`/`not`), more than 8 KB serialized, or\n more than 50 declared fields. Keep schemas to a small set of flat,\n single-type fields \u2014 one canonical name per concept \u2014 and handle\n aliases or edge cases in component code instead.\n\n## 0.0.32 \u2014 2026-06-18\n\n### Added\n- Chat components can expand an image into a full-screen, on-brand\n lightbox via a new `onShowLightbox({ src, originRect })` prop. Wire it\n to an image's `onClick` \u2014 pass the image URL and, for a smooth zoom,\n the clicked element's `getBoundingClientRect()`. Use it for product\n photos, swatches, or size charts the shopper may want to inspect up\n close, instead of building your own overlay.\n\n## 0.0.31 \u2014 2026-06-18\n\n### Added\n- Components can read a secondary brand font from `--font-secondary`,\n for a second layer of typography. Falls back to the primary font.\n\n### Changed\n- The brand font variable is now `--font-primary` (was `--font-sans`).\n\n## 0.0.30 \u2014 2026-06-17\n\n### Added\n- The agent skill documents a new way to give the assistant background\n context without sending a visible message:\n `window.GreatStore.updateModelContext(text)` on the page, and the\n matching `onUpdateModelContext(text)` prop inside a chat component. Use\n it to keep the assistant aware of what the shopper is doing \u2014 the\n product they're viewing, what's in their cart, the variant they just\n selected \u2014 so its replies stay on point. Each call replaces the previous\n value, and nothing renders in the chat.\n\n## 0.0.29 \u2014 2026-06-12\n\n### Fixed\n- The \"update available\" notice actually fires now. It previously raced\n a 1-second timeout against the npm registry and usually lost, so most\n installs never saw it. The notice is now served instantly from a local\n cache, refreshed in the background after each day's first invocation \u2014\n it can lag one run behind a release, but it no longer adds latency or\n goes silent on slow networks.\n\n## 0.0.28 \u2014 2026-06-12\n\n### Added\n- `gs configure` \u2014 view and edit the store configuration from the CLI:\n display name, assistant name, sales guide, store link, extra origins,\n theme, CSP host lists, and icon/logo uploads. Same fields and\n behaviour as the dashboard's Configure panel.\n- `gs connectors` \u2014 manage the store's MCP connectors: list, add (with\n a discovery probe before saving), remove, enable/disable, toggle the\n Maker MCP, and health-check. Same behaviour as the dashboard's\n Connectors panel.\n- `--store <slug>` on the new admin commands, so they work outside a\n scaffolded component project (a `.gsrc` is still used when present).\n- The agent skill gains a store-administration reference: coding agents\n can read the store's configuration and connectors to ground their\n work, self-serve additive changes like origin allowlists and CSP\n hosts (read-merge-write), and are told which changes need the\n merchant's go-ahead first.\n\n### Changed\n- Component commands now live under `gs apps` (`gs apps push`,\n `gs apps build`, \u2026), matching the dashboard's Apps panel. The old\n top-level forms keep working as aliases, so existing scripts and\n scaffolded projects are unaffected.\n- The skill's structured-content guide now teaches \"point, don't\n paste\": name the SKU/product/collection and let GreatStore research\n the catalog itself instead of inlining fetched specs; validate with\n `gs connectors` that a connector exists for the data a prompt or\n schema depends on (research can't exceed the wired-up connectors);\n and never put shopper data in prompts \u2014 GreatStore already knows the\n shopper, and identified shoppers get per-shopper cached responses.\n\n## 0.0.27 \u2014 2026-06-11\n\n### Changed\n- Skill code samples now carry an explicit reference-only disclaimer:\n coding agents are told to re-express the logic in the host repo's\n framework (React, Vue, Shopify Liquid, Svelte, \u2026) instead of\n retrofitting the framework-free samples as-is.\n\n## 0.0.26 \u2014 2026-06-11\n\n### Added\n- The agent skill gains a \"GreatStore launchers\" recipe: a horizontally\n scrollable row of AI-generated chips, each an engaging first-person\n question about the current page that's sent to the assistant on tap.\n\n### Changed\n- Skill recipes are now one file each under `recipes/`, indexed from\n SKILL.md by a table with description and use-case columns.\n\n## 0.0.25 \u2014 2026-06-11\n\n### Added\n- `gs skill` installs the GreatStore agent skill \u2014 a guide AI coding\n agents use to build with GreatStore: AI content for your own UI, chat\n entry points, page tools, custom in-chat components, push\n notifications, and the store's MCP endpoints. Installs into\n `./.claude/skills/`; use `--global` for `~/.claude/skills/`, or\n `--dir <path>` for agents that read skills from somewhere else. Run\n it again any time to update an installed copy.\n\n## 0.0.24 \u2014 2026-06-03\n\n### Changed\n- `gs init` in an existing project now fills in any scaffold files that\n are missing (for example, the `AGENTS.md` design guide added in\n 0.0.23) and leaves your own files alone. Pass `--force` to refresh\n every scaffold file to the latest version. Your pinned store\n (`.gsrc`) is never rewritten either way.\n\n## 0.0.23 \u2014 2026-06-03\n\n### Added\n- `gs init` now scaffolds an `AGENTS.md` (with `CLAUDE.md` and\n `GEMINI.md` symlinked to it) documenting the design rules every\n component should follow \u2014 use `em` rather than `rem` for sizing, and\n style from the provided brand CSS variables so components match the\n store's theme. It doubles as guidance for AI coding agents.\n\n## 0.0.22 \u2014 2026-06-02\n\n### Added\n- `gs list` now shows a link to each component's page in the dashboard,\n so you can jump straight to a component to preview or publish it. The\n link is also included in `gs list --json`.\n\n## 0.0.21 \u2014 2026-05-31\n\n### Changed\n- The `gs init` component scaffold now shows how to write **async**\n components that load data before they render \u2014 including validating\n inputs up front and signalling a failure by throwing. The scaffolded\n component no longer includes an `onError` prop; throw from an async\n component to report a failure instead.\n\n## 0.0.20 \u2014 2026-05-29\n\n### Added\n- `gs pull`, `gs push`, and `gs publish` now accept several component\n names at once (e.g. `gs publish header footer cart`). Each component\n is reported on its own line and one failure no longer stops the rest.\n\n## 0.0.19 \u2014 2026-05-28\n\n### Fixed\n- `gs login` on Windows no longer opens a sign-in URL with parameters\n stripped, which surfaced as a \"Missing redirect_uri or state\n parameter\" page in the browser.\n\n## 0.0.18 \u2014 2026-05-28\n\n### Changed\n- Push and publish errors now name the specific reason \u2014 including\n every failing field in `manifest.json` \u2014 instead of the previous\n generic message.\n\n## 0.0.17 \u2014 2026-05-28\n\n### Added\n- Each command now prints a one-line upgrade notice when a newer\n `@greatstore/cli` is available on npm.\n\n## 0.0.16 \u2014 2026-05-28\n\n### Changed\n- Simplified error messages.\n\n## 0.0.15 \u2014 2026-05-24\n\n### Added\n- Scaffolded `component.tsx` now declares the four injected lifecycle\n props (`onSendMessage`, `onCallTool`, `onClose`, `onError`) on\n `Props`. Use `onError(message)` to report expected failures (failed\n fetch, host action rejected, invalid host state) so the AI can\n recover on its next turn. Render-time crashes are reported for you.\n\n## 0.0.14 \u2014 2026-05-24\n\n### Changed\n- `gs build` output is now whitespace-minified \u2014 typically ~50% smaller.\n\n## 0.0.13 \u2014 2026-05-24\n\n### Changed\n- `gs build` prints the next-step hint (`gs push`, then `gs publish`).\n\n## 0.0.12 \u2014 2026-05-24\n\n### Fixed\n- `gs build` failing to load Vite in some setups.\n\n## 0.0.11 \u2014 2026-05-24\n\n### Added\n- Multi-component projects. `gs init` (no args) scaffolds the project\n root; `gs init <name>` adds a component under `components/<name>/`.\n- `gs build [<name>]` \u2014 compiles every `components/<name>/bundle.js`.\n- `gs push` (no args) uploads only the components that changed.\n- `gs pull` (no args, or `*`) downloads every component. Locally\n edited components are skipped; pass `--force` to overwrite.\n- Public `CHANGELOG.md`; `gs --version` prints recent entries.\n\n### Changed\n- A project folder ships to exactly one store. Only `gs init` accepts\n `--store`; every other command reads the slug from `.gsrc`. The old\n single-component layout is rejected with a migration hint.\n- `gs init` requires `--store <slug>` for a fresh root, and rejects\n `--store` on an existing root.\n- `gs init` no longer writes `build.mjs` \u2014 scripts call `gs build`.\n\n## 0.0.10 \u2014 2026-05-23\n\n### Changed\n- The sign-in browser tab auto-closes once `gs login` finishes.\n\n## 0.0.9 \u2014 2026-05-23\n\n### Changed\n- Scaffolded manifests include a `displayName` so the admin UI has a\n friendlier label.\n\n## 0.0.8 \u2014 2026-05-23\n\n### Changed\n- `gs push` and `gs publish` print a link to view the component.\n\n## 0.0.6 \u2014 2026-05-23\n\n### Changed\n- `gs --version` reads from the published package version.\n\n## 0.0.4 \u2014 2026-05-23\n\n### Changed\n- Scaffolded projects produce browser-ready bundles out of the box.\n\n## 0.0.3 \u2014 2026-05-23\n\n### Changed\n- Trimmed public README to the essentials.\n\n## 0.0.2 \u2014 2026-05-23\n\n### Fixed\n- Sign-in callback parameter handling.\n" : "";
|
|
2808
2851
|
var HELP = `gs \u2014 GreatStore CLI (v${CLI_VERSION})
|
|
2809
2852
|
|
|
2810
2853
|
Usage:
|
|
@@ -2834,7 +2877,8 @@ Store administration:
|
|
|
2834
2877
|
top level (\`gs push\` == \`gs apps push\`).
|
|
2835
2878
|
|
|
2836
2879
|
Agent tooling:
|
|
2837
|
-
skill
|
|
2880
|
+
skill Print how to install the GreatStore agent skill,
|
|
2881
|
+
for the AI coding agent that runs it.
|
|
2838
2882
|
|
|
2839
2883
|
Common flags:
|
|
2840
2884
|
--store <slug> Target store for admin commands (default: nearest .gsrc,
|
|
@@ -2848,8 +2892,6 @@ Common flags:
|
|
|
2848
2892
|
--bundle <path> Path to bundle for \`apps push\`.
|
|
2849
2893
|
--force Overwrite for \`apps init\` / \`apps pull\`; skip confirmation for \`apps delete\`.
|
|
2850
2894
|
--yes Skip confirmation for \`apps delete\`.
|
|
2851
|
-
--global \`skill\` only \u2014 install to ~/.claude/skills instead of ./.claude/skills.
|
|
2852
|
-
--dir <path> \`skill\` only \u2014 install into a custom skills directory.
|
|
2853
2895
|
-h, --help Show this help.
|
|
2854
2896
|
-v, --version Print version.
|
|
2855
2897
|
|
|
@@ -2915,7 +2957,7 @@ async function main() {
|
|
|
2915
2957
|
await connectorsCommand(parsed);
|
|
2916
2958
|
return 0;
|
|
2917
2959
|
case "skill":
|
|
2918
|
-
skillCommand(
|
|
2960
|
+
skillCommand();
|
|
2919
2961
|
return 0;
|
|
2920
2962
|
default:
|
|
2921
2963
|
process.stderr.write(`Unknown command: ${parsed.command}
|