@greatstore/cli 0.1.1 → 0.1.4
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 +32 -0
- package/README.md +15 -16
- package/dist/cli.js +422 -141
- 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 +131 -0
- package/dist/gs-skill/references/structured-content.md +252 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -442,7 +442,7 @@ async function sendOnce(url, options, accessToken) {
|
|
|
442
442
|
} catch {
|
|
443
443
|
}
|
|
444
444
|
const identifier = parsed.code ?? parsed.error;
|
|
445
|
-
const message = buildErrorMessage(res.status, identifier, parsed.detail);
|
|
445
|
+
const message = options.validationDetail && res.status === 400 && parsed.error ? parsed.error : buildErrorMessage(res.status, identifier, parsed.detail);
|
|
446
446
|
throw new HttpError(res.status, message, parsed.code ?? parsed.error);
|
|
447
447
|
}
|
|
448
448
|
var KNOWN_ERROR_MESSAGES = {
|
|
@@ -635,11 +635,247 @@ 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/configure/theme-fields.ts
|
|
715
|
+
var THEME_FIELDS = {
|
|
716
|
+
mode: {
|
|
717
|
+
kind: "string",
|
|
718
|
+
values: ["auto", "light", "dark", "custom"],
|
|
719
|
+
help: "Color scheme. `auto` follows the shopper's system preference.",
|
|
720
|
+
brandVisible: true
|
|
721
|
+
},
|
|
722
|
+
radius: {
|
|
723
|
+
kind: "string",
|
|
724
|
+
values: ["sharp", "default", "rounded"],
|
|
725
|
+
help: "Corner rounding preset applied to every surface.",
|
|
726
|
+
brandVisible: true
|
|
727
|
+
},
|
|
728
|
+
panelPosition: {
|
|
729
|
+
kind: "string",
|
|
730
|
+
values: ["left", "right", "middle"],
|
|
731
|
+
help: "Where the chat panel sits on desktop.",
|
|
732
|
+
brandVisible: true
|
|
733
|
+
},
|
|
734
|
+
fontFamily: {
|
|
735
|
+
kind: "string",
|
|
736
|
+
help: "Primary font family name.",
|
|
737
|
+
brandVisible: true
|
|
738
|
+
},
|
|
739
|
+
secondaryFontFamily: {
|
|
740
|
+
kind: "string",
|
|
741
|
+
help: "Font family for the assistant's chat bubbles.",
|
|
742
|
+
brandVisible: true
|
|
743
|
+
},
|
|
744
|
+
mobileBottomBar: {
|
|
745
|
+
kind: "boolean",
|
|
746
|
+
help: "Show the compact bottom bar on mobile.",
|
|
747
|
+
brandVisible: true
|
|
748
|
+
},
|
|
749
|
+
zIndex: {
|
|
750
|
+
kind: "integer",
|
|
751
|
+
help: "Stacking order of the chat panel on the host page."
|
|
752
|
+
},
|
|
753
|
+
brandColor: { kind: "color", help: "Accent color.", brandVisible: true },
|
|
754
|
+
surfaceColor: { kind: "color", help: "Background color.", brandVisible: true },
|
|
755
|
+
textColor: { kind: "color", help: "Text color.", brandVisible: true },
|
|
756
|
+
surface: { kind: "color", help: "Panel background." },
|
|
757
|
+
surfaceSecondary: { kind: "color", help: "Secondary background." },
|
|
758
|
+
surfaceAccent: { kind: "color", help: "Accented background." },
|
|
759
|
+
surfaceHover: { kind: "color", help: "Hovered background." },
|
|
760
|
+
foreground: { kind: "color", help: "Body text." },
|
|
761
|
+
foregroundSecondary: { kind: "color", help: "Secondary text." },
|
|
762
|
+
foregroundMuted: { kind: "color", help: "Muted text." },
|
|
763
|
+
foregroundAccent: { kind: "color", help: "Accented text." },
|
|
764
|
+
border: { kind: "color", help: "Border color." },
|
|
765
|
+
borderFocus: { kind: "color", help: "Focus-ring color." },
|
|
766
|
+
primary: { kind: "color", help: "Primary button background." },
|
|
767
|
+
primaryForeground: { kind: "color", help: "Primary button text." },
|
|
768
|
+
primaryHover: { kind: "color", help: "Primary button hover." },
|
|
769
|
+
primaryMuted: { kind: "color", help: "Muted primary background." },
|
|
770
|
+
primaryMutedForeground: { kind: "color", help: "Muted primary text." },
|
|
771
|
+
primaryTint: { kind: "color", help: "Primary tint wash." },
|
|
772
|
+
link: { kind: "color", help: "Link color." }
|
|
773
|
+
};
|
|
774
|
+
function suggestThemeFields(input) {
|
|
775
|
+
const target = input.toLowerCase();
|
|
776
|
+
return Object.keys(THEME_FIELDS).map((name) => {
|
|
777
|
+
const lower = name.toLowerCase();
|
|
778
|
+
return {
|
|
779
|
+
name,
|
|
780
|
+
distance: editDistance(target, lower),
|
|
781
|
+
prefix: commonPrefixLength(target, lower)
|
|
782
|
+
};
|
|
783
|
+
}).filter(
|
|
784
|
+
({ name, distance, prefix }) => distance <= Math.max(2, name.length / 3) || prefix >= 4
|
|
785
|
+
).sort((a, b) => b.prefix - a.prefix || a.distance - b.distance).slice(0, 3).map(({ name }) => name);
|
|
786
|
+
}
|
|
787
|
+
function commonPrefixLength(a, b) {
|
|
788
|
+
let i = 0;
|
|
789
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
790
|
+
return i;
|
|
791
|
+
}
|
|
792
|
+
function editDistance(a, b) {
|
|
793
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
794
|
+
for (let i = 1; i <= a.length; i++) {
|
|
795
|
+
const row = [i];
|
|
796
|
+
for (let j = 1; j <= b.length; j++) {
|
|
797
|
+
row[j] = Math.min(
|
|
798
|
+
(prev[j] ?? 0) + 1,
|
|
799
|
+
(row[j - 1] ?? 0) + 1,
|
|
800
|
+
(prev[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
prev = row;
|
|
804
|
+
}
|
|
805
|
+
return prev[b.length] ?? 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/commands/configure/help.ts
|
|
809
|
+
var CONFIGURE_HELP = `gs configure \u2014 view and edit a store's configuration.
|
|
810
|
+
|
|
811
|
+
Usage:
|
|
812
|
+
gs configure Show the current configuration.
|
|
813
|
+
gs configure set --<field> <value> Save one or more fields.
|
|
814
|
+
gs configure upload <kind> <file> Upload icon / logoLight / logoDark.
|
|
815
|
+
gs configure clear <kind> Remove an uploaded asset.
|
|
816
|
+
|
|
817
|
+
Run \`gs configure set --help\` for the full field list.
|
|
818
|
+
|
|
819
|
+
Flags:
|
|
820
|
+
--store <slug> Which store to act on (default: nearest .gsrc).
|
|
821
|
+
--json Machine-readable output.
|
|
822
|
+
`;
|
|
823
|
+
function themeFieldLines() {
|
|
824
|
+
const rows = Object.entries(THEME_FIELDS).map(([name, field]) => {
|
|
825
|
+
const type = field.values ? field.values.join(" | ") : field.kind === "color" ? "<css color>" : field.kind === "boolean" ? "true | false" : field.kind === "integer" ? "<integer>" : "<name>";
|
|
826
|
+
return { flag: ` --theme.${name} ${type}`, help: field.help };
|
|
827
|
+
});
|
|
828
|
+
const width = Math.max(...rows.map((r) => r.flag.length)) + 2;
|
|
829
|
+
return rows.map((r) => `${r.flag.padEnd(width)}${r.help}`).join("\n");
|
|
830
|
+
}
|
|
831
|
+
function brandVisibleNames() {
|
|
832
|
+
return Object.entries(THEME_FIELDS).filter(([, field]) => field.brandVisible).map(([name]) => name).join(", ");
|
|
833
|
+
}
|
|
834
|
+
function configureSetHelp() {
|
|
835
|
+
return `gs configure set \u2014 save configuration fields.
|
|
836
|
+
|
|
837
|
+
Only the fields you pass are written; everything else is left alone. Pass an
|
|
838
|
+
empty value to clear a field (e.g. --displayName "").
|
|
839
|
+
|
|
840
|
+
Store fields:
|
|
841
|
+
--displayName <text> Store name shown to shoppers.
|
|
842
|
+
--assistantName <text> What the assistant calls itself.
|
|
843
|
+
--storeLink <url> Where "visit the store" points.
|
|
844
|
+
--extraOrigins <a,b,c> Extra origins allowed to embed the widget.
|
|
845
|
+
--cspScriptHosts <a,b,c> Extra script hosts to allow.
|
|
846
|
+
--cspConnectHosts <a,b,c> Extra connect hosts to allow.
|
|
847
|
+
|
|
848
|
+
Theme fields (merge into the stored theme):
|
|
849
|
+
${themeFieldLines()}
|
|
850
|
+
|
|
851
|
+
Whole-object theme writes:
|
|
852
|
+
--theme '<json>' Merge this JSON object into the stored theme.
|
|
853
|
+
--themeFile <path> Same, read from a file.
|
|
854
|
+
--replace Replace the theme outright instead of merging.
|
|
855
|
+
--theme "" Clear the theme entirely.
|
|
856
|
+
|
|
857
|
+
These are shopper-visible, so confirm them with the merchant before writing:
|
|
858
|
+
${brandVisibleNames()}
|
|
859
|
+
|
|
860
|
+
Examples:
|
|
861
|
+
gs configure set --theme.fontFamily Manrope --theme.radius sharp
|
|
862
|
+
gs configure set --theme.brandColor "#1e1d66"
|
|
863
|
+
gs configure set --theme '{"mode":"dark"}' --replace
|
|
864
|
+
`;
|
|
865
|
+
}
|
|
866
|
+
function configureHelp(sub) {
|
|
867
|
+
if (sub === void 0 || sub === "show") return CONFIGURE_HELP;
|
|
868
|
+
if (sub === "set") return configureSetHelp();
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/commands/apps/init.ts
|
|
873
|
+
import * as fs4 from "fs";
|
|
874
|
+
import * as path5 from "path";
|
|
639
875
|
|
|
640
876
|
// src/template.ts
|
|
641
877
|
import { readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
642
|
-
import { fileURLToPath } from "url";
|
|
878
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
643
879
|
var cache = null;
|
|
644
880
|
function loadTemplate() {
|
|
645
881
|
if (cache) return cache;
|
|
@@ -668,58 +904,8 @@ function applyTemplate(content, vars) {
|
|
|
668
904
|
return out;
|
|
669
905
|
}
|
|
670
906
|
|
|
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
907
|
// src/version.ts
|
|
722
|
-
var CLI_VERSION = true ? "0.1.
|
|
908
|
+
var CLI_VERSION = true ? "0.1.4" : "0.0.0-dev";
|
|
723
909
|
|
|
724
910
|
// src/commands/apps/init.ts
|
|
725
911
|
var NAME_REGEX = /^[a-z][a-z0-9_]*$/;
|
|
@@ -731,7 +917,7 @@ function initCommand(args) {
|
|
|
731
917
|
const force = flagBool(args.flags, "force");
|
|
732
918
|
const storeFlag = flagString(args.flags, "store");
|
|
733
919
|
const outRel = flagString(args.flags, "out") ?? ".";
|
|
734
|
-
const root =
|
|
920
|
+
const root = path5.resolve(outRel);
|
|
735
921
|
const rootExisted = hasRootScaffold(root);
|
|
736
922
|
let store;
|
|
737
923
|
if (rootExisted) {
|
|
@@ -766,7 +952,7 @@ function initCommand(args) {
|
|
|
766
952
|
lines.push(
|
|
767
953
|
"",
|
|
768
954
|
"Next steps:",
|
|
769
|
-
` cd ${
|
|
955
|
+
` cd ${path5.relative(process.cwd(), root) || "."}`,
|
|
770
956
|
" npm install",
|
|
771
957
|
" gs apps init <component_name> # add your first component",
|
|
772
958
|
""
|
|
@@ -774,9 +960,9 @@ function initCommand(args) {
|
|
|
774
960
|
process.stdout.write(lines.join("\n"));
|
|
775
961
|
return;
|
|
776
962
|
}
|
|
777
|
-
const componentDir =
|
|
963
|
+
const componentDir = path5.join(root, "components", name);
|
|
778
964
|
ensureComponent(componentDir, name, force);
|
|
779
|
-
const projectLabel =
|
|
965
|
+
const projectLabel = path5.relative(process.cwd(), root) || ".";
|
|
780
966
|
process.stdout.write(
|
|
781
967
|
[
|
|
782
968
|
`Added component "${name}" at components/${name}/.`,
|
|
@@ -792,7 +978,7 @@ function initCommand(args) {
|
|
|
792
978
|
);
|
|
793
979
|
}
|
|
794
980
|
function hasRootScaffold(dir) {
|
|
795
|
-
return fs4.existsSync(
|
|
981
|
+
return fs4.existsSync(path5.join(dir, "package.json")) && fs4.existsSync(path5.join(dir, ".gsrc"));
|
|
796
982
|
}
|
|
797
983
|
function ensureRoot(root, rootExisted, force, opts) {
|
|
798
984
|
fs4.mkdirSync(root, { recursive: true });
|
|
@@ -806,15 +992,15 @@ function ensureRoot(root, rootExisted, force, opts) {
|
|
|
806
992
|
}
|
|
807
993
|
const files = rootExisted ? existingRootFiles() : rootFiles({ store: opts.store });
|
|
808
994
|
const written = writeRootFiles(root, files, force);
|
|
809
|
-
fs4.mkdirSync(
|
|
995
|
+
fs4.mkdirSync(path5.join(root, "components"), { recursive: true });
|
|
810
996
|
return written;
|
|
811
997
|
}
|
|
812
998
|
var CLI_OWNED_ROOT_FILES = /* @__PURE__ */ new Set(["AGENTS.md"]);
|
|
813
999
|
function writeRootFiles(root, files, force) {
|
|
814
1000
|
const written = [];
|
|
815
1001
|
for (const [relPath, content] of files) {
|
|
816
|
-
const full =
|
|
817
|
-
fs4.mkdirSync(
|
|
1002
|
+
const full = path5.join(root, relPath);
|
|
1003
|
+
fs4.mkdirSync(path5.dirname(full), { recursive: true });
|
|
818
1004
|
const exists = fs4.existsSync(full);
|
|
819
1005
|
if (!force && exists && !CLI_OWNED_ROOT_FILES.has(relPath)) continue;
|
|
820
1006
|
const changed = !exists || fs4.readFileSync(full, "utf8") !== content;
|
|
@@ -822,7 +1008,7 @@ function writeRootFiles(root, files, force) {
|
|
|
822
1008
|
if (changed) written.push(relPath);
|
|
823
1009
|
}
|
|
824
1010
|
for (const link of ["CLAUDE.md", "GEMINI.md"]) {
|
|
825
|
-
const linkPath =
|
|
1011
|
+
const linkPath = path5.join(root, link);
|
|
826
1012
|
if (force && pathExists(linkPath)) fs4.rmSync(linkPath);
|
|
827
1013
|
if (!pathExists(linkPath)) {
|
|
828
1014
|
fs4.symlinkSync("AGENTS.md", linkPath);
|
|
@@ -850,7 +1036,7 @@ function ensureComponent(componentDir, name, force) {
|
|
|
850
1036
|
}
|
|
851
1037
|
fs4.mkdirSync(componentDir, { recursive: true });
|
|
852
1038
|
for (const [relPath, content] of componentFiles(name)) {
|
|
853
|
-
fs4.writeFileSync(
|
|
1039
|
+
fs4.writeFileSync(path5.join(componentDir, relPath), content);
|
|
854
1040
|
}
|
|
855
1041
|
}
|
|
856
1042
|
function templateFiles(prefix, vars, exclude = /* @__PURE__ */ new Set()) {
|
|
@@ -885,16 +1071,16 @@ function pascal(name) {
|
|
|
885
1071
|
|
|
886
1072
|
// src/commands/apps/build.ts
|
|
887
1073
|
import * as fs6 from "fs";
|
|
888
|
-
import * as
|
|
1074
|
+
import * as path8 from "path";
|
|
889
1075
|
import { createRequire as createRequire2 } from "module";
|
|
890
1076
|
|
|
891
1077
|
// src/validation/context.ts
|
|
892
1078
|
import * as fs5 from "fs";
|
|
893
|
-
import * as
|
|
1079
|
+
import * as path7 from "path";
|
|
894
1080
|
|
|
895
1081
|
// src/validation/ast.ts
|
|
896
1082
|
import { createRequire } from "module";
|
|
897
|
-
import * as
|
|
1083
|
+
import * as path6 from "path";
|
|
898
1084
|
function analyzeComponentProps(root, sourcePath, sourceText) {
|
|
899
1085
|
const ts = loadTypeScript(root);
|
|
900
1086
|
if (!ts) {
|
|
@@ -922,7 +1108,7 @@ function analyzeComponentProps(root, sourcePath, sourceText) {
|
|
|
922
1108
|
} catch (err) {
|
|
923
1109
|
return {
|
|
924
1110
|
ok: false,
|
|
925
|
-
reason: `couldn't check props against the manifest \u2014 ${
|
|
1111
|
+
reason: `couldn't check props against the manifest \u2014 ${path6.basename(sourcePath)} could not be parsed (${err.message})`
|
|
926
1112
|
};
|
|
927
1113
|
}
|
|
928
1114
|
}
|
|
@@ -966,7 +1152,7 @@ function isGlobalObject(ts, node) {
|
|
|
966
1152
|
return ts.isIdentifier(node) && (node.text === "window" || node.text === "globalThis");
|
|
967
1153
|
}
|
|
968
1154
|
function loadTypeScript(root) {
|
|
969
|
-
const localRequire = createRequire(
|
|
1155
|
+
const localRequire = createRequire(path6.join(root, "package.json"));
|
|
970
1156
|
try {
|
|
971
1157
|
return localRequire("typescript");
|
|
972
1158
|
} catch {
|
|
@@ -1124,9 +1310,9 @@ function mergeProps(parts) {
|
|
|
1124
1310
|
// src/validation/context.ts
|
|
1125
1311
|
function buildContext(paths) {
|
|
1126
1312
|
const { root, name } = paths;
|
|
1127
|
-
const componentDir =
|
|
1128
|
-
const manifestPath = paths.manifestPath ??
|
|
1129
|
-
const sourcePath = paths.sourcePath ??
|
|
1313
|
+
const componentDir = path7.join(root, "components", name);
|
|
1314
|
+
const manifestPath = paths.manifestPath ?? path7.join(componentDir, "manifest.json");
|
|
1315
|
+
const sourcePath = paths.sourcePath ?? path7.join(componentDir, "component.tsx");
|
|
1130
1316
|
const manifestFile = relative4(root, manifestPath);
|
|
1131
1317
|
const sourceFile = relative4(root, sourcePath);
|
|
1132
1318
|
const diagnostics = [];
|
|
@@ -1184,8 +1370,8 @@ function readIfPresent(file) {
|
|
|
1184
1370
|
}
|
|
1185
1371
|
}
|
|
1186
1372
|
function relative4(root, target) {
|
|
1187
|
-
const rel =
|
|
1188
|
-
return rel.startsWith("..") ? target : rel.split(
|
|
1373
|
+
const rel = path7.relative(root, target);
|
|
1374
|
+
return rel.startsWith("..") ? target : rel.split(path7.sep).join("/");
|
|
1189
1375
|
}
|
|
1190
1376
|
|
|
1191
1377
|
// src/validation/injected-props.ts
|
|
@@ -1282,24 +1468,24 @@ function schemaFieldNames(inputSchema) {
|
|
|
1282
1468
|
var MAX_SCHEMA_BYTES = 8 * 1024;
|
|
1283
1469
|
var MAX_SCHEMA_FIELDS = 50;
|
|
1284
1470
|
var UNION_KEYWORDS = ["anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
1285
|
-
function measure(node,
|
|
1471
|
+
function measure(node, path15, acc) {
|
|
1286
1472
|
if (Array.isArray(node)) {
|
|
1287
|
-
node.forEach((entry, i) => measure(entry, `${
|
|
1473
|
+
node.forEach((entry, i) => measure(entry, `${path15}[${i}]`, acc));
|
|
1288
1474
|
return;
|
|
1289
1475
|
}
|
|
1290
1476
|
if (!node || typeof node !== "object") return;
|
|
1291
1477
|
const record = node;
|
|
1292
1478
|
for (const keyword of UNION_KEYWORDS) {
|
|
1293
|
-
if (keyword in record) acc.unionPaths.push(`${
|
|
1479
|
+
if (keyword in record) acc.unionPaths.push(`${path15}.${keyword}`);
|
|
1294
1480
|
}
|
|
1295
1481
|
const properties = record["properties"];
|
|
1296
1482
|
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
1297
1483
|
for (const [name, sub] of Object.entries(properties)) {
|
|
1298
1484
|
acc.fields += 1;
|
|
1299
|
-
measure(sub, `${
|
|
1485
|
+
measure(sub, `${path15}.${name}`, acc);
|
|
1300
1486
|
}
|
|
1301
1487
|
}
|
|
1302
|
-
if ("items" in record) measure(record["items"], `${
|
|
1488
|
+
if ("items" in record) measure(record["items"], `${path15}[]`, acc);
|
|
1303
1489
|
}
|
|
1304
1490
|
function inputSchemaComplexityRule(ctx) {
|
|
1305
1491
|
const inputSchema = ctx.manifest?.inputSchema;
|
|
@@ -1393,7 +1579,7 @@ function validateComponent(paths) {
|
|
|
1393
1579
|
var COMPONENTS_DIR = "components";
|
|
1394
1580
|
async function buildCommand(args) {
|
|
1395
1581
|
const root = process.cwd();
|
|
1396
|
-
if (!fs6.existsSync(
|
|
1582
|
+
if (!fs6.existsSync(path8.join(root, "package.json")) || !fs6.existsSync(path8.join(root, COMPONENTS_DIR))) {
|
|
1397
1583
|
throw new Error(
|
|
1398
1584
|
`\`gs apps build\` must run from a project root (contains \`package.json\` and \`${COMPONENTS_DIR}/\`). Current dir: ${root}`
|
|
1399
1585
|
);
|
|
@@ -1430,14 +1616,14 @@ Nothing built \u2014 fix the errors above.
|
|
|
1430
1616
|
}
|
|
1431
1617
|
const { build, reactPlugin, transformWithEsbuild } = await loadVite(root);
|
|
1432
1618
|
for (const name of buildable) {
|
|
1433
|
-
const dir =
|
|
1434
|
-
const bundlePath =
|
|
1619
|
+
const dir = path8.join(root, COMPONENTS_DIR, name);
|
|
1620
|
+
const bundlePath = path8.join(dir, "bundle.js");
|
|
1435
1621
|
await build({
|
|
1436
1622
|
plugins: [reactPlugin()],
|
|
1437
1623
|
logLevel: "warn",
|
|
1438
1624
|
build: {
|
|
1439
1625
|
lib: {
|
|
1440
|
-
entry:
|
|
1626
|
+
entry: path8.join(dir, "component.tsx"),
|
|
1441
1627
|
formats: ["es"],
|
|
1442
1628
|
fileName: () => "bundle.js"
|
|
1443
1629
|
},
|
|
@@ -1490,10 +1676,10 @@ Built locally \u2014 nothing uploaded yet. Next: \`gs apps push\` to upload, the
|
|
|
1490
1676
|
);
|
|
1491
1677
|
}
|
|
1492
1678
|
function listComponents(root) {
|
|
1493
|
-
const dir =
|
|
1679
|
+
const dir = path8.join(root, COMPONENTS_DIR);
|
|
1494
1680
|
return fs6.readdirSync(dir).filter((entry) => {
|
|
1495
|
-
const candidate =
|
|
1496
|
-
return fs6.statSync(candidate).isDirectory() && fs6.existsSync(
|
|
1681
|
+
const candidate = path8.join(dir, entry);
|
|
1682
|
+
return fs6.statSync(candidate).isDirectory() && fs6.existsSync(path8.join(candidate, "component.tsx"));
|
|
1497
1683
|
}).sort();
|
|
1498
1684
|
}
|
|
1499
1685
|
function formatBytes(n) {
|
|
@@ -1505,7 +1691,7 @@ function pctSmaller(before, after) {
|
|
|
1505
1691
|
return `${Math.round((1 - after / before) * 100)}%`;
|
|
1506
1692
|
}
|
|
1507
1693
|
async function loadVite(root) {
|
|
1508
|
-
const localRequire = createRequire2(
|
|
1694
|
+
const localRequire = createRequire2(path8.join(root, "package.json"));
|
|
1509
1695
|
const vitePath = resolveEsmEntry(localRequire, "vite");
|
|
1510
1696
|
if (!vitePath) {
|
|
1511
1697
|
throw new Error(
|
|
@@ -1553,7 +1739,7 @@ function resolveEsmEntry(req, specifier) {
|
|
|
1553
1739
|
}
|
|
1554
1740
|
const pkgJsonPath = findOwningPackageJson(anchor, specifier);
|
|
1555
1741
|
if (!pkgJsonPath) return null;
|
|
1556
|
-
const pkgDir =
|
|
1742
|
+
const pkgDir = path8.dirname(pkgJsonPath);
|
|
1557
1743
|
let pkg;
|
|
1558
1744
|
try {
|
|
1559
1745
|
pkg = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf8"));
|
|
@@ -1563,12 +1749,12 @@ function resolveEsmEntry(req, specifier) {
|
|
|
1563
1749
|
const fromExports = pickImportEntry(pkg.exports);
|
|
1564
1750
|
const entry = fromExports ?? (typeof pkg.module === "string" ? pkg.module : null) ?? (typeof pkg.main === "string" ? pkg.main : null);
|
|
1565
1751
|
if (!entry) return null;
|
|
1566
|
-
return
|
|
1752
|
+
return path8.resolve(pkgDir, entry);
|
|
1567
1753
|
}
|
|
1568
1754
|
function findOwningPackageJson(start, specifier) {
|
|
1569
|
-
let dir =
|
|
1755
|
+
let dir = path8.dirname(start);
|
|
1570
1756
|
while (true) {
|
|
1571
|
-
const candidate =
|
|
1757
|
+
const candidate = path8.join(dir, "package.json");
|
|
1572
1758
|
if (fs6.existsSync(candidate)) {
|
|
1573
1759
|
try {
|
|
1574
1760
|
const parsed = JSON.parse(fs6.readFileSync(candidate, "utf8"));
|
|
@@ -1576,7 +1762,7 @@ function findOwningPackageJson(start, specifier) {
|
|
|
1576
1762
|
} catch {
|
|
1577
1763
|
}
|
|
1578
1764
|
}
|
|
1579
|
-
const parent =
|
|
1765
|
+
const parent = path8.dirname(dir);
|
|
1580
1766
|
if (parent === dir) return null;
|
|
1581
1767
|
dir = parent;
|
|
1582
1768
|
}
|
|
@@ -1637,12 +1823,12 @@ function pad(s, width) {
|
|
|
1637
1823
|
|
|
1638
1824
|
// src/commands/apps/pull.ts
|
|
1639
1825
|
import * as fs8 from "fs";
|
|
1640
|
-
import * as
|
|
1826
|
+
import * as path10 from "path";
|
|
1641
1827
|
|
|
1642
1828
|
// src/sync.ts
|
|
1643
1829
|
import * as crypto2 from "crypto";
|
|
1644
1830
|
import * as fs7 from "fs";
|
|
1645
|
-
import * as
|
|
1831
|
+
import * as path9 from "path";
|
|
1646
1832
|
var SYNC_FILE = ".gssync.json";
|
|
1647
1833
|
function hashString(text) {
|
|
1648
1834
|
return crypto2.createHash("sha256").update(text).digest("hex");
|
|
@@ -1652,17 +1838,17 @@ function hashFile(filePath) {
|
|
|
1652
1838
|
return hashString(fs7.readFileSync(filePath, "utf8"));
|
|
1653
1839
|
}
|
|
1654
1840
|
function computeComponentHashes(componentDir) {
|
|
1655
|
-
const manifestHash = hashFile(
|
|
1841
|
+
const manifestHash = hashFile(path9.join(componentDir, "manifest.json"));
|
|
1656
1842
|
if (manifestHash === null) {
|
|
1657
1843
|
throw new Error(`Missing manifest.json in ${componentDir}`);
|
|
1658
1844
|
}
|
|
1659
1845
|
return {
|
|
1660
1846
|
manifestHash,
|
|
1661
|
-
sourceHash: hashFile(
|
|
1847
|
+
sourceHash: hashFile(path9.join(componentDir, "component.tsx"))
|
|
1662
1848
|
};
|
|
1663
1849
|
}
|
|
1664
1850
|
function readSyncState(componentDir) {
|
|
1665
|
-
const file =
|
|
1851
|
+
const file = path9.join(componentDir, SYNC_FILE);
|
|
1666
1852
|
if (!fs7.existsSync(file)) return null;
|
|
1667
1853
|
try {
|
|
1668
1854
|
const parsed = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
@@ -1680,7 +1866,7 @@ function readSyncState(componentDir) {
|
|
|
1680
1866
|
}
|
|
1681
1867
|
function writeSyncState(componentDir, state) {
|
|
1682
1868
|
fs7.writeFileSync(
|
|
1683
|
-
|
|
1869
|
+
path9.join(componentDir, SYNC_FILE),
|
|
1684
1870
|
JSON.stringify(state, null, 2) + "\n"
|
|
1685
1871
|
);
|
|
1686
1872
|
}
|
|
@@ -1721,7 +1907,7 @@ async function pullCommand(args) {
|
|
|
1721
1907
|
for (const name of targets) {
|
|
1722
1908
|
const outcome = await pullOne({
|
|
1723
1909
|
slug,
|
|
1724
|
-
componentDir:
|
|
1910
|
+
componentDir: path10.join(root, "components", name),
|
|
1725
1911
|
name,
|
|
1726
1912
|
revisionQuery,
|
|
1727
1913
|
force
|
|
@@ -1750,12 +1936,12 @@ async function pullAll(opts) {
|
|
|
1750
1936
|
}
|
|
1751
1937
|
const outcomes = [];
|
|
1752
1938
|
for (const entry of list.components) {
|
|
1753
|
-
const componentDir =
|
|
1939
|
+
const componentDir = path10.join(root, "components", entry.name);
|
|
1754
1940
|
const outcome = await pullOne({
|
|
1755
1941
|
slug,
|
|
1756
1942
|
componentDir,
|
|
1757
1943
|
name: entry.name,
|
|
1758
|
-
revisionQuery,
|
|
1944
|
+
revisionQuery: revisionQuery === "" && entry.live === null ? "?revision=draft" : revisionQuery,
|
|
1759
1945
|
force
|
|
1760
1946
|
});
|
|
1761
1947
|
outcomes.push(outcome);
|
|
@@ -1788,25 +1974,37 @@ async function pullOne(opts) {
|
|
|
1788
1974
|
};
|
|
1789
1975
|
}
|
|
1790
1976
|
}
|
|
1791
|
-
const
|
|
1977
|
+
const base = `${apiBaseFor(slug)}/api/apps/components/${encodeURIComponent(name)}`;
|
|
1792
1978
|
let data;
|
|
1793
1979
|
try {
|
|
1794
|
-
data = await request(
|
|
1980
|
+
data = await request(`${base}${revisionQuery}`);
|
|
1795
1981
|
} catch (err) {
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1982
|
+
if (revisionQuery === "" && isNotFound(err)) {
|
|
1983
|
+
try {
|
|
1984
|
+
data = await request(`${base}?revision=draft`);
|
|
1985
|
+
} catch (draftErr) {
|
|
1986
|
+
return {
|
|
1987
|
+
name,
|
|
1988
|
+
status: "failed",
|
|
1989
|
+
message: draftErr instanceof Error ? draftErr.message : String(draftErr)
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
} else {
|
|
1993
|
+
return {
|
|
1994
|
+
name,
|
|
1995
|
+
status: "failed",
|
|
1996
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1801
1999
|
}
|
|
1802
2000
|
fs8.mkdirSync(componentDir, { recursive: true });
|
|
1803
2001
|
const manifestText = JSON.stringify(data.manifest, null, 2) + "\n";
|
|
1804
|
-
fs8.writeFileSync(
|
|
1805
|
-
fs8.writeFileSync(
|
|
2002
|
+
fs8.writeFileSync(path10.join(componentDir, "manifest.json"), manifestText);
|
|
2003
|
+
fs8.writeFileSync(path10.join(componentDir, "bundle.js"), data.bundle);
|
|
1806
2004
|
const wrote = ["manifest.json", "bundle.js"];
|
|
1807
2005
|
let sourceHash = "";
|
|
1808
2006
|
if (data.source !== null) {
|
|
1809
|
-
fs8.writeFileSync(
|
|
2007
|
+
fs8.writeFileSync(path10.join(componentDir, "component.tsx"), data.source);
|
|
1810
2008
|
wrote.push("component.tsx");
|
|
1811
2009
|
sourceHash = hashString(data.source);
|
|
1812
2010
|
}
|
|
@@ -1824,7 +2022,10 @@ async function pullOne(opts) {
|
|
|
1824
2022
|
return { name, status: "pulled", version: data.version, wrote };
|
|
1825
2023
|
}
|
|
1826
2024
|
function ensureComponentsDir(root) {
|
|
1827
|
-
fs8.mkdirSync(
|
|
2025
|
+
fs8.mkdirSync(path10.join(root, "components"), { recursive: true });
|
|
2026
|
+
}
|
|
2027
|
+
function isNotFound(err) {
|
|
2028
|
+
return err instanceof HttpError && err.status === 404;
|
|
1828
2029
|
}
|
|
1829
2030
|
function buildRevisionQuery(args) {
|
|
1830
2031
|
const version = flagString(args.flags, "version");
|
|
@@ -1859,12 +2060,12 @@ function printOutcome(outcome, force) {
|
|
|
1859
2060
|
|
|
1860
2061
|
// src/commands/apps/push.ts
|
|
1861
2062
|
import * as fs9 from "fs";
|
|
1862
|
-
import * as
|
|
2063
|
+
import * as path11 from "path";
|
|
1863
2064
|
async function pushCommand(args) {
|
|
1864
2065
|
const slug = requireProjectStore();
|
|
1865
2066
|
const root = process.cwd();
|
|
1866
2067
|
rejectLegacyLayout(root);
|
|
1867
|
-
const componentsDir =
|
|
2068
|
+
const componentsDir = path11.join(root, "components");
|
|
1868
2069
|
if (!fs9.existsSync(componentsDir)) {
|
|
1869
2070
|
throw new Error(
|
|
1870
2071
|
"No components/ directory here. Run `gs apps init <name>` to scaffold the project root and your first component."
|
|
@@ -1924,7 +2125,7 @@ ${summary.join(", ")} (of ${outcomes.length}) \u2192 ${slug}
|
|
|
1924
2125
|
}
|
|
1925
2126
|
async function pushOne(opts) {
|
|
1926
2127
|
const { slug, root, name, force } = opts;
|
|
1927
|
-
const componentDir =
|
|
2128
|
+
const componentDir = path11.join(root, "components", name);
|
|
1928
2129
|
if (!fs9.existsSync(componentDir)) {
|
|
1929
2130
|
return {
|
|
1930
2131
|
name,
|
|
@@ -1932,9 +2133,9 @@ async function pushOne(opts) {
|
|
|
1932
2133
|
message: `components/${name}/ does not exist`
|
|
1933
2134
|
};
|
|
1934
2135
|
}
|
|
1935
|
-
const manifestPath = opts.manifestPath ?
|
|
1936
|
-
const bundlePath = opts.bundlePath ?
|
|
1937
|
-
const sourcePath =
|
|
2136
|
+
const manifestPath = opts.manifestPath ? path11.resolve(opts.manifestPath) : path11.join(componentDir, "manifest.json");
|
|
2137
|
+
const bundlePath = opts.bundlePath ? path11.resolve(opts.bundlePath) : path11.join(componentDir, "bundle.js");
|
|
2138
|
+
const sourcePath = path11.join(componentDir, "component.tsx");
|
|
1938
2139
|
if (!fs9.existsSync(bundlePath)) {
|
|
1939
2140
|
return {
|
|
1940
2141
|
name,
|
|
@@ -2015,13 +2216,13 @@ ${OUTCOME_INDENT}`)
|
|
|
2015
2216
|
}
|
|
2016
2217
|
function listLocalComponents(componentsDir) {
|
|
2017
2218
|
return fs9.readdirSync(componentsDir).filter((entry) => {
|
|
2018
|
-
const dir =
|
|
2019
|
-
return fs9.statSync(dir).isDirectory() && fs9.existsSync(
|
|
2219
|
+
const dir = path11.join(componentsDir, entry);
|
|
2220
|
+
return fs9.statSync(dir).isDirectory() && fs9.existsSync(path11.join(dir, "manifest.json"));
|
|
2020
2221
|
}).sort();
|
|
2021
2222
|
}
|
|
2022
2223
|
function rejectLegacyLayout(root) {
|
|
2023
|
-
const rootManifest =
|
|
2024
|
-
const componentsDir =
|
|
2224
|
+
const rootManifest = path11.join(root, "manifest.json");
|
|
2225
|
+
const componentsDir = path11.join(root, "components");
|
|
2025
2226
|
if (fs9.existsSync(rootManifest) && !fs9.existsSync(componentsDir)) {
|
|
2026
2227
|
throw new Error(
|
|
2027
2228
|
[
|
|
@@ -2265,7 +2466,8 @@ function formatList(list) {
|
|
|
2265
2466
|
|
|
2266
2467
|
// src/commands/configure/set.ts
|
|
2267
2468
|
import * as fs10 from "fs";
|
|
2268
|
-
import * as
|
|
2469
|
+
import * as path12 from "path";
|
|
2470
|
+
var THEME_FLAG_PREFIX = "theme.";
|
|
2269
2471
|
var TEXT_FIELDS = [
|
|
2270
2472
|
"displayName",
|
|
2271
2473
|
"assistantName",
|
|
@@ -2295,17 +2497,36 @@ async function setConfig(args) {
|
|
|
2295
2497
|
body[field] = entries.length === 0 ? null : entries;
|
|
2296
2498
|
}
|
|
2297
2499
|
const themeFile = flagString(args.flags, "themeFile");
|
|
2500
|
+
const perField = collectThemeFieldFlags(args.flags);
|
|
2501
|
+
const replace = flagBool(args.flags, "replace");
|
|
2298
2502
|
if ("theme" in args.flags && themeFile !== void 0) {
|
|
2299
2503
|
throw new Error("Pass either --theme or --themeFile, not both.");
|
|
2300
2504
|
}
|
|
2505
|
+
if (perField !== null && ("theme" in args.flags || themeFile !== void 0)) {
|
|
2506
|
+
throw new Error(
|
|
2507
|
+
"Pass either --theme.<field> flags or a whole --theme/--themeFile object, not both."
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
let themePatch;
|
|
2301
2511
|
if ("theme" in args.flags) {
|
|
2302
2512
|
const v = args.flags["theme"];
|
|
2303
2513
|
if (typeof v !== "string") {
|
|
2304
2514
|
throw new Error('--theme requires a JSON value (use --theme "" to clear).');
|
|
2305
2515
|
}
|
|
2306
|
-
|
|
2516
|
+
themePatch = v === "" ? null : parseThemeJson(v, "--theme");
|
|
2307
2517
|
} else if (themeFile !== void 0) {
|
|
2308
|
-
|
|
2518
|
+
themePatch = parseThemeJson(readTextFile(themeFile, "--themeFile"), "--themeFile");
|
|
2519
|
+
} else if (perField !== null) {
|
|
2520
|
+
themePatch = perField;
|
|
2521
|
+
}
|
|
2522
|
+
if (themePatch !== void 0) {
|
|
2523
|
+
body["theme"] = themePatch === null || replace ? themePatch : { ...await readStoredTheme(slug), ...themePatch };
|
|
2524
|
+
if (body["theme"] !== null) {
|
|
2525
|
+
const merged = body["theme"];
|
|
2526
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
2527
|
+
if (value === "") delete merged[key];
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2309
2530
|
}
|
|
2310
2531
|
if (Object.keys(body).length === 0) {
|
|
2311
2532
|
throw new Error(
|
|
@@ -2315,7 +2536,10 @@ async function setConfig(args) {
|
|
|
2315
2536
|
const url = `${adminApiBase(slug)}/configure/save`;
|
|
2316
2537
|
const data = await request(url, {
|
|
2317
2538
|
method: "POST",
|
|
2318
|
-
body
|
|
2539
|
+
body,
|
|
2540
|
+
// Every 400 from this endpoint names the offending field and the values
|
|
2541
|
+
// it accepts — worth far more to the caller than "Request rejected."
|
|
2542
|
+
validationDetail: true
|
|
2319
2543
|
});
|
|
2320
2544
|
if (flagBool(args.flags, "json")) {
|
|
2321
2545
|
process.stdout.write(JSON.stringify(data.config, null, 2) + "\n");
|
|
@@ -2325,7 +2549,7 @@ async function setConfig(args) {
|
|
|
2325
2549
|
`);
|
|
2326
2550
|
}
|
|
2327
2551
|
function readTextFile(filePath, flag) {
|
|
2328
|
-
const resolved =
|
|
2552
|
+
const resolved = path12.resolve(filePath);
|
|
2329
2553
|
if (!fs10.existsSync(resolved)) {
|
|
2330
2554
|
throw new Error(`${flag}: file not found: ${resolved}`);
|
|
2331
2555
|
}
|
|
@@ -2343,10 +2567,63 @@ function parseThemeJson(raw, flag) {
|
|
|
2343
2567
|
}
|
|
2344
2568
|
return parsed;
|
|
2345
2569
|
}
|
|
2570
|
+
async function readStoredTheme(slug) {
|
|
2571
|
+
const url = `${adminApiBase(slug)}/configure/config`;
|
|
2572
|
+
const data = await request(url);
|
|
2573
|
+
return data.brand.theme ?? {};
|
|
2574
|
+
}
|
|
2575
|
+
function collectThemeFieldFlags(flags) {
|
|
2576
|
+
const patch = {};
|
|
2577
|
+
for (const [flag, raw] of Object.entries(flags)) {
|
|
2578
|
+
if (!flag.startsWith(THEME_FLAG_PREFIX)) continue;
|
|
2579
|
+
const name = flag.slice(THEME_FLAG_PREFIX.length);
|
|
2580
|
+
const field = THEME_FIELDS[name];
|
|
2581
|
+
if (!field) {
|
|
2582
|
+
const suggestions = suggestThemeFields(name);
|
|
2583
|
+
throw new Error(
|
|
2584
|
+
`Unknown theme field "${name}".
|
|
2585
|
+
Valid fields: ${Object.keys(THEME_FIELDS).join(", ")}.` + (suggestions.length > 0 ? `
|
|
2586
|
+
Did you mean: ${suggestions.join(", ")}?` : "")
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
if (typeof raw !== "string") {
|
|
2590
|
+
if (field.kind === "boolean") {
|
|
2591
|
+
patch[name] = true;
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
throw new Error(`--${flag} requires a value (use --${flag} "" to clear).`);
|
|
2595
|
+
}
|
|
2596
|
+
patch[name] = coerceThemeValue(flag, name, raw);
|
|
2597
|
+
}
|
|
2598
|
+
return Object.keys(patch).length > 0 ? patch : null;
|
|
2599
|
+
}
|
|
2600
|
+
function coerceThemeValue(flag, name, raw) {
|
|
2601
|
+
const field = THEME_FIELDS[name];
|
|
2602
|
+
if (!field || raw === "") return raw;
|
|
2603
|
+
if (field.values && !field.values.includes(raw)) {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
`--${flag} must be one of: ${field.values.join(", ")} (got "${raw}").`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
if (field.kind === "boolean") {
|
|
2609
|
+
if (raw !== "true" && raw !== "false") {
|
|
2610
|
+
throw new Error(`--${flag} must be true or false (got "${raw}").`);
|
|
2611
|
+
}
|
|
2612
|
+
return raw === "true";
|
|
2613
|
+
}
|
|
2614
|
+
if (field.kind === "integer") {
|
|
2615
|
+
const n = Number.parseInt(raw, 10);
|
|
2616
|
+
if (!Number.isInteger(n) || String(n) !== raw.trim()) {
|
|
2617
|
+
throw new Error(`--${flag} must be an integer (got "${raw}").`);
|
|
2618
|
+
}
|
|
2619
|
+
return n;
|
|
2620
|
+
}
|
|
2621
|
+
return raw;
|
|
2622
|
+
}
|
|
2346
2623
|
|
|
2347
2624
|
// src/commands/configure/upload.ts
|
|
2348
2625
|
import * as fs11 from "fs";
|
|
2349
|
-
import * as
|
|
2626
|
+
import * as path13 from "path";
|
|
2350
2627
|
|
|
2351
2628
|
// src/commands/configure/shared.ts
|
|
2352
2629
|
var ASSET_KINDS = ["icon", "logoLight", "logoDark"];
|
|
@@ -2371,11 +2648,11 @@ async function uploadAsset(args) {
|
|
|
2371
2648
|
if (!filePath) {
|
|
2372
2649
|
throw new Error(`Usage: gs configure upload <${ASSET_KINDS.join("|")}> <file>`);
|
|
2373
2650
|
}
|
|
2374
|
-
const resolved =
|
|
2651
|
+
const resolved = path13.resolve(filePath);
|
|
2375
2652
|
if (!fs11.existsSync(resolved)) {
|
|
2376
2653
|
throw new Error(`File not found: ${resolved}`);
|
|
2377
2654
|
}
|
|
2378
|
-
const mime = MIME_BY_EXT[
|
|
2655
|
+
const mime = MIME_BY_EXT[path13.extname(resolved).toLowerCase()];
|
|
2379
2656
|
if (!mime) {
|
|
2380
2657
|
throw new Error("Unsupported image type. Use .png, .jpg, or .webp.");
|
|
2381
2658
|
}
|
|
@@ -2384,7 +2661,7 @@ async function uploadAsset(args) {
|
|
|
2384
2661
|
form.append(
|
|
2385
2662
|
"file",
|
|
2386
2663
|
new Blob([new Uint8Array(bytes)], { type: mime }),
|
|
2387
|
-
|
|
2664
|
+
path13.basename(resolved)
|
|
2388
2665
|
);
|
|
2389
2666
|
const url = `${adminApiBase(slug)}/configure/upload/${kind}`;
|
|
2390
2667
|
const data = await request(url, {
|
|
@@ -2704,11 +2981,12 @@ function recentChangelog(text, minItems = 15) {
|
|
|
2704
2981
|
// src/version-check.ts
|
|
2705
2982
|
import * as fs12 from "fs";
|
|
2706
2983
|
import * as os3 from "os";
|
|
2707
|
-
import * as
|
|
2984
|
+
import * as path14 from "path";
|
|
2708
2985
|
var REFRESH_COMMAND = "__refresh-version-cache";
|
|
2709
2986
|
var PKG = "@greatstore/cli";
|
|
2710
2987
|
var REGISTRY_URL = `https://registry.npmjs.org/${PKG}/latest`;
|
|
2711
2988
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
2989
|
+
var CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
2712
2990
|
var OutdatedCliError = class extends Error {
|
|
2713
2991
|
constructor(current, latest) {
|
|
2714
2992
|
super(
|
|
@@ -2730,6 +3008,7 @@ async function assertUpToDate(current, opts = {}) {
|
|
|
2730
3008
|
if (cached && compareSemver(cached.latest, current) > 0) {
|
|
2731
3009
|
throw new OutdatedCliError(current, cached.latest);
|
|
2732
3010
|
}
|
|
3011
|
+
if (cached && now() - cached.checkedAt < CACHE_TTL_MS) return;
|
|
2733
3012
|
const fresh = await fetcher();
|
|
2734
3013
|
if (fresh) {
|
|
2735
3014
|
writeCache(home, { latest: fresh, checkedAt: now() });
|
|
@@ -2765,7 +3044,7 @@ async function fetchLatest() {
|
|
|
2765
3044
|
}
|
|
2766
3045
|
}
|
|
2767
3046
|
function cachePath(home) {
|
|
2768
|
-
return
|
|
3047
|
+
return path14.join(home, ".greatstore", "version-check.json");
|
|
2769
3048
|
}
|
|
2770
3049
|
function readCache(home) {
|
|
2771
3050
|
try {
|
|
@@ -2778,11 +3057,11 @@ function readCache(home) {
|
|
|
2778
3057
|
}
|
|
2779
3058
|
return null;
|
|
2780
3059
|
}
|
|
2781
|
-
function writeCache(home,
|
|
3060
|
+
function writeCache(home, cache2) {
|
|
2782
3061
|
try {
|
|
2783
3062
|
const file = cachePath(home);
|
|
2784
|
-
fs12.mkdirSync(
|
|
2785
|
-
fs12.writeFileSync(file, JSON.stringify(
|
|
3063
|
+
fs12.mkdirSync(path14.dirname(file), { recursive: true });
|
|
3064
|
+
fs12.writeFileSync(file, JSON.stringify(cache2));
|
|
2786
3065
|
} catch {
|
|
2787
3066
|
}
|
|
2788
3067
|
}
|
|
@@ -2804,7 +3083,7 @@ function parseVer(v) {
|
|
|
2804
3083
|
}
|
|
2805
3084
|
|
|
2806
3085
|
// 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" : "";
|
|
3086
|
+
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.4 \u2014 2026-09-12\n\n### Added\n- Edit one theme field at a time: `gs configure set --theme.fontFamily Manrope\n --theme.radius sharp`. Theme writes merge into the stored theme, so a\n one-field change no longer means retyping the whole object (and no longer\n drops the keys you left out). `--replace` writes a theme outright.\n- `gs configure set --help` prints every field it accepts with its type and\n legal values, and a mistyped field name or value is now rejected with the\n valid list before anything is sent.\n\n### Changed\n- Rejected configuration writes say what was wrong with them \u2014 the field and\n the values it accepts \u2014 instead of \"Request rejected.\"\n- Commands no longer wait on a version check every run, which takes about half\n a second off each one.\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
3087
|
var HELP = `gs \u2014 GreatStore CLI (v${CLI_VERSION})
|
|
2809
3088
|
|
|
2810
3089
|
Usage:
|
|
@@ -2820,6 +3099,8 @@ Account:
|
|
|
2820
3099
|
Store administration:
|
|
2821
3100
|
configure Show the store configuration.
|
|
2822
3101
|
configure set --<field> <val> Edit it (same fields as the admin Configure panel).
|
|
3102
|
+
\`gs configure set --help\` lists every field,
|
|
3103
|
+
including the theme fields and their values.
|
|
2823
3104
|
configure upload <kind> <file> Upload icon / logoLight / logoDark.
|
|
2824
3105
|
configure clear <kind> Remove an uploaded asset.
|
|
2825
3106
|
connectors List the store's MCP connectors.
|
|
@@ -2834,7 +3115,8 @@ Store administration:
|
|
|
2834
3115
|
top level (\`gs push\` == \`gs apps push\`).
|
|
2835
3116
|
|
|
2836
3117
|
Agent tooling:
|
|
2837
|
-
skill
|
|
3118
|
+
skill Print how to install the GreatStore agent skill,
|
|
3119
|
+
for the AI coding agent that runs it.
|
|
2838
3120
|
|
|
2839
3121
|
Common flags:
|
|
2840
3122
|
--store <slug> Target store for admin commands (default: nearest .gsrc,
|
|
@@ -2848,8 +3130,6 @@ Common flags:
|
|
|
2848
3130
|
--bundle <path> Path to bundle for \`apps push\`.
|
|
2849
3131
|
--force Overwrite for \`apps init\` / \`apps pull\`; skip confirmation for \`apps delete\`.
|
|
2850
3132
|
--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
3133
|
-h, --help Show this help.
|
|
2854
3134
|
-v, --version Print version.
|
|
2855
3135
|
|
|
@@ -2886,7 +3166,8 @@ async function main() {
|
|
|
2886
3166
|
return 0;
|
|
2887
3167
|
}
|
|
2888
3168
|
if (flagBool(parsed.flags, "help") || parsed.command === "help" || parsed.command === null) {
|
|
2889
|
-
|
|
3169
|
+
const specific = parsed.command === "configure" ? configureHelp(parsed.positional[0]) : void 0;
|
|
3170
|
+
process.stdout.write(specific ?? HELP);
|
|
2890
3171
|
return 0;
|
|
2891
3172
|
}
|
|
2892
3173
|
if (parsed.command !== null && APPS_ALIASES.has(parsed.command)) {
|
|
@@ -2915,7 +3196,7 @@ async function main() {
|
|
|
2915
3196
|
await connectorsCommand(parsed);
|
|
2916
3197
|
return 0;
|
|
2917
3198
|
case "skill":
|
|
2918
|
-
skillCommand(
|
|
3199
|
+
skillCommand();
|
|
2919
3200
|
return 0;
|
|
2920
3201
|
default:
|
|
2921
3202
|
process.stderr.write(`Unknown command: ${parsed.command}
|