@greatstore/cli 0.0.42 → 0.0.44
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 +21 -1
- package/dist/cli.js +558 -167
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
All notable changes to `@greatstore/cli` are recorded here. The format
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
|
|
6
|
+
## 0.0.44 — 2026-08-18
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- New check: a component that still declares `onError` gets a warning
|
|
10
|
+
pointing at the async-component pattern that replaced it, with the
|
|
11
|
+
throw-vs-fallback caveat (a throw asks the assistant to retry, so
|
|
12
|
+
permanent failures should render a fallback rather than throw). Earlier
|
|
13
|
+
this surfaced as the generic "prop isn't declared in the schema"
|
|
14
|
+
warning, whose suggested fix was wrong for a former lifecycle callback.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `gs apps init` now always refreshes `AGENTS.md` (the agent guidance
|
|
18
|
+
file) so it tracks the installed CLI version instead of going stale.
|
|
19
|
+
The file carries a "do not edit — auto-generated" banner; your own
|
|
20
|
+
project files are still left untouched.
|
|
21
|
+
|
|
22
|
+
## 0.0.43 — 2026-08-18
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `gs apps build` and `gs apps push` now check each component before
|
|
26
|
+
building or uploading it. Findings are reported as **errors** (a
|
|
27
|
+
blocker — the component isn't built or uploaded) or **warnings** (it
|
|
28
|
+
builds and is ready to publish, but something is worth improving),
|
|
29
|
+
and a single run reports everything it found rather than stopping at
|
|
30
|
+
the first problem.
|
|
31
|
+
- New check: a component's props and its `inputSchema.properties` must
|
|
32
|
+
agree. A schema field the component doesn't accept is an error; a prop
|
|
33
|
+
the schema doesn't declare is a warning, since nothing will ever pass
|
|
34
|
+
it. The props GreatStore injects (`onSendMessage`, `onCallTool`,
|
|
35
|
+
`onUpdateModelContext`, `onShowLightbox`, `onClose`, `storeData`,
|
|
36
|
+
`Image`) are exempt.
|
|
37
|
+
|
|
6
38
|
## 0.0.42 — 2026-08-15
|
|
7
39
|
|
|
8
40
|
### Changed
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ working.
|
|
|
60
60
|
| Command | What it does |
|
|
61
61
|
| ------------------------------------------------------ | -------------------------------------------------- |
|
|
62
62
|
| `gs apps init <name> [--out <dir>] [--store <s>]` | Scaffold a working component project. |
|
|
63
|
-
| `gs apps build [<name>]` |
|
|
63
|
+
| `gs apps build [<name>]` | Check and build component bundle(s); no args = all. |
|
|
64
64
|
| `gs apps list [--json]` | List components in the current store. |
|
|
65
65
|
| `gs apps pull <name> [--draft\|--live\|--version N] [-o <dir>]` | Download a component to disk. |
|
|
66
66
|
| `gs apps push [<name>] [--manifest path] [--bundle path]` | Upload local changes. |
|
|
@@ -68,6 +68,26 @@ working.
|
|
|
68
68
|
| `gs apps unpublish <name>` | Hide a published component. |
|
|
69
69
|
| `gs apps delete <name> [--yes]` | Remove the component. |
|
|
70
70
|
|
|
71
|
+
### Component checks
|
|
72
|
+
|
|
73
|
+
`gs apps build` and `gs apps push` check each component before they do
|
|
74
|
+
their work, and report what they find as either:
|
|
75
|
+
|
|
76
|
+
- **error** — a blocker. The component isn't built or uploaded until it's
|
|
77
|
+
fixed.
|
|
78
|
+
- **warning** — the component builds and is ready to publish, but
|
|
79
|
+
something is worth improving.
|
|
80
|
+
|
|
81
|
+
The checks cover the manifest (valid JSON, a `name` matching the folder,
|
|
82
|
+
an `inputSchema` simple enough for the assistant to fill reliably) and
|
|
83
|
+
the component's props: every prop it declares should be a field in
|
|
84
|
+
`inputSchema.properties`, apart from the props GreatStore injects
|
|
85
|
+
(`onSendMessage`, `onCallTool`, `onUpdateModelContext`,
|
|
86
|
+
`onShowLightbox`, `onClose`, `storeData`, `Image`). A prop nothing
|
|
87
|
+
declares is a warning — nothing will ever pass it. A schema field the
|
|
88
|
+
component doesn't accept is an error. A component still declaring
|
|
89
|
+
`onError` gets a warning pointing at async components, which replaced it.
|
|
90
|
+
|
|
71
91
|
### Agent skill
|
|
72
92
|
|
|
73
93
|
| Command | What it does |
|
package/dist/cli.js
CHANGED
|
@@ -643,7 +643,7 @@ import { fileURLToPath } from "url";
|
|
|
643
643
|
var cache = null;
|
|
644
644
|
function loadTemplate() {
|
|
645
645
|
if (cache) return cache;
|
|
646
|
-
cache = true ? JSON.parse('{"component/component.tsx":"import React from \\"react\\";\\n\\n// Add your tool args (matching manifest.json#inputSchema.properties)\\n// alongside the GreatStore-injected lifecycle props. See AGENTS.md for\\n// what each lifecycle prop does.\\ninterface Props {\\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 onClose: () => void;\\n // Drop-in for <img>: `<Image src=\u2026 alt=\u2026 />`. See AGENTS.md.\\n Image: React.ElementType<React.ComponentProps<\\"img\\">>;\\n}\\n\\n// See AGENTS.md for the design rules (em-based sizing, brand CSS\\n// variables), the lifecycle props, and how to write an async,\\n// backend-backed component.\\nexport default function __GS_PASCAL__(_props: Props): React.ReactElement {\\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 <strong>__GS_NAME__</strong> \u2014 hello from your component!\\n </div>\\n );\\n}\\n","component/manifest.json":"{\\n \\"name\\": \\"__GS_NAME__\\",\\n \\"displayName\\": \\"__GS_DISPLAY_NAME__\\",\\n \\"description\\": \\"Renders the __GS_NAME__ widget.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {}\\n }\\n}\\n","root/.gitignore":"node_modules/\\ncomponents/*/bundle.js\\ncomponents/*/.gssync.json\\n*.tsbuildinfo\\n.DS_Store\\n","root/.gsrc":"{\\n \\"store\\": \\"__GS_STORE__\\"\\n}\\n","root/AGENTS.md":"# AGENTS.md\\n\\nGuidance for AI coding agents working on the components in this project.\\n`CLAUDE.md` and `GEMINI.md` are symlinks to this file.\\n\\nThese components render inside arbitrary publisher websites (the embed\\nwidget mounts on a host page) as well as the GreatStore storefront. You\\ncontrol neither the host page\'s base font size nor its color scheme, and\\nthe store owner controls the look from GreatStore theme settings. The\\nrules below keep components portable across both.\\n\\n## Design requirements\\n\\n### Never use `rem` for fonts and sizing \u2014 always use `em`\\n\\n`rem` resolves against the host page\'s root font size, which is\\narbitrary and outside our control. A host that sets `html { font-size:\\n8px }` (or 200%) will shrink or blow up every `rem`-based dimension.\\n`em` resolves against the component\'s own font size, so it stays\\nself-consistent wherever the component is mounted.\\n\\nUse `em` for font sizes, padding, margins, gaps, widths, heights,\\nand any other length. Borders may stay in `px` (hairlines should not\\nscale with text).\\n\\n```tsx\\n// Bad \u2014 breaks on hosts with a non-default root font size\\n<div style={{ padding: \\"1rem\\", fontSize: \\"0.875rem\\" }} />\\n\\n// Good\\n<div style={{ padding: \\"1em\\", fontSize: \\"0.875em\\" }} />\\n```\\n\\n### Don\'t hardcode colors, fonts, or corner radius \u2014 use GreatStore brand variables\\n\\nThe store owner themes the assistant from GreatStore settings. Hardcoded\\nvalues ignore that and make the component clash with the rest of the\\nstore. Read from the CSS variables GreatStore injects on the surface\\ninstead, so the component restyles itself when the theme changes.\\n\\nAvailable variables:\\n\\n| Purpose | Variable |\\n| --- | --- |\\n| Surface background | `--color-surface` |\\n| Secondary surface | `--color-surface-secondary` |\\n| Accent surface | `--color-surface-accent` |\\n| Hover surface | `--color-surface-hover` |\\n| Foreground text | `--color-foreground` |\\n| Secondary text | `--color-foreground-secondary` |\\n| Muted text | `--color-foreground-muted` |\\n| Accent text | `--color-foreground-accent` |\\n| Border | `--color-border-default` |\\n| Focus border | `--color-border-focus` |\\n| Primary / brand | `--color-primary` |\\n| Text on primary | `--color-primary-foreground` |\\n| Primary hover | `--color-primary-hover` |\\n| Primary muted | `--color-primary-muted` |\\n| Text on primary muted | `--color-primary-muted-foreground` |\\n| Primary tint | `--color-primary-tint` |\\n| Link | `--color-link` |\\n| Font family | `--font-primary` |\\n| Secondary font family | `--font-secondary` |\\n| Corner radius (scale) | `--radius-xs` \u2026 `--radius-4xl`, `--radius-pill` |\\n\\nThe store owner can set a secondary font family in their GreatStore\\ntheme settings, exposed as `--font-secondary` (it falls back to\\n`--font-primary` when unset). Reach for it to add a tasteful second\\nlayer of typography \u2014 pair `--font-primary` for body copy with\\n`--font-secondary` for headings, prices, or other accents \u2014 so the\\ncomponent reflects the store\'s full type system instead of a single\\nface.\\n\\n```tsx\\n// Bad \u2014 hardcoded, ignores the store\'s theme\\n<button style={{ background: \\"#4f46e5\\", borderRadius: 8, fontFamily: \\"Inter\\" }} />\\n\\n// Good\\n<button\\n style={{\\n background: \\"var(--color-primary)\\",\\n color: \\"var(--color-primary-foreground)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n/>\\n```\\n\\nProvide a sensible fallback only when a value might be absent, e.g.\\n`var(--color-primary, currentColor)`.\\n\\n## Component props\\n\\nEach component receives its tool args (the fields you declare in\\n`manifest.json#inputSchema.properties`) plus these GreatStore-injected\\nlifecycle props, which are always present:\\n\\n| Prop | What it does |\\n| --- | --- |\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it. |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject invisible background context for the model (selected variant, configured options, current step). Replaces the prior value \u2014 never appends; pass `\\"\\"` to clear. Read on the next chat turn. Use it when the assistant should *know* in-component state without a visible message; use `onSendMessage` when you want a turn to happen now. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay your component can\'t render itself (it\'s 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| `onClose()` | Dismiss the host slot. Over-input clears the overlay, fullscreen reverts the pane, inline is a no-op. |\\n| `Image` | A drop-in for `<img>`: render `<Image src=\u2026 alt=\u2026 />` instead of `<img>` and your images are automatically served at the right size for the store. Takes the same props as `<img>` (`src`, `alt`, `style`, `loading`, `onClick`, \u2026). Previewing the component outside the store? Pass `Image={\\"img\\"}` (or your own component) to render a plain image. |\\n\\n## Async components (backend-backed, render-blocking data)\\n\\nIf a component must load data from a backend/API before it can render\\ncorrectly, make it async \u2014 don\'t render an empty shell and fetch in a\\n`useEffect`. Set `\\"async\\": true` in `manifest.json` and export an async\\ndefault. GreatStore waits for your promise (showing a normal loading\\nstate, so you don\'t render your own placeholder), then renders what it\\nresolves to. Components that render purely from their props stay\\nsynchronous.\\n\\nA thrown error is a **retry signal**: the in-store AI sees it and\\nusually re-calls the tool. So only throw when a *different* call could\\nhelp:\\n\\n1. `await` the backend call, then return the finished JSX.\\n2. Validate the AI-passed props first and throw on bad input \u2014 the AI\\n can fix the args and retry. (Don\'t validate the API\'s *output* and\\n throw: the AI can\'t fix the backend, it\'ll just loop.)\\n3. Broadcast a backend failure ONLY when retrying differently could\\n succeed, and say what to change (e.g. empty search \u2192 \\"try a broader\\n keyword\\"). For idempotent failures (500, timeout, missing record)\\n re-running the same call changes nothing \u2014 render a graceful fallback\\n instead of throwing.\\n\\n```tsx\\nexport default async function Example(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","root/README.md":"# GreatStore components\\n\\nCustom React components published to your GreatStore tenant. Each\\ncomponent lives in its own folder under `components/`.\\n\\n```\\nnpm install\\ngs apps init <component_name> # add a new component\\ngs apps build # builds every components/<name>/bundle.js\\ngs apps push # uploads every changed component as a draft\\ngs apps publish <component_name> # promote a specific component to live\\n```\\n\\n- `gs apps push` (no args) hashes each component and only uploads the ones\\n that have changed since the last sync.\\n- `gs apps pull` (no args) refreshes every remote component into\\n `components/<name>/`. Components with unsaved local edits are skipped\\n with a warning; pass `--force` to overwrite.\\n\\nSee `AGENTS.md` for the design rules every component must follow\\n(em-based sizing, brand CSS variables) \u2014 it doubles as guidance for AI\\ncoding agents (`CLAUDE.md` / `GEMINI.md` symlink to it).\\n","root/package.json":"{\\n \\"name\\": \\"greatstore-components\\",\\n \\"version\\": \\"0.0.1\\",\\n \\"private\\": true,\\n \\"type\\": \\"module\\",\\n \\"scripts\\": {\\n \\"build\\": \\"gs apps build\\",\\n \\"push\\": \\"gs apps build && gs apps push\\"\\n },\\n \\"dependencies\\": {\\n \\"react\\": \\"^19.0.0\\",\\n \\"react-dom\\": \\"^19.0.0\\"\\n },\\n \\"devDependencies\\": {\\n \\"@types/react\\": \\"^19.0.0\\",\\n \\"@types/react-dom\\": \\"^19.0.0\\",\\n \\"@vitejs/plugin-react\\": \\"^4.3.0\\",\\n \\"typescript\\": \\"^5.6.0\\",\\n \\"vite\\": \\"^5.4.0\\"\\n }\\n}\\n","root/tsconfig.json":"{\\n \\"compilerOptions\\": {\\n \\"target\\": \\"ES2022\\",\\n \\"module\\": \\"ESNext\\",\\n \\"moduleResolution\\": \\"Bundler\\",\\n \\"jsx\\": \\"react-jsx\\",\\n \\"lib\\": [\\"ES2022\\", \\"DOM\\"],\\n \\"strict\\": true,\\n \\"esModuleInterop\\": true,\\n \\"skipLibCheck\\": true,\\n \\"isolatedModules\\": true,\\n \\"noEmit\\": true\\n },\\n \\"include\\": [\\"components/**/component.tsx\\", \\"vite.config.ts\\"]\\n}\\n","root/vite.config.ts":"import { defineConfig } from \\"vite\\";\\nimport react from \\"@vitejs/plugin-react\\";\\n\\n// Real builds happen in `gs build` (one Vite invocation per\\n// component, externals + runtime shim paths owned by the CLI). This\\n// file exists only so editors / language servers can resolve the\\n// React plugin when inspecting components/*/component.tsx.\\nexport default defineConfig({\\n plugins: [react()],\\n});\\n"}') : readTreeFromDisk(new URL("../template/", import.meta.url));
|
|
646
|
+
cache = true ? JSON.parse('{"component/component.tsx":"import React from \\"react\\";\\n\\n// Add your tool args (matching manifest.json#inputSchema.properties)\\n// alongside the GreatStore-injected lifecycle props. See AGENTS.md for\\n// what each lifecycle prop does.\\ninterface Props {\\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 onClose: () => void;\\n // Drop-in for <img>: `<Image src=\u2026 alt=\u2026 />`. See AGENTS.md.\\n Image: React.ElementType<React.ComponentProps<\\"img\\">>;\\n}\\n\\n// See AGENTS.md for the design rules (em-based sizing, brand CSS\\n// variables), the lifecycle props, and how to write an async,\\n// backend-backed component.\\nexport default function __GS_PASCAL__(_props: Props): React.ReactElement {\\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 <strong>__GS_NAME__</strong> \u2014 hello from your component!\\n </div>\\n );\\n}\\n","component/manifest.json":"{\\n \\"name\\": \\"__GS_NAME__\\",\\n \\"displayName\\": \\"__GS_DISPLAY_NAME__\\",\\n \\"description\\": \\"Renders the __GS_NAME__ widget.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {}\\n }\\n}\\n","root/.gitignore":"node_modules/\\ncomponents/*/bundle.js\\ncomponents/*/.gssync.json\\n*.tsbuildinfo\\n.DS_Store\\n","root/.gsrc":"{\\n \\"store\\": \\"__GS_STORE__\\"\\n}\\n","root/AGENTS.md":"# AGENTS.md\\n\\n> **Do not edit \u2014 auto-generated by the GreatStore CLI.** This file is\\n> rewritten by `gs apps init`, so any changes you make here will be\\n> overwritten. Put project-specific notes in another file.\\n\\nGuidance for AI coding agents working on the components in this project.\\n`CLAUDE.md` and `GEMINI.md` are symlinks to this file.\\n\\nThese components render inside arbitrary publisher websites (the embed\\nwidget mounts on a host page) as well as the GreatStore storefront. You\\ncontrol neither the host page\'s base font size nor its color scheme, and\\nthe store owner controls the look from GreatStore theme settings. The\\nrules below keep components portable across both.\\n\\n## Design requirements\\n\\n### Never use `rem` for fonts and sizing \u2014 always use `em`\\n\\n`rem` resolves against the host page\'s root font size, which is\\narbitrary and outside our control. A host that sets `html { font-size:\\n8px }` (or 200%) will shrink or blow up every `rem`-based dimension.\\n`em` resolves against the component\'s own font size, so it stays\\nself-consistent wherever the component is mounted.\\n\\nUse `em` for font sizes, padding, margins, gaps, widths, heights,\\nand any other length. Borders may stay in `px` (hairlines should not\\nscale with text).\\n\\n```tsx\\n// Bad \u2014 breaks on hosts with a non-default root font size\\n<div style={{ padding: \\"1rem\\", fontSize: \\"0.875rem\\" }} />\\n\\n// Good\\n<div style={{ padding: \\"1em\\", fontSize: \\"0.875em\\" }} />\\n```\\n\\n### Don\'t hardcode colors, fonts, or corner radius \u2014 use GreatStore brand variables\\n\\nThe store owner themes the assistant from GreatStore settings. Hardcoded\\nvalues ignore that and make the component clash with the rest of the\\nstore. Read from the CSS variables GreatStore injects on the surface\\ninstead, so the component restyles itself when the theme changes.\\n\\nAvailable variables:\\n\\n| Purpose | Variable |\\n| --- | --- |\\n| Surface background | `--color-surface` |\\n| Secondary surface | `--color-surface-secondary` |\\n| Accent surface | `--color-surface-accent` |\\n| Hover surface | `--color-surface-hover` |\\n| Foreground text | `--color-foreground` |\\n| Secondary text | `--color-foreground-secondary` |\\n| Muted text | `--color-foreground-muted` |\\n| Accent text | `--color-foreground-accent` |\\n| Border | `--color-border-default` |\\n| Focus border | `--color-border-focus` |\\n| Primary / brand | `--color-primary` |\\n| Text on primary | `--color-primary-foreground` |\\n| Primary hover | `--color-primary-hover` |\\n| Primary muted | `--color-primary-muted` |\\n| Text on primary muted | `--color-primary-muted-foreground` |\\n| Primary tint | `--color-primary-tint` |\\n| Link | `--color-link` |\\n| Font family | `--font-primary` |\\n| Secondary font family | `--font-secondary` |\\n| Corner radius (scale) | `--radius-xs` \u2026 `--radius-4xl`, `--radius-pill` |\\n\\nThe store owner can set a secondary font family in their GreatStore\\ntheme settings, exposed as `--font-secondary` (it falls back to\\n`--font-primary` when unset). Reach for it to add a tasteful second\\nlayer of typography \u2014 pair `--font-primary` for body copy with\\n`--font-secondary` for headings, prices, or other accents \u2014 so the\\ncomponent reflects the store\'s full type system instead of a single\\nface.\\n\\n```tsx\\n// Bad \u2014 hardcoded, ignores the store\'s theme\\n<button style={{ background: \\"#4f46e5\\", borderRadius: 8, fontFamily: \\"Inter\\" }} />\\n\\n// Good\\n<button\\n style={{\\n background: \\"var(--color-primary)\\",\\n color: \\"var(--color-primary-foreground)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n/>\\n```\\n\\nProvide a sensible fallback only when a value might be absent, e.g.\\n`var(--color-primary, currentColor)`.\\n\\n## Component props\\n\\nEach component receives its tool args (the fields you declare in\\n`manifest.json#inputSchema.properties`) plus these GreatStore-injected\\nlifecycle props, which are always present:\\n\\n| Prop | What it does |\\n| --- | --- |\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it. |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject invisible background context for the model (selected variant, configured options, current step). Replaces the prior value \u2014 never appends; pass `\\"\\"` to clear. Read on the next chat turn. Use it when the assistant should *know* in-component state without a visible message; use `onSendMessage` when you want a turn to happen now. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay your component can\'t render itself (it\'s 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| `onClose()` | Dismiss the host slot. Over-input clears the overlay, fullscreen reverts the pane, inline is a no-op. |\\n| `Image` | A drop-in for `<img>`: render `<Image src=\u2026 alt=\u2026 />` instead of `<img>` and your images are automatically served at the right size for the store. Takes the same props as `<img>` (`src`, `alt`, `style`, `loading`, `onClick`, \u2026). Previewing the component outside the store? Pass `Image={\\"img\\"}` (or your own component) to render a plain image. |\\n\\nYour props and `inputSchema.properties` must line up: `gs apps build`\\nerrors if the schema declares a field your component doesn\'t accept, and\\nwarns if your component declares a prop the schema doesn\'t (nothing will\\never pass it). The injected props above are exempt.\\n\\nThere is no `onError` prop. If you still declare one, `gs apps build`\\nwarns. Report failures from an async component instead \u2014 but mind the\\nthrow-vs-fallback rule below: a throw asks the AI to retry, so a\\npermanent failure should render a fallback, not throw.\\n\\n## Async components (backend-backed, render-blocking data)\\n\\nIf a component must load data from a backend/API before it can render\\ncorrectly, make it async \u2014 don\'t render an empty shell and fetch in a\\n`useEffect`. Set `\\"async\\": true` in `manifest.json` and export an async\\ndefault. GreatStore waits for your promise (showing a normal loading\\nstate, so you don\'t render your own placeholder), then renders what it\\nresolves to. Components that render purely from their props stay\\nsynchronous.\\n\\nA thrown error is a **retry signal**: the in-store AI sees it and\\nusually re-calls the tool. So only throw when a *different* call could\\nhelp:\\n\\n1. `await` the backend call, then return the finished JSX.\\n2. Validate the AI-passed props first and throw on bad input \u2014 the AI\\n can fix the args and retry. (Don\'t validate the API\'s *output* and\\n throw: the AI can\'t fix the backend, it\'ll just loop.)\\n3. Broadcast a backend failure ONLY when retrying differently could\\n succeed, and say what to change (e.g. empty search \u2192 \\"try a broader\\n keyword\\"). For idempotent failures (500, timeout, missing record)\\n re-running the same call changes nothing \u2014 render a graceful fallback\\n instead of throwing.\\n\\n```tsx\\nexport default async function Example(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","root/README.md":"# GreatStore components\\n\\nCustom React components published to your GreatStore tenant. Each\\ncomponent lives in its own folder under `components/`.\\n\\n```\\nnpm install\\ngs apps init <component_name> # add a new component\\ngs apps build # builds every components/<name>/bundle.js\\ngs apps push # uploads every changed component as a draft\\ngs apps publish <component_name> # promote a specific component to live\\n```\\n\\n- `gs apps push` (no args) hashes each component and only uploads the ones\\n that have changed since the last sync.\\n- `gs apps pull` (no args) refreshes every remote component into\\n `components/<name>/`. Components with unsaved local edits are skipped\\n with a warning; pass `--force` to overwrite.\\n\\nSee `AGENTS.md` for the design rules every component must follow\\n(em-based sizing, brand CSS variables) \u2014 it doubles as guidance for AI\\ncoding agents (`CLAUDE.md` / `GEMINI.md` symlink to it).\\n","root/package.json":"{\\n \\"name\\": \\"greatstore-components\\",\\n \\"version\\": \\"0.0.1\\",\\n \\"private\\": true,\\n \\"type\\": \\"module\\",\\n \\"scripts\\": {\\n \\"build\\": \\"gs apps build\\",\\n \\"push\\": \\"gs apps build && gs apps push\\"\\n },\\n \\"dependencies\\": {\\n \\"react\\": \\"^19.0.0\\",\\n \\"react-dom\\": \\"^19.0.0\\"\\n },\\n \\"devDependencies\\": {\\n \\"@types/react\\": \\"^19.0.0\\",\\n \\"@types/react-dom\\": \\"^19.0.0\\",\\n \\"@vitejs/plugin-react\\": \\"^4.3.0\\",\\n \\"typescript\\": \\"^5.6.0\\",\\n \\"vite\\": \\"^5.4.0\\"\\n }\\n}\\n","root/tsconfig.json":"{\\n \\"compilerOptions\\": {\\n \\"target\\": \\"ES2022\\",\\n \\"module\\": \\"ESNext\\",\\n \\"moduleResolution\\": \\"Bundler\\",\\n \\"jsx\\": \\"react-jsx\\",\\n \\"lib\\": [\\"ES2022\\", \\"DOM\\"],\\n \\"strict\\": true,\\n \\"esModuleInterop\\": true,\\n \\"skipLibCheck\\": true,\\n \\"isolatedModules\\": true,\\n \\"noEmit\\": true\\n },\\n \\"include\\": [\\"components/**/component.tsx\\", \\"vite.config.ts\\"]\\n}\\n","root/vite.config.ts":"import { defineConfig } from \\"vite\\";\\nimport react from \\"@vitejs/plugin-react\\";\\n\\n// Real builds happen in `gs build` (one Vite invocation per\\n// component, externals + runtime shim paths owned by the CLI). This\\n// file exists only so editors / language servers can resolve the\\n// React plugin when inspecting components/*/component.tsx.\\nexport default defineConfig({\\n plugins: [react()],\\n});\\n"}') : readTreeFromDisk(new URL("../template/", import.meta.url));
|
|
647
647
|
return cache;
|
|
648
648
|
}
|
|
649
649
|
var TOKENS = {
|
|
@@ -803,15 +803,17 @@ function ensureRoot(root, rootExisted, force, opts) {
|
|
|
803
803
|
fs4.mkdirSync(path4.join(root, "components"), { recursive: true });
|
|
804
804
|
return written;
|
|
805
805
|
}
|
|
806
|
+
var CLI_OWNED_ROOT_FILES = /* @__PURE__ */ new Set(["AGENTS.md"]);
|
|
806
807
|
function writeRootFiles(root, files, force) {
|
|
807
808
|
const written = [];
|
|
808
809
|
for (const [relPath, content] of files) {
|
|
809
810
|
const full = path4.join(root, relPath);
|
|
810
811
|
fs4.mkdirSync(path4.dirname(full), { recursive: true });
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
812
|
+
const exists = fs4.existsSync(full);
|
|
813
|
+
if (!force && exists && !CLI_OWNED_ROOT_FILES.has(relPath)) continue;
|
|
814
|
+
const changed = !exists || fs4.readFileSync(full, "utf8") !== content;
|
|
815
|
+
fs4.writeFileSync(full, content);
|
|
816
|
+
if (changed) written.push(relPath);
|
|
815
817
|
}
|
|
816
818
|
for (const link of ["CLAUDE.md", "GEMINI.md"]) {
|
|
817
819
|
const linkPath = path4.join(root, link);
|
|
@@ -876,13 +878,443 @@ function pascal(name) {
|
|
|
876
878
|
}
|
|
877
879
|
|
|
878
880
|
// src/commands/apps/build.ts
|
|
881
|
+
import * as fs6 from "fs";
|
|
882
|
+
import * as path7 from "path";
|
|
883
|
+
import { createRequire as createRequire2 } from "module";
|
|
884
|
+
|
|
885
|
+
// src/validation/context.ts
|
|
879
886
|
import * as fs5 from "fs";
|
|
880
|
-
import * as
|
|
887
|
+
import * as path6 from "path";
|
|
888
|
+
|
|
889
|
+
// src/validation/ast.ts
|
|
881
890
|
import { createRequire } from "module";
|
|
891
|
+
import * as path5 from "path";
|
|
892
|
+
function analyzeComponentProps(root, sourcePath, sourceText) {
|
|
893
|
+
const ts = loadTypeScript(root);
|
|
894
|
+
if (!ts) {
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
reason: "couldn't check props against the manifest \u2014 `typescript` isn't installed in this project (run `npm install`)"
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const sourceFile = ts.createSourceFile(
|
|
902
|
+
sourcePath,
|
|
903
|
+
sourceText,
|
|
904
|
+
ts.ScriptTarget.Latest,
|
|
905
|
+
true,
|
|
906
|
+
ts.ScriptKind.TSX
|
|
907
|
+
);
|
|
908
|
+
const fn = findDefaultExport(ts, sourceFile);
|
|
909
|
+
if (!fn) {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
reason: "couldn't find a default-exported component function to check props against \u2014 export the component as `export default function \u2026`"
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
return propsOfParameter(ts, sourceFile, fn);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
return {
|
|
918
|
+
ok: false,
|
|
919
|
+
reason: `couldn't check props against the manifest \u2014 ${path5.basename(sourcePath)} could not be parsed (${err.message})`
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function loadTypeScript(root) {
|
|
924
|
+
const localRequire = createRequire(path5.join(root, "package.json"));
|
|
925
|
+
try {
|
|
926
|
+
return localRequire("typescript");
|
|
927
|
+
} catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function findDefaultExport(ts, sourceFile) {
|
|
932
|
+
let assigned = null;
|
|
933
|
+
for (const statement of sourceFile.statements) {
|
|
934
|
+
if (ts.isFunctionDeclaration(statement) && hasDefaultModifier(ts, statement)) {
|
|
935
|
+
return statement;
|
|
936
|
+
}
|
|
937
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
938
|
+
assigned = statement.expression;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (!assigned) return null;
|
|
942
|
+
const expression = unwrap(ts, assigned);
|
|
943
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
944
|
+
return expression;
|
|
945
|
+
}
|
|
946
|
+
if (ts.isIdentifier(expression)) {
|
|
947
|
+
return findLocalFunction(ts, sourceFile, expression.text);
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
function hasDefaultModifier(ts, node) {
|
|
952
|
+
return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
|
|
953
|
+
}
|
|
954
|
+
function unwrap(ts, node) {
|
|
955
|
+
let current = node;
|
|
956
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
957
|
+
current = current.expression;
|
|
958
|
+
}
|
|
959
|
+
return current;
|
|
960
|
+
}
|
|
961
|
+
function findLocalFunction(ts, sourceFile, name) {
|
|
962
|
+
for (const statement of sourceFile.statements) {
|
|
963
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) {
|
|
964
|
+
return statement;
|
|
965
|
+
}
|
|
966
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
967
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
968
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name) continue;
|
|
969
|
+
if (!declaration.initializer) continue;
|
|
970
|
+
const initializer = unwrap(ts, declaration.initializer);
|
|
971
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) {
|
|
972
|
+
return initializer;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
function propsOfParameter(ts, sourceFile, fn) {
|
|
979
|
+
const parameter = fn.parameters[0];
|
|
980
|
+
if (!parameter) return { ok: true, props: { names: [], open: false } };
|
|
981
|
+
if (parameter.type) {
|
|
982
|
+
const fromType = namesFromTypeNode(ts, sourceFile, parameter.type, /* @__PURE__ */ new Set());
|
|
983
|
+
if (fromType) return { ok: true, props: fromType };
|
|
984
|
+
}
|
|
985
|
+
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
986
|
+
return { ok: true, props: namesFromBindingPattern(ts, parameter.name) };
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
reason: "couldn't check props against the manifest \u2014 declare the component's props as a local `interface`/`type` or destructure them in the signature"
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function namesFromTypeNode(ts, sourceFile, node, seen) {
|
|
994
|
+
if (ts.isTypeLiteralNode(node)) {
|
|
995
|
+
return propsFromMembers(ts, node.members);
|
|
996
|
+
}
|
|
997
|
+
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
998
|
+
return namesFromTypeName(ts, sourceFile, node.typeName.text, seen);
|
|
999
|
+
}
|
|
1000
|
+
if (ts.isIntersectionTypeNode(node)) {
|
|
1001
|
+
const parts = node.types.map((t) => namesFromTypeNode(ts, sourceFile, t, seen));
|
|
1002
|
+
if (parts.every((p) => p === null)) return null;
|
|
1003
|
+
return mergeProps(parts.map((p) => p ?? { names: [], open: true }));
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
function namesFromTypeName(ts, sourceFile, name, seen) {
|
|
1008
|
+
if (seen.has(name)) return { names: [], open: false };
|
|
1009
|
+
seen.add(name);
|
|
1010
|
+
for (const statement of sourceFile.statements) {
|
|
1011
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === name) {
|
|
1012
|
+
const own = propsFromMembers(ts, statement.members);
|
|
1013
|
+
const bases = (statement.heritageClauses ?? []).flatMap(
|
|
1014
|
+
(clause) => clause.types.map(
|
|
1015
|
+
(base) => ts.isIdentifier(base.expression) ? namesFromTypeName(ts, sourceFile, base.expression.text, seen) ?? {
|
|
1016
|
+
names: [],
|
|
1017
|
+
open: true
|
|
1018
|
+
} : { names: [], open: true }
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
return mergeProps([own, ...bases]);
|
|
1022
|
+
}
|
|
1023
|
+
if (ts.isTypeAliasDeclaration(statement) && statement.name.text === name) {
|
|
1024
|
+
return namesFromTypeNode(ts, sourceFile, statement.type, seen);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
function propsFromMembers(ts, members) {
|
|
1030
|
+
const names = [];
|
|
1031
|
+
let open = false;
|
|
1032
|
+
for (const member of members) {
|
|
1033
|
+
if (ts.isIndexSignatureDeclaration(member)) {
|
|
1034
|
+
open = true;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (!ts.isPropertySignature(member) && !ts.isMethodSignature(member)) continue;
|
|
1038
|
+
const memberName = staticMemberName(ts, member.name);
|
|
1039
|
+
if (memberName === null) {
|
|
1040
|
+
open = true;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
names.push(memberName);
|
|
1044
|
+
}
|
|
1045
|
+
return { names, open };
|
|
1046
|
+
}
|
|
1047
|
+
function namesFromBindingPattern(ts, pattern) {
|
|
1048
|
+
const names = [];
|
|
1049
|
+
let open = false;
|
|
1050
|
+
for (const element of pattern.elements) {
|
|
1051
|
+
if (element.dotDotDotToken) {
|
|
1052
|
+
open = true;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const key = element.propertyName ?? element.name;
|
|
1056
|
+
const name = staticMemberName(ts, key);
|
|
1057
|
+
if (name === null) open = true;
|
|
1058
|
+
else names.push(name);
|
|
1059
|
+
}
|
|
1060
|
+
return { names, open };
|
|
1061
|
+
}
|
|
1062
|
+
function staticMemberName(ts, name) {
|
|
1063
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
1064
|
+
if (ts.isStringLiteral(name)) return name.text;
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
function mergeProps(parts) {
|
|
1068
|
+
const names = [];
|
|
1069
|
+
let open = false;
|
|
1070
|
+
for (const part of parts) {
|
|
1071
|
+
for (const name of part.names) {
|
|
1072
|
+
if (!names.includes(name)) names.push(name);
|
|
1073
|
+
}
|
|
1074
|
+
open ||= part.open;
|
|
1075
|
+
}
|
|
1076
|
+
return { names, open };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/validation/context.ts
|
|
1080
|
+
function buildContext(paths) {
|
|
1081
|
+
const { root, name } = paths;
|
|
1082
|
+
const componentDir = path6.join(root, "components", name);
|
|
1083
|
+
const manifestPath = paths.manifestPath ?? path6.join(componentDir, "manifest.json");
|
|
1084
|
+
const sourcePath = paths.sourcePath ?? path6.join(componentDir, "component.tsx");
|
|
1085
|
+
const manifestFile = relative4(root, manifestPath);
|
|
1086
|
+
const sourceFile = relative4(root, sourcePath);
|
|
1087
|
+
const diagnostics = [];
|
|
1088
|
+
let manifest = null;
|
|
1089
|
+
const manifestText = readIfPresent(manifestPath);
|
|
1090
|
+
if (manifestText === null) {
|
|
1091
|
+
diagnostics.push({
|
|
1092
|
+
severity: "error",
|
|
1093
|
+
rule: "manifest/missing",
|
|
1094
|
+
file: manifestFile,
|
|
1095
|
+
message: "manifest not found."
|
|
1096
|
+
});
|
|
1097
|
+
} else {
|
|
1098
|
+
try {
|
|
1099
|
+
const parsed = JSON.parse(manifestText);
|
|
1100
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1101
|
+
diagnostics.push({
|
|
1102
|
+
severity: "error",
|
|
1103
|
+
rule: "manifest/invalid",
|
|
1104
|
+
file: manifestFile,
|
|
1105
|
+
message: "manifest must be a JSON object."
|
|
1106
|
+
});
|
|
1107
|
+
} else {
|
|
1108
|
+
manifest = parsed;
|
|
1109
|
+
}
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
diagnostics.push({
|
|
1112
|
+
severity: "error",
|
|
1113
|
+
rule: "manifest/invalid",
|
|
1114
|
+
file: manifestFile,
|
|
1115
|
+
message: `manifest is not valid JSON: ${err.message}`
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const sourceText = readIfPresent(sourcePath);
|
|
1120
|
+
const props = sourceText === null ? { ok: false, reason: `couldn't check props against the manifest \u2014 ${sourceFile} not found` } : analyzeComponentProps(root, sourcePath, sourceText);
|
|
1121
|
+
return {
|
|
1122
|
+
ctx: { name, manifestFile, sourceFile, manifest, props },
|
|
1123
|
+
diagnostics
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function readIfPresent(file) {
|
|
1127
|
+
try {
|
|
1128
|
+
return fs5.readFileSync(file, "utf8");
|
|
1129
|
+
} catch {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function relative4(root, target) {
|
|
1134
|
+
const rel = path6.relative(root, target);
|
|
1135
|
+
return rel.startsWith("..") ? target : rel.split(path6.sep).join("/");
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/validation/injected-props.ts
|
|
1139
|
+
var INJECTED_PROPS = /* @__PURE__ */ new Set([
|
|
1140
|
+
"onSendMessage",
|
|
1141
|
+
"onCallTool",
|
|
1142
|
+
"onClose",
|
|
1143
|
+
"onUpdateModelContext",
|
|
1144
|
+
"onShowLightbox",
|
|
1145
|
+
"storeData",
|
|
1146
|
+
"Image"
|
|
1147
|
+
]);
|
|
1148
|
+
|
|
1149
|
+
// src/validation/retired-props.ts
|
|
1150
|
+
var RETIRED_LIFECYCLE_PROPS = /* @__PURE__ */ new Map([
|
|
1151
|
+
[
|
|
1152
|
+
"onError",
|
|
1153
|
+
'prop `onError` is deprecated and no longer provided \u2014 prefer an async component to report failures. Drop the prop; see the "Async components" section in AGENTS.md for how to signal errors.'
|
|
1154
|
+
]
|
|
1155
|
+
]);
|
|
1156
|
+
|
|
1157
|
+
// src/validation/rules/component-props.ts
|
|
1158
|
+
function componentPropsRule(ctx) {
|
|
1159
|
+
if (!ctx.manifest) return [];
|
|
1160
|
+
const schemaFields = schemaFieldNames(ctx.manifest.inputSchema);
|
|
1161
|
+
if (schemaFields === null) return [];
|
|
1162
|
+
if (!ctx.props.ok) {
|
|
1163
|
+
return [
|
|
1164
|
+
{
|
|
1165
|
+
severity: "warning",
|
|
1166
|
+
rule: "props/not-analyzable",
|
|
1167
|
+
file: ctx.sourceFile,
|
|
1168
|
+
message: `${ctx.props.reason}.`
|
|
1169
|
+
}
|
|
1170
|
+
];
|
|
1171
|
+
}
|
|
1172
|
+
if (ctx.props.props.open) return [];
|
|
1173
|
+
const declared = new Set(ctx.props.props.names);
|
|
1174
|
+
const schema = new Set(schemaFields);
|
|
1175
|
+
const diagnostics = [];
|
|
1176
|
+
for (const prop of ctx.props.props.names) {
|
|
1177
|
+
if (INJECTED_PROPS.has(prop)) continue;
|
|
1178
|
+
const retirement = RETIRED_LIFECYCLE_PROPS.get(prop);
|
|
1179
|
+
if (retirement) {
|
|
1180
|
+
diagnostics.push({
|
|
1181
|
+
severity: "warning",
|
|
1182
|
+
rule: "props/retired-lifecycle",
|
|
1183
|
+
file: ctx.sourceFile,
|
|
1184
|
+
message: retirement
|
|
1185
|
+
});
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (schema.has(prop)) continue;
|
|
1189
|
+
diagnostics.push({
|
|
1190
|
+
severity: "warning",
|
|
1191
|
+
rule: "props/undeclared",
|
|
1192
|
+
file: ctx.sourceFile,
|
|
1193
|
+
message: `prop \`${prop}\` is not declared in ${ctx.manifestFile}#inputSchema.properties, so nothing will ever pass it. Declare it in the schema, or drop it from the component.`
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
for (const field of schemaFields) {
|
|
1197
|
+
if (declared.has(field)) continue;
|
|
1198
|
+
diagnostics.push({
|
|
1199
|
+
severity: "error",
|
|
1200
|
+
rule: "props/missing",
|
|
1201
|
+
file: ctx.sourceFile,
|
|
1202
|
+
message: `${ctx.manifestFile} declares inputSchema field \`${field}\`, but the component does not accept a \`${field}\` prop. Add it to the component's props, or remove it from the schema.`
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return diagnostics;
|
|
1206
|
+
}
|
|
1207
|
+
function schemaFieldNames(inputSchema) {
|
|
1208
|
+
if (!inputSchema || typeof inputSchema !== "object") return null;
|
|
1209
|
+
const properties = inputSchema["properties"];
|
|
1210
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
return Object.keys(properties);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/validation/rules/input-schema-complexity.ts
|
|
1217
|
+
var MAX_SCHEMA_BYTES = 8 * 1024;
|
|
1218
|
+
var MAX_SCHEMA_FIELDS = 50;
|
|
1219
|
+
var UNION_KEYWORDS = ["anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
1220
|
+
function measure(node, path14, acc) {
|
|
1221
|
+
if (Array.isArray(node)) {
|
|
1222
|
+
node.forEach((entry, i) => measure(entry, `${path14}[${i}]`, acc));
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (!node || typeof node !== "object") return;
|
|
1226
|
+
const record = node;
|
|
1227
|
+
for (const keyword of UNION_KEYWORDS) {
|
|
1228
|
+
if (keyword in record) acc.unionPaths.push(`${path14}.${keyword}`);
|
|
1229
|
+
}
|
|
1230
|
+
const properties = record["properties"];
|
|
1231
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
1232
|
+
for (const [name, sub] of Object.entries(properties)) {
|
|
1233
|
+
acc.fields += 1;
|
|
1234
|
+
measure(sub, `${path14}.${name}`, acc);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if ("items" in record) measure(record["items"], `${path14}[]`, acc);
|
|
1238
|
+
}
|
|
1239
|
+
function inputSchemaComplexityRule(ctx) {
|
|
1240
|
+
const inputSchema = ctx.manifest?.inputSchema;
|
|
1241
|
+
if (!inputSchema || typeof inputSchema !== "object") return [];
|
|
1242
|
+
const acc = {
|
|
1243
|
+
bytes: Buffer.byteLength(JSON.stringify(inputSchema), "utf8"),
|
|
1244
|
+
fields: 0,
|
|
1245
|
+
unionPaths: []
|
|
1246
|
+
};
|
|
1247
|
+
measure(inputSchema, "inputSchema", acc);
|
|
1248
|
+
const problems = [];
|
|
1249
|
+
if (acc.unionPaths.length > 0) {
|
|
1250
|
+
const shown = acc.unionPaths.slice(0, 3).join(", ");
|
|
1251
|
+
const more = acc.unionPaths.length > 3 ? ` (+${acc.unionPaths.length - 3} more)` : "";
|
|
1252
|
+
problems.push(
|
|
1253
|
+
`uses ${shown}${more} \u2014 declare exactly one type per field; accept alternate shapes in component code instead`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
if (acc.bytes > MAX_SCHEMA_BYTES) {
|
|
1257
|
+
problems.push(
|
|
1258
|
+
`is ${(acc.bytes / 1024).toFixed(1)} KB serialized (limit ${MAX_SCHEMA_BYTES / 1024} KB)`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
if (acc.fields > MAX_SCHEMA_FIELDS) {
|
|
1262
|
+
problems.push(`declares ${acc.fields} fields (limit ${MAX_SCHEMA_FIELDS})`);
|
|
1263
|
+
}
|
|
1264
|
+
if (problems.length === 0) return [];
|
|
1265
|
+
return [
|
|
1266
|
+
{
|
|
1267
|
+
severity: "error",
|
|
1268
|
+
rule: "manifest/input-schema-too-complex",
|
|
1269
|
+
file: ctx.manifestFile,
|
|
1270
|
+
message: `inputSchema ${problems.join("; ")}.
|
|
1271
|
+
A schema this complex is usually a sign the component is overengineered, and the assistant will fill it unreliably or stop calling the tool altogether. Keep it to a small set of flat, single-type fields (one canonical name per concept \u2014 no aliases), and let component code handle formatting, fallbacks, and edge cases.`
|
|
1272
|
+
}
|
|
1273
|
+
];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/validation/rules/manifest-name.ts
|
|
1277
|
+
function manifestNameRule(ctx) {
|
|
1278
|
+
const declared = ctx.manifest?.name;
|
|
1279
|
+
if (typeof declared !== "string" || declared === ctx.name) return [];
|
|
1280
|
+
return [
|
|
1281
|
+
{
|
|
1282
|
+
severity: "error",
|
|
1283
|
+
rule: "manifest/name-mismatch",
|
|
1284
|
+
file: ctx.manifestFile,
|
|
1285
|
+
message: `manifest name "${declared}" does not match folder name "${ctx.name}".`
|
|
1286
|
+
}
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/validation/types.ts
|
|
1291
|
+
function errorsIn(diagnostics) {
|
|
1292
|
+
return diagnostics.filter((d) => d.severity === "error");
|
|
1293
|
+
}
|
|
1294
|
+
function warningsIn(diagnostics) {
|
|
1295
|
+
return diagnostics.filter((d) => d.severity === "warning");
|
|
1296
|
+
}
|
|
1297
|
+
function formatDiagnostic(d) {
|
|
1298
|
+
return `${d.severity === "error" ? "error" : "warning"} ${d.file}: ${d.message}`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/validation/index.ts
|
|
1302
|
+
var RULES = [
|
|
1303
|
+
manifestNameRule,
|
|
1304
|
+
inputSchemaComplexityRule,
|
|
1305
|
+
componentPropsRule
|
|
1306
|
+
];
|
|
1307
|
+
function validateComponent(paths) {
|
|
1308
|
+
const { ctx, diagnostics } = buildContext(paths);
|
|
1309
|
+
for (const rule of RULES) diagnostics.push(...rule(ctx));
|
|
1310
|
+
return { diagnostics, ok: errorsIn(diagnostics).length === 0 };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/commands/apps/build.ts
|
|
882
1314
|
var COMPONENTS_DIR = "components";
|
|
883
1315
|
async function buildCommand(args) {
|
|
884
1316
|
const root = process.cwd();
|
|
885
|
-
if (!
|
|
1317
|
+
if (!fs6.existsSync(path7.join(root, "package.json")) || !fs6.existsSync(path7.join(root, COMPONENTS_DIR))) {
|
|
886
1318
|
throw new Error(
|
|
887
1319
|
`\`gs apps build\` must run from a project root (contains \`package.json\` and \`${COMPONENTS_DIR}/\`). Current dir: ${root}`
|
|
888
1320
|
);
|
|
@@ -900,16 +1332,33 @@ async function buildCommand(args) {
|
|
|
900
1332
|
if (target && queue.length === 0) {
|
|
901
1333
|
throw new Error(`No component named "${target}" in ./${COMPONENTS_DIR}`);
|
|
902
1334
|
}
|
|
903
|
-
const
|
|
1335
|
+
const invalid = [];
|
|
1336
|
+
const buildable = [];
|
|
904
1337
|
for (const name of queue) {
|
|
905
|
-
const
|
|
906
|
-
const
|
|
1338
|
+
const report = validateComponent({ root, name });
|
|
1339
|
+
for (const diagnostic of report.diagnostics) {
|
|
1340
|
+
process.stdout.write(` ${formatDiagnostic(diagnostic)}
|
|
1341
|
+
`);
|
|
1342
|
+
}
|
|
1343
|
+
(report.ok ? buildable : invalid).push(name);
|
|
1344
|
+
}
|
|
1345
|
+
if (buildable.length === 0) {
|
|
1346
|
+
process.stdout.write(`
|
|
1347
|
+
Nothing built \u2014 fix the errors above.
|
|
1348
|
+
`);
|
|
1349
|
+
process.exitCode = 1;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const { build, reactPlugin, transformWithEsbuild } = await loadVite(root);
|
|
1353
|
+
for (const name of buildable) {
|
|
1354
|
+
const dir = path7.join(root, COMPONENTS_DIR, name);
|
|
1355
|
+
const bundlePath = path7.join(dir, "bundle.js");
|
|
907
1356
|
await build({
|
|
908
1357
|
plugins: [reactPlugin()],
|
|
909
1358
|
logLevel: "warn",
|
|
910
1359
|
build: {
|
|
911
1360
|
lib: {
|
|
912
|
-
entry:
|
|
1361
|
+
entry: path7.join(dir, "component.tsx"),
|
|
913
1362
|
formats: ["es"],
|
|
914
1363
|
fileName: () => "bundle.js"
|
|
915
1364
|
},
|
|
@@ -930,8 +1379,8 @@ async function buildCommand(args) {
|
|
|
930
1379
|
sourcemap: false
|
|
931
1380
|
}
|
|
932
1381
|
});
|
|
933
|
-
const beforeBytes =
|
|
934
|
-
const src =
|
|
1382
|
+
const beforeBytes = fs6.statSync(bundlePath).size;
|
|
1383
|
+
const src = fs6.readFileSync(bundlePath, "utf8");
|
|
935
1384
|
const { code } = await transformWithEsbuild(src, bundlePath, {
|
|
936
1385
|
minify: true,
|
|
937
1386
|
legalComments: "none",
|
|
@@ -939,13 +1388,22 @@ async function buildCommand(args) {
|
|
|
939
1388
|
loader: "js",
|
|
940
1389
|
sourcemap: false
|
|
941
1390
|
});
|
|
942
|
-
|
|
1391
|
+
fs6.writeFileSync(bundlePath, code);
|
|
943
1392
|
const afterBytes = Buffer.byteLength(code, "utf8");
|
|
944
1393
|
process.stdout.write(
|
|
945
1394
|
`built ${COMPONENTS_DIR}/${name}/bundle.js (${formatBytes(afterBytes)}, ${pctSmaller(beforeBytes, afterBytes)} smaller)
|
|
946
1395
|
`
|
|
947
1396
|
);
|
|
948
1397
|
}
|
|
1398
|
+
if (invalid.length > 0) {
|
|
1399
|
+
process.stdout.write(
|
|
1400
|
+
`
|
|
1401
|
+
Not built (fix the errors above): ${invalid.join(", ")}
|
|
1402
|
+
`
|
|
1403
|
+
);
|
|
1404
|
+
process.exitCode = 1;
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
949
1407
|
process.stdout.write(
|
|
950
1408
|
`
|
|
951
1409
|
Built locally \u2014 nothing uploaded yet. Next: \`gs apps push\` to upload, then \`gs apps publish <name>\` to ship. If you're an AI assistant: confirm with the user before running these (or just run them if they already asked you to ship end-to-end).
|
|
@@ -953,10 +1411,10 @@ Built locally \u2014 nothing uploaded yet. Next: \`gs apps push\` to upload, the
|
|
|
953
1411
|
);
|
|
954
1412
|
}
|
|
955
1413
|
function listComponents(root) {
|
|
956
|
-
const dir =
|
|
957
|
-
return
|
|
958
|
-
const candidate =
|
|
959
|
-
return
|
|
1414
|
+
const dir = path7.join(root, COMPONENTS_DIR);
|
|
1415
|
+
return fs6.readdirSync(dir).filter((entry) => {
|
|
1416
|
+
const candidate = path7.join(dir, entry);
|
|
1417
|
+
return fs6.statSync(candidate).isDirectory() && fs6.existsSync(path7.join(candidate, "component.tsx"));
|
|
960
1418
|
}).sort();
|
|
961
1419
|
}
|
|
962
1420
|
function formatBytes(n) {
|
|
@@ -968,7 +1426,7 @@ function pctSmaller(before, after) {
|
|
|
968
1426
|
return `${Math.round((1 - after / before) * 100)}%`;
|
|
969
1427
|
}
|
|
970
1428
|
async function loadVite(root) {
|
|
971
|
-
const localRequire =
|
|
1429
|
+
const localRequire = createRequire2(path7.join(root, "package.json"));
|
|
972
1430
|
const vitePath = resolveEsmEntry(localRequire, "vite");
|
|
973
1431
|
if (!vitePath) {
|
|
974
1432
|
throw new Error(
|
|
@@ -1016,30 +1474,30 @@ function resolveEsmEntry(req, specifier) {
|
|
|
1016
1474
|
}
|
|
1017
1475
|
const pkgJsonPath = findOwningPackageJson(anchor, specifier);
|
|
1018
1476
|
if (!pkgJsonPath) return null;
|
|
1019
|
-
const pkgDir =
|
|
1477
|
+
const pkgDir = path7.dirname(pkgJsonPath);
|
|
1020
1478
|
let pkg;
|
|
1021
1479
|
try {
|
|
1022
|
-
pkg = JSON.parse(
|
|
1480
|
+
pkg = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf8"));
|
|
1023
1481
|
} catch {
|
|
1024
1482
|
return null;
|
|
1025
1483
|
}
|
|
1026
1484
|
const fromExports = pickImportEntry(pkg.exports);
|
|
1027
1485
|
const entry = fromExports ?? (typeof pkg.module === "string" ? pkg.module : null) ?? (typeof pkg.main === "string" ? pkg.main : null);
|
|
1028
1486
|
if (!entry) return null;
|
|
1029
|
-
return
|
|
1487
|
+
return path7.resolve(pkgDir, entry);
|
|
1030
1488
|
}
|
|
1031
1489
|
function findOwningPackageJson(start, specifier) {
|
|
1032
|
-
let dir =
|
|
1490
|
+
let dir = path7.dirname(start);
|
|
1033
1491
|
while (true) {
|
|
1034
|
-
const candidate =
|
|
1035
|
-
if (
|
|
1492
|
+
const candidate = path7.join(dir, "package.json");
|
|
1493
|
+
if (fs6.existsSync(candidate)) {
|
|
1036
1494
|
try {
|
|
1037
|
-
const parsed = JSON.parse(
|
|
1495
|
+
const parsed = JSON.parse(fs6.readFileSync(candidate, "utf8"));
|
|
1038
1496
|
if (parsed.name === specifier) return candidate;
|
|
1039
1497
|
} catch {
|
|
1040
1498
|
}
|
|
1041
1499
|
}
|
|
1042
|
-
const parent =
|
|
1500
|
+
const parent = path7.dirname(dir);
|
|
1043
1501
|
if (parent === dir) return null;
|
|
1044
1502
|
dir = parent;
|
|
1045
1503
|
}
|
|
@@ -1099,36 +1557,36 @@ function pad(s, width) {
|
|
|
1099
1557
|
}
|
|
1100
1558
|
|
|
1101
1559
|
// src/commands/apps/pull.ts
|
|
1102
|
-
import * as
|
|
1103
|
-
import * as
|
|
1560
|
+
import * as fs8 from "fs";
|
|
1561
|
+
import * as path9 from "path";
|
|
1104
1562
|
|
|
1105
1563
|
// src/sync.ts
|
|
1106
1564
|
import * as crypto2 from "crypto";
|
|
1107
|
-
import * as
|
|
1108
|
-
import * as
|
|
1565
|
+
import * as fs7 from "fs";
|
|
1566
|
+
import * as path8 from "path";
|
|
1109
1567
|
var SYNC_FILE = ".gssync.json";
|
|
1110
1568
|
function hashString(text) {
|
|
1111
1569
|
return crypto2.createHash("sha256").update(text).digest("hex");
|
|
1112
1570
|
}
|
|
1113
1571
|
function hashFile(filePath) {
|
|
1114
|
-
if (!
|
|
1115
|
-
return hashString(
|
|
1572
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
1573
|
+
return hashString(fs7.readFileSync(filePath, "utf8"));
|
|
1116
1574
|
}
|
|
1117
1575
|
function computeComponentHashes(componentDir) {
|
|
1118
|
-
const manifestHash = hashFile(
|
|
1576
|
+
const manifestHash = hashFile(path8.join(componentDir, "manifest.json"));
|
|
1119
1577
|
if (manifestHash === null) {
|
|
1120
1578
|
throw new Error(`Missing manifest.json in ${componentDir}`);
|
|
1121
1579
|
}
|
|
1122
1580
|
return {
|
|
1123
1581
|
manifestHash,
|
|
1124
|
-
sourceHash: hashFile(
|
|
1582
|
+
sourceHash: hashFile(path8.join(componentDir, "component.tsx"))
|
|
1125
1583
|
};
|
|
1126
1584
|
}
|
|
1127
1585
|
function readSyncState(componentDir) {
|
|
1128
|
-
const file =
|
|
1129
|
-
if (!
|
|
1586
|
+
const file = path8.join(componentDir, SYNC_FILE);
|
|
1587
|
+
if (!fs7.existsSync(file)) return null;
|
|
1130
1588
|
try {
|
|
1131
|
-
const parsed = JSON.parse(
|
|
1589
|
+
const parsed = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
1132
1590
|
if (typeof parsed.version === "number" && typeof parsed.manifestHash === "string" && typeof parsed.sourceHash === "string") {
|
|
1133
1591
|
return {
|
|
1134
1592
|
version: parsed.version,
|
|
@@ -1142,8 +1600,8 @@ function readSyncState(componentDir) {
|
|
|
1142
1600
|
}
|
|
1143
1601
|
}
|
|
1144
1602
|
function writeSyncState(componentDir, state) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1603
|
+
fs7.writeFileSync(
|
|
1604
|
+
path8.join(componentDir, SYNC_FILE),
|
|
1147
1605
|
JSON.stringify(state, null, 2) + "\n"
|
|
1148
1606
|
);
|
|
1149
1607
|
}
|
|
@@ -1184,7 +1642,7 @@ async function pullCommand(args) {
|
|
|
1184
1642
|
for (const name of targets) {
|
|
1185
1643
|
const outcome = await pullOne({
|
|
1186
1644
|
slug,
|
|
1187
|
-
componentDir:
|
|
1645
|
+
componentDir: path9.join(root, "components", name),
|
|
1188
1646
|
name,
|
|
1189
1647
|
revisionQuery,
|
|
1190
1648
|
force
|
|
@@ -1213,7 +1671,7 @@ async function pullAll(opts) {
|
|
|
1213
1671
|
}
|
|
1214
1672
|
const outcomes = [];
|
|
1215
1673
|
for (const entry of list.components) {
|
|
1216
|
-
const componentDir =
|
|
1674
|
+
const componentDir = path9.join(root, "components", entry.name);
|
|
1217
1675
|
const outcome = await pullOne({
|
|
1218
1676
|
slug,
|
|
1219
1677
|
componentDir,
|
|
@@ -1241,7 +1699,7 @@ ${summary.join(", ")} (of ${outcomes.length}) \u2190 ${slug}
|
|
|
1241
1699
|
}
|
|
1242
1700
|
async function pullOne(opts) {
|
|
1243
1701
|
const { slug, componentDir, name, revisionQuery, force } = opts;
|
|
1244
|
-
if (!force &&
|
|
1702
|
+
if (!force && fs8.existsSync(componentDir)) {
|
|
1245
1703
|
const sync = readSyncState(componentDir);
|
|
1246
1704
|
if (hasLocalChanges(componentDir, sync)) {
|
|
1247
1705
|
return {
|
|
@@ -1262,14 +1720,14 @@ async function pullOne(opts) {
|
|
|
1262
1720
|
message: err instanceof Error ? err.message : String(err)
|
|
1263
1721
|
};
|
|
1264
1722
|
}
|
|
1265
|
-
|
|
1723
|
+
fs8.mkdirSync(componentDir, { recursive: true });
|
|
1266
1724
|
const manifestText = JSON.stringify(data.manifest, null, 2) + "\n";
|
|
1267
|
-
|
|
1268
|
-
|
|
1725
|
+
fs8.writeFileSync(path9.join(componentDir, "manifest.json"), manifestText);
|
|
1726
|
+
fs8.writeFileSync(path9.join(componentDir, "bundle.js"), data.bundle);
|
|
1269
1727
|
const wrote = ["manifest.json", "bundle.js"];
|
|
1270
1728
|
let sourceHash = "";
|
|
1271
1729
|
if (data.source !== null) {
|
|
1272
|
-
|
|
1730
|
+
fs8.writeFileSync(path9.join(componentDir, "component.tsx"), data.source);
|
|
1273
1731
|
wrote.push("component.tsx");
|
|
1274
1732
|
sourceHash = hashString(data.source);
|
|
1275
1733
|
}
|
|
@@ -1287,7 +1745,7 @@ async function pullOne(opts) {
|
|
|
1287
1745
|
return { name, status: "pulled", version: data.version, wrote };
|
|
1288
1746
|
}
|
|
1289
1747
|
function ensureComponentsDir(root) {
|
|
1290
|
-
|
|
1748
|
+
fs8.mkdirSync(path9.join(root, "components"), { recursive: true });
|
|
1291
1749
|
}
|
|
1292
1750
|
function buildRevisionQuery(args) {
|
|
1293
1751
|
const version = flagString(args.flags, "version");
|
|
@@ -1321,72 +1779,14 @@ function printOutcome(outcome, force) {
|
|
|
1321
1779
|
}
|
|
1322
1780
|
|
|
1323
1781
|
// src/commands/apps/push.ts
|
|
1324
|
-
import * as
|
|
1325
|
-
import * as
|
|
1326
|
-
|
|
1327
|
-
// src/commands/apps/manifest-lint.ts
|
|
1328
|
-
var MAX_SCHEMA_BYTES = 8 * 1024;
|
|
1329
|
-
var MAX_SCHEMA_FIELDS = 50;
|
|
1330
|
-
var UNION_KEYWORDS = ["anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
1331
|
-
function measure(node, path12, acc) {
|
|
1332
|
-
if (Array.isArray(node)) {
|
|
1333
|
-
node.forEach((entry, i) => measure(entry, `${path12}[${i}]`, acc));
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
if (!node || typeof node !== "object") return;
|
|
1337
|
-
const record = node;
|
|
1338
|
-
for (const keyword of UNION_KEYWORDS) {
|
|
1339
|
-
if (keyword in record) acc.unionPaths.push(`${path12}.${keyword}`);
|
|
1340
|
-
}
|
|
1341
|
-
const properties = record["properties"];
|
|
1342
|
-
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
1343
|
-
for (const [name, sub] of Object.entries(properties)) {
|
|
1344
|
-
acc.fields += 1;
|
|
1345
|
-
measure(sub, `${path12}.${name}`, acc);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
if ("items" in record) measure(record["items"], `${path12}[]`, acc);
|
|
1349
|
-
}
|
|
1350
|
-
function assertSaneInputSchema(label, inputSchema) {
|
|
1351
|
-
if (!inputSchema || typeof inputSchema !== "object") return;
|
|
1352
|
-
const acc = {
|
|
1353
|
-
bytes: Buffer.byteLength(JSON.stringify(inputSchema), "utf8"),
|
|
1354
|
-
fields: 0,
|
|
1355
|
-
unionPaths: []
|
|
1356
|
-
};
|
|
1357
|
-
measure(inputSchema, "inputSchema", acc);
|
|
1358
|
-
const problems = [];
|
|
1359
|
-
if (acc.unionPaths.length > 0) {
|
|
1360
|
-
const shown = acc.unionPaths.slice(0, 3).join(", ");
|
|
1361
|
-
const more = acc.unionPaths.length > 3 ? ` (+${acc.unionPaths.length - 3} more)` : "";
|
|
1362
|
-
problems.push(
|
|
1363
|
-
`uses ${shown}${more} \u2014 declare exactly one type per field; accept alternate shapes in component code instead`
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
if (acc.bytes > MAX_SCHEMA_BYTES) {
|
|
1367
|
-
problems.push(
|
|
1368
|
-
`is ${(acc.bytes / 1024).toFixed(1)} KB serialized (limit ${MAX_SCHEMA_BYTES / 1024} KB)`
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
if (acc.fields > MAX_SCHEMA_FIELDS) {
|
|
1372
|
-
problems.push(
|
|
1373
|
-
`declares ${acc.fields} fields (limit ${MAX_SCHEMA_FIELDS})`
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
if (problems.length === 0) return;
|
|
1377
|
-
throw new Error(
|
|
1378
|
-
`${label}: inputSchema ${problems.join("; ")}.
|
|
1379
|
-
A schema this complex is usually a sign the component is overengineered, and the assistant will fill it unreliably or stop calling the tool altogether. Keep it to a small set of flat, single-type fields (one canonical name per concept \u2014 no aliases), and let component code handle formatting, fallbacks, and edge cases.`
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// src/commands/apps/push.ts
|
|
1782
|
+
import * as fs9 from "fs";
|
|
1783
|
+
import * as path10 from "path";
|
|
1384
1784
|
async function pushCommand(args) {
|
|
1385
1785
|
const slug = requireProjectStore();
|
|
1386
1786
|
const root = process.cwd();
|
|
1387
1787
|
rejectLegacyLayout(root);
|
|
1388
|
-
const componentsDir =
|
|
1389
|
-
if (!
|
|
1788
|
+
const componentsDir = path10.join(root, "components");
|
|
1789
|
+
if (!fs9.existsSync(componentsDir)) {
|
|
1390
1790
|
throw new Error(
|
|
1391
1791
|
"No components/ directory here. Run `gs apps init <name>` to scaffold the project root and your first component."
|
|
1392
1792
|
);
|
|
@@ -1445,53 +1845,43 @@ ${summary.join(", ")} (of ${outcomes.length}) \u2192 ${slug}
|
|
|
1445
1845
|
}
|
|
1446
1846
|
async function pushOne(opts) {
|
|
1447
1847
|
const { slug, root, name, force } = opts;
|
|
1448
|
-
const componentDir =
|
|
1449
|
-
if (!
|
|
1848
|
+
const componentDir = path10.join(root, "components", name);
|
|
1849
|
+
if (!fs9.existsSync(componentDir)) {
|
|
1450
1850
|
return {
|
|
1451
1851
|
name,
|
|
1452
1852
|
status: "failed",
|
|
1453
1853
|
message: `components/${name}/ does not exist`
|
|
1454
1854
|
};
|
|
1455
1855
|
}
|
|
1456
|
-
const manifestPath = opts.manifestPath ?
|
|
1457
|
-
const bundlePath = opts.bundlePath ?
|
|
1458
|
-
const sourcePath =
|
|
1459
|
-
if (!
|
|
1460
|
-
return { name, status: "failed", message: `manifest not found: ${manifestPath}` };
|
|
1461
|
-
}
|
|
1462
|
-
if (!fs8.existsSync(bundlePath)) {
|
|
1856
|
+
const manifestPath = opts.manifestPath ? path10.resolve(opts.manifestPath) : path10.join(componentDir, "manifest.json");
|
|
1857
|
+
const bundlePath = opts.bundlePath ? path10.resolve(opts.bundlePath) : path10.join(componentDir, "bundle.js");
|
|
1858
|
+
const sourcePath = path10.join(componentDir, "component.tsx");
|
|
1859
|
+
if (!fs9.existsSync(bundlePath)) {
|
|
1463
1860
|
return {
|
|
1464
1861
|
name,
|
|
1465
1862
|
status: "failed",
|
|
1466
1863
|
message: `bundle not found: ${bundlePath} (did you run \`npm run build\`?)`
|
|
1467
1864
|
};
|
|
1468
1865
|
}
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
name,
|
|
1479
|
-
status: "failed",
|
|
1480
|
-
message: `manifest is not valid JSON: ${err.message}`
|
|
1481
|
-
};
|
|
1866
|
+
const report = validateComponent({
|
|
1867
|
+
root,
|
|
1868
|
+
name,
|
|
1869
|
+
manifestPath,
|
|
1870
|
+
sourcePath
|
|
1871
|
+
});
|
|
1872
|
+
for (const diagnostic of warningsIn(report.diagnostics)) {
|
|
1873
|
+
process.stdout.write(` ${formatDiagnostic(diagnostic)}
|
|
1874
|
+
`);
|
|
1482
1875
|
}
|
|
1483
|
-
if (
|
|
1876
|
+
if (!report.ok) {
|
|
1484
1877
|
return {
|
|
1485
1878
|
name,
|
|
1486
1879
|
status: "failed",
|
|
1487
|
-
message:
|
|
1880
|
+
message: errorsIn(report.diagnostics).map((d) => `${d.file}: ${d.message}`).join(`
|
|
1881
|
+
${OUTCOME_INDENT}`)
|
|
1488
1882
|
};
|
|
1489
1883
|
}
|
|
1490
|
-
|
|
1491
|
-
assertSaneInputSchema(`components/${name}/manifest.json`, manifestInputSchema);
|
|
1492
|
-
} catch (err) {
|
|
1493
|
-
return { name, status: "failed", message: err.message };
|
|
1494
|
-
}
|
|
1884
|
+
const manifestText = fs9.readFileSync(manifestPath, "utf8");
|
|
1495
1885
|
if (!force) {
|
|
1496
1886
|
const hashes2 = computeComponentHashes(componentDir);
|
|
1497
1887
|
const sync = readSyncState(componentDir);
|
|
@@ -1500,8 +1890,8 @@ async function pushOne(opts) {
|
|
|
1500
1890
|
return { name, status: "unchanged" };
|
|
1501
1891
|
}
|
|
1502
1892
|
}
|
|
1503
|
-
const bundleText =
|
|
1504
|
-
const sourceText =
|
|
1893
|
+
const bundleText = fs9.readFileSync(bundlePath, "utf8");
|
|
1894
|
+
const sourceText = fs9.existsSync(sourcePath) ? fs9.readFileSync(sourcePath, "utf8") : null;
|
|
1505
1895
|
const form = new FormData();
|
|
1506
1896
|
form.append(
|
|
1507
1897
|
"manifest",
|
|
@@ -1545,15 +1935,15 @@ async function pushOne(opts) {
|
|
|
1545
1935
|
};
|
|
1546
1936
|
}
|
|
1547
1937
|
function listLocalComponents(componentsDir) {
|
|
1548
|
-
return
|
|
1549
|
-
const dir =
|
|
1550
|
-
return
|
|
1938
|
+
return fs9.readdirSync(componentsDir).filter((entry) => {
|
|
1939
|
+
const dir = path10.join(componentsDir, entry);
|
|
1940
|
+
return fs9.statSync(dir).isDirectory() && fs9.existsSync(path10.join(dir, "manifest.json"));
|
|
1551
1941
|
}).sort();
|
|
1552
1942
|
}
|
|
1553
1943
|
function rejectLegacyLayout(root) {
|
|
1554
|
-
const rootManifest =
|
|
1555
|
-
const componentsDir =
|
|
1556
|
-
if (
|
|
1944
|
+
const rootManifest = path10.join(root, "manifest.json");
|
|
1945
|
+
const componentsDir = path10.join(root, "components");
|
|
1946
|
+
if (fs9.existsSync(rootManifest) && !fs9.existsSync(componentsDir)) {
|
|
1557
1947
|
throw new Error(
|
|
1558
1948
|
[
|
|
1559
1949
|
"Detected the old single-component layout (manifest.json at the project root).",
|
|
@@ -1564,6 +1954,7 @@ function rejectLegacyLayout(root) {
|
|
|
1564
1954
|
);
|
|
1565
1955
|
}
|
|
1566
1956
|
}
|
|
1957
|
+
var OUTCOME_INDENT = " ".repeat(" failed ".length);
|
|
1567
1958
|
function printOutcome2(outcome) {
|
|
1568
1959
|
switch (outcome.status) {
|
|
1569
1960
|
case "pushed":
|
|
@@ -1794,8 +2185,8 @@ function formatList(list) {
|
|
|
1794
2185
|
}
|
|
1795
2186
|
|
|
1796
2187
|
// src/commands/configure/set.ts
|
|
1797
|
-
import * as
|
|
1798
|
-
import * as
|
|
2188
|
+
import * as fs10 from "fs";
|
|
2189
|
+
import * as path11 from "path";
|
|
1799
2190
|
var TEXT_FIELDS = [
|
|
1800
2191
|
"displayName",
|
|
1801
2192
|
"assistantName",
|
|
@@ -1855,11 +2246,11 @@ async function setConfig(args) {
|
|
|
1855
2246
|
`);
|
|
1856
2247
|
}
|
|
1857
2248
|
function readTextFile(filePath, flag) {
|
|
1858
|
-
const resolved =
|
|
1859
|
-
if (!
|
|
2249
|
+
const resolved = path11.resolve(filePath);
|
|
2250
|
+
if (!fs10.existsSync(resolved)) {
|
|
1860
2251
|
throw new Error(`${flag}: file not found: ${resolved}`);
|
|
1861
2252
|
}
|
|
1862
|
-
return
|
|
2253
|
+
return fs10.readFileSync(resolved, "utf8");
|
|
1863
2254
|
}
|
|
1864
2255
|
function parseThemeJson(raw, flag) {
|
|
1865
2256
|
let parsed;
|
|
@@ -1875,8 +2266,8 @@ function parseThemeJson(raw, flag) {
|
|
|
1875
2266
|
}
|
|
1876
2267
|
|
|
1877
2268
|
// src/commands/configure/upload.ts
|
|
1878
|
-
import * as
|
|
1879
|
-
import * as
|
|
2269
|
+
import * as fs11 from "fs";
|
|
2270
|
+
import * as path12 from "path";
|
|
1880
2271
|
|
|
1881
2272
|
// src/commands/configure/shared.ts
|
|
1882
2273
|
var ASSET_KINDS = ["icon", "logoLight", "logoDark"];
|
|
@@ -1901,20 +2292,20 @@ async function uploadAsset(args) {
|
|
|
1901
2292
|
if (!filePath) {
|
|
1902
2293
|
throw new Error(`Usage: gs configure upload <${ASSET_KINDS.join("|")}> <file>`);
|
|
1903
2294
|
}
|
|
1904
|
-
const resolved =
|
|
1905
|
-
if (!
|
|
2295
|
+
const resolved = path12.resolve(filePath);
|
|
2296
|
+
if (!fs11.existsSync(resolved)) {
|
|
1906
2297
|
throw new Error(`File not found: ${resolved}`);
|
|
1907
2298
|
}
|
|
1908
|
-
const mime = MIME_BY_EXT[
|
|
2299
|
+
const mime = MIME_BY_EXT[path12.extname(resolved).toLowerCase()];
|
|
1909
2300
|
if (!mime) {
|
|
1910
2301
|
throw new Error("Unsupported image type. Use .png, .jpg, or .webp.");
|
|
1911
2302
|
}
|
|
1912
|
-
const bytes =
|
|
2303
|
+
const bytes = fs11.readFileSync(resolved);
|
|
1913
2304
|
const form = new FormData();
|
|
1914
2305
|
form.append(
|
|
1915
2306
|
"file",
|
|
1916
2307
|
new Blob([new Uint8Array(bytes)], { type: mime }),
|
|
1917
|
-
|
|
2308
|
+
path12.basename(resolved)
|
|
1918
2309
|
);
|
|
1919
2310
|
const url = `${adminApiBase(slug)}/configure/upload/${kind}`;
|
|
1920
2311
|
const data = await request(url, {
|
|
@@ -2232,9 +2623,9 @@ function recentChangelog(text, minItems = 15) {
|
|
|
2232
2623
|
}
|
|
2233
2624
|
|
|
2234
2625
|
// src/version-check.ts
|
|
2235
|
-
import * as
|
|
2626
|
+
import * as fs12 from "fs";
|
|
2236
2627
|
import * as os3 from "os";
|
|
2237
|
-
import * as
|
|
2628
|
+
import * as path13 from "path";
|
|
2238
2629
|
var REFRESH_COMMAND = "__refresh-version-cache";
|
|
2239
2630
|
var PKG = "@greatstore/cli";
|
|
2240
2631
|
var REGISTRY_URL = `https://registry.npmjs.org/${PKG}/latest`;
|
|
@@ -2295,11 +2686,11 @@ async function fetchLatest() {
|
|
|
2295
2686
|
}
|
|
2296
2687
|
}
|
|
2297
2688
|
function cachePath(home) {
|
|
2298
|
-
return
|
|
2689
|
+
return path13.join(home, ".greatstore", "version-check.json");
|
|
2299
2690
|
}
|
|
2300
2691
|
function readCache(home) {
|
|
2301
2692
|
try {
|
|
2302
|
-
const raw =
|
|
2693
|
+
const raw = fs12.readFileSync(cachePath(home), "utf8");
|
|
2303
2694
|
const parsed = JSON.parse(raw);
|
|
2304
2695
|
if (typeof parsed.latest === "string" && typeof parsed.checkedAt === "number") {
|
|
2305
2696
|
return { latest: parsed.latest, checkedAt: parsed.checkedAt };
|
|
@@ -2311,8 +2702,8 @@ function readCache(home) {
|
|
|
2311
2702
|
function writeCache(home, cache3) {
|
|
2312
2703
|
try {
|
|
2313
2704
|
const file = cachePath(home);
|
|
2314
|
-
|
|
2315
|
-
|
|
2705
|
+
fs12.mkdirSync(path13.dirname(file), { recursive: true });
|
|
2706
|
+
fs12.writeFileSync(file, JSON.stringify(cache3));
|
|
2316
2707
|
} catch {
|
|
2317
2708
|
}
|
|
2318
2709
|
}
|
|
@@ -2334,8 +2725,8 @@ function parseVer(v) {
|
|
|
2334
2725
|
}
|
|
2335
2726
|
|
|
2336
2727
|
// src/index.ts
|
|
2337
|
-
var VERSION = true ? "0.0.
|
|
2338
|
-
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.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" : "";
|
|
2728
|
+
var VERSION = true ? "0.0.44" : "0.0.0-dev";
|
|
2729
|
+
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.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" : "";
|
|
2339
2730
|
var HELP = `gs \u2014 GreatStore CLI (v${VERSION})
|
|
2340
2731
|
|
|
2341
2732
|
Usage:
|