diff --git a/README.md b/README.md
index efde533b..21aa52fe 100644
--- a/README.md
+++ b/README.md
@@ -244,6 +244,7 @@ This collection would not be possible without the incredible work of the Claude
- **[zebbern/claude-code-guide](https://github.com/zebbern/claude-code-guide)**: Comprehensive Security suite & Guide (Source for ~60 new skills).
- **[alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills)**: Senior Engineering and PM toolkit.
- **[karanb192/awesome-claude-skills](https://github.com/karanb192/awesome-claude-skills)**: A massive list of verified skills for Claude Code.
+- **[zircote/.claude](https://github.com/zircote/.claude)**: Shopify development skill reference.
### Inspirations
@@ -270,6 +271,6 @@ For repository maintainers, add these topics to maximize discoverability:
claude-code, gemini-cli, codex-cli, antigravity, cursor, github-copilot, opencode,
agentic-skills, ai-coding, llm-tools, ai-agents, autonomous-coding, mcp,
ai-developer-tools, ai-pair-programming, vibe-coding, skill, skills, SKILL.md, rules.md, CLAUDE.md, GEMINI.md, CURSOR.md
-claude-code, gemini-cli, codex-cli, antigravity, cursor, github-copilot, opencode,
+claude-code, gemini-cli, codex-cli, antigravity, cursor, github-copilot, opencode,
agentic-skills, ai-coding, llm-tools, ai-agents, autonomous-coding, mcp
```
diff --git a/skills/shopify-development/README.md b/skills/shopify-development/README.md
new file mode 100644
index 00000000..998f63cb
--- /dev/null
+++ b/skills/shopify-development/README.md
@@ -0,0 +1,60 @@
+# Shopify Development Skill
+
+Comprehensive skill for building on Shopify platform: apps, extensions, themes, and API integrations.
+
+## Features
+
+- **App Development** - OAuth authentication, GraphQL Admin API, webhooks, billing integration
+- **UI Extensions** - Checkout, Admin, POS customizations with Polaris components
+- **Theme Development** - Liquid templating, sections, snippets
+- **Shopify Functions** - Custom discounts, payment, delivery rules
+
+## Structure
+
+```
+shopify-development/
+├── SKILL.md # Main skill file (AI-optimized)
+├── README.md # This file
+├── references/
+│ ├── app-development.md # OAuth, API, webhooks, billing
+│ ├── extensions.md # UI extensions, Functions
+│ └── themes.md # Liquid, theme architecture
+└── scripts/
+ ├── shopify_init.py # Interactive project scaffolding
+ ├── shopify_graphql.py # GraphQL utilities & templates
+ └── tests/ # Unit tests
+```
+
+## Validated GraphQL
+
+All GraphQL queries and mutations in this skill have been validated against Shopify Admin API 2026-01 schema using the official Shopify MCP.
+
+## Quick Start
+
+```bash
+# Install Shopify CLI
+npm install -g @shopify/cli@latest
+
+# Create new app
+shopify app init
+
+# Start development
+shopify app dev
+```
+
+## Usage Triggers
+
+This skill activates when the user mentions:
+
+- "shopify app", "shopify extension", "shopify theme"
+- "checkout extension", "admin extension", "POS extension"
+- "liquid template", "polaris", "shopify graphql"
+- "shopify webhook", "shopify billing", "metafields"
+
+## API Version
+
+Current: **2026-01** (Quarterly releases with 12-month support)
+
+## License
+
+MIT
diff --git a/skills/shopify-development/SKILL.md b/skills/shopify-development/SKILL.md
new file mode 100644
index 00000000..ea89ab10
--- /dev/null
+++ b/skills/shopify-development/SKILL.md
@@ -0,0 +1,366 @@
+---
+name: shopify-development
+description: |
+ Build Shopify apps, extensions, themes using GraphQL Admin API, Shopify CLI, Polaris UI, and Liquid.
+ TRIGGER: "shopify", "shopify app", "checkout extension", "admin extension", "POS extension",
+ "shopify theme", "liquid template", "polaris", "shopify graphql", "shopify webhook",
+ "shopify billing", "app subscription", "metafields", "shopify functions"
+---
+
+# Shopify Development Skill
+
+Use this skill when the user asks about:
+
+- Building Shopify apps or extensions
+- Creating checkout/admin/POS UI customizations
+- Developing themes with Liquid templating
+- Integrating with Shopify GraphQL or REST APIs
+- Implementing webhooks or billing
+- Working with metafields or Shopify Functions
+
+---
+
+## ROUTING: What to Build
+
+**IF user wants to integrate external services OR build merchant tools OR charge for features:**
+→ Build an **App** (see `references/app-development.md`)
+
+**IF user wants to customize checkout OR add admin UI OR create POS actions OR implement discount rules:**
+→ Build an **Extension** (see `references/extensions.md`)
+
+**IF user wants to customize storefront design OR modify product/collection pages:**
+→ Build a **Theme** (see `references/themes.md`)
+
+**IF user needs both backend logic AND storefront UI:**
+→ Build **App + Theme Extension** combination
+
+---
+
+## Shopify CLI Commands
+
+Install CLI:
+
+```bash
+npm install -g @shopify/cli@latest
+```
+
+Create and run app:
+
+```bash
+shopify app init # Create new app
+shopify app dev # Start dev server with tunnel
+shopify app deploy # Build and upload to Shopify
+```
+
+Generate extension:
+
+```bash
+shopify app generate extension --type checkout_ui_extension
+shopify app generate extension --type admin_action
+shopify app generate extension --type admin_block
+shopify app generate extension --type pos_ui_extension
+shopify app generate extension --type function
+```
+
+Theme development:
+
+```bash
+shopify theme init # Create new theme
+shopify theme dev # Start local preview at localhost:9292
+shopify theme pull --live # Pull live theme
+shopify theme push --development # Push to dev theme
+```
+
+---
+
+## Access Scopes
+
+Configure in `shopify.app.toml`:
+
+```toml
+[access_scopes]
+scopes = "read_products,write_products,read_orders,write_orders,read_customers"
+```
+
+Common scopes:
+
+- `read_products`, `write_products` - Product catalog access
+- `read_orders`, `write_orders` - Order management
+- `read_customers`, `write_customers` - Customer data
+- `read_inventory`, `write_inventory` - Stock levels
+- `read_fulfillments`, `write_fulfillments` - Order fulfillment
+
+---
+
+## GraphQL Patterns (Validated against API 2026-01)
+
+### Query Products
+
+```graphql
+query GetProducts($first: Int!, $query: String) {
+ products(first: $first, query: $query) {
+ edges {
+ node {
+ id
+ title
+ handle
+ status
+ variants(first: 5) {
+ edges {
+ node {
+ id
+ price
+ inventoryQuantity
+ }
+ }
+ }
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}
+```
+
+### Query Orders
+
+```graphql
+query GetOrders($first: Int!) {
+ orders(first: $first) {
+ edges {
+ node {
+ id
+ name
+ createdAt
+ displayFinancialStatus
+ totalPriceSet {
+ shopMoney {
+ amount
+ currencyCode
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### Set Metafields
+
+```graphql
+mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
+ metafieldsSet(metafields: $metafields) {
+ metafields {
+ id
+ namespace
+ key
+ value
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables example:
+
+```json
+{
+ "metafields": [
+ {
+ "ownerId": "gid://shopify/Product/123",
+ "namespace": "custom",
+ "key": "care_instructions",
+ "value": "Handle with care",
+ "type": "single_line_text_field"
+ }
+ ]
+}
+```
+
+---
+
+## Checkout Extension Example
+
+```tsx
+import {
+ reactExtension,
+ BlockStack,
+ TextField,
+ Checkbox,
+ useApplyAttributeChange,
+} from "@shopify/ui-extensions-react/checkout";
+
+export default reactExtension("purchase.checkout.block.render", () => (
+
+));
+
+function GiftMessage() {
+ const [isGift, setIsGift] = useState(false);
+ const [message, setMessage] = useState("");
+ const applyAttributeChange = useApplyAttributeChange();
+
+ useEffect(() => {
+ if (isGift && message) {
+ applyAttributeChange({
+ type: "updateAttribute",
+ key: "gift_message",
+ value: message,
+ });
+ }
+ }, [isGift, message]);
+
+ return (
+
+
+ This is a gift
+
+ {isGift && (
+
+ )}
+
+ );
+}
+```
+
+---
+
+## Liquid Template Example
+
+```liquid
+{% comment %} Product Card Snippet {% endcomment %}
+
+```
+
+---
+
+## Webhook Configuration
+
+In `shopify.app.toml`:
+
+```toml
+[webhooks]
+api_version = "2026-01"
+
+[[webhooks.subscriptions]]
+topics = ["orders/create", "orders/updated"]
+uri = "/webhooks/orders"
+
+[[webhooks.subscriptions]]
+topics = ["products/update"]
+uri = "/webhooks/products"
+
+# GDPR mandatory webhooks (required for app approval)
+[webhooks.privacy_compliance]
+customer_data_request_url = "/webhooks/gdpr/data-request"
+customer_deletion_url = "/webhooks/gdpr/customer-deletion"
+shop_deletion_url = "/webhooks/gdpr/shop-deletion"
+```
+
+---
+
+## Best Practices
+
+### API Usage
+
+- Use GraphQL over REST for new development
+- Request only fields you need (reduces query cost)
+- Implement cursor-based pagination with `pageInfo.endCursor`
+- Use bulk operations for processing more than 250 items
+- Handle rate limits with exponential backoff
+
+### Security
+
+- Store API credentials in environment variables
+- Always verify webhook HMAC signatures before processing
+- Validate OAuth state parameter to prevent CSRF
+- Request minimal access scopes
+- Use session tokens for embedded apps
+
+### Performance
+
+- Cache API responses when data doesn't change frequently
+- Use lazy loading in extensions
+- Optimize images in themes using `img_url` filter
+- Monitor GraphQL query costs via response headers
+
+---
+
+## Troubleshooting
+
+**IF you see rate limit errors:**
+→ Implement exponential backoff retry logic
+→ Switch to bulk operations for large datasets
+→ Monitor `X-Shopify-Shop-Api-Call-Limit` header
+
+**IF authentication fails:**
+→ Verify the access token is still valid
+→ Check that all required scopes were granted
+→ Ensure OAuth flow completed successfully
+
+**IF extension is not appearing:**
+→ Verify the extension target is correct
+→ Check that extension is published via `shopify app deploy`
+→ Confirm the app is installed on the test store
+
+**IF webhook is not receiving events:**
+→ Verify the webhook URL is publicly accessible
+→ Check HMAC signature validation logic
+→ Review webhook logs in Partner Dashboard
+
+**IF GraphQL query fails:**
+→ Validate query against schema (use GraphiQL explorer)
+→ Check for deprecated fields in error message
+→ Verify you have required access scopes
+
+---
+
+## Reference Files
+
+For detailed implementation guides, read these files:
+
+- `references/app-development.md` - OAuth authentication flow, GraphQL mutations for products/orders/billing, webhook handlers, billing API integration
+- `references/extensions.md` - Checkout UI components, Admin UI extensions, POS extensions, Shopify Functions for discounts/payment/delivery
+- `references/themes.md` - Liquid syntax reference, theme directory structure, sections and snippets, common patterns
+
+---
+
+## Scripts
+
+- `scripts/shopify_init.py` - Interactive project scaffolding. Run: `python scripts/shopify_init.py`
+- `scripts/shopify_graphql.py` - GraphQL utilities with query templates, pagination, rate limiting. Import: `from shopify_graphql import ShopifyGraphQL`
+
+---
+
+## Official Documentation Links
+
+- Shopify Developer Docs: https://shopify.dev/docs
+- GraphQL Admin API Reference: https://shopify.dev/docs/api/admin-graphql
+- Shopify CLI Reference: https://shopify.dev/docs/api/shopify-cli
+- Polaris Design System: https://polaris.shopify.com
+
+API Version: 2026-01 (quarterly releases, 12-month deprecation window)
diff --git a/skills/shopify-development/references/app-development.md b/skills/shopify-development/references/app-development.md
new file mode 100644
index 00000000..d1134c81
--- /dev/null
+++ b/skills/shopify-development/references/app-development.md
@@ -0,0 +1,578 @@
+# App Development Reference
+
+Guide for building Shopify apps with OAuth, GraphQL/REST APIs, webhooks, and billing.
+
+## OAuth Authentication
+
+### OAuth 2.0 Flow
+
+**1. Redirect to Authorization URL:**
+
+```
+https://{shop}.myshopify.com/admin/oauth/authorize?
+ client_id={api_key}&
+ scope={scopes}&
+ redirect_uri={redirect_uri}&
+ state={nonce}
+```
+
+**2. Handle Callback:**
+
+```javascript
+app.get("/auth/callback", async (req, res) => {
+ const { code, shop, state } = req.query;
+
+ // Verify state to prevent CSRF
+ if (state !== storedState) {
+ return res.status(403).send("Invalid state");
+ }
+
+ // Exchange code for access token
+ const accessToken = await exchangeCodeForToken(shop, code);
+
+ // Store token securely
+ await storeAccessToken(shop, accessToken);
+
+ res.redirect(`https://${shop}/admin/apps/${appHandle}`);
+});
+```
+
+**3. Exchange Code for Token:**
+
+```javascript
+async function exchangeCodeForToken(shop, code) {
+ const response = await fetch(`https://${shop}/admin/oauth/access_token`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ client_id: process.env.SHOPIFY_API_KEY,
+ client_secret: process.env.SHOPIFY_API_SECRET,
+ code,
+ }),
+ });
+
+ const { access_token } = await response.json();
+ return access_token;
+}
+```
+
+### Access Scopes
+
+**Common Scopes:**
+
+- `read_products`, `write_products` - Product catalog
+- `read_orders`, `write_orders` - Order management
+- `read_customers`, `write_customers` - Customer data
+- `read_inventory`, `write_inventory` - Stock levels
+- `read_fulfillments`, `write_fulfillments` - Order fulfillment
+- `read_shipping`, `write_shipping` - Shipping rates
+- `read_analytics` - Store analytics
+- `read_checkouts`, `write_checkouts` - Checkout data
+
+Full list: https://shopify.dev/api/usage/access-scopes
+
+### Session Tokens (Embedded Apps)
+
+For embedded apps using App Bridge:
+
+```javascript
+import { getSessionToken } from '@shopify/app-bridge/utilities';
+
+async function authenticatedFetch(url, options = {}) {
+ const app = createApp({ ... });
+ const token = await getSessionToken(app);
+
+ return fetch(url, {
+ ...options,
+ headers: {
+ ...options.headers,
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+}
+```
+
+## GraphQL Admin API
+
+### Making Requests
+
+```javascript
+async function graphqlRequest(shop, accessToken, query, variables = {}) {
+ const response = await fetch(
+ `https://${shop}/admin/api/2026-01/graphql.json`,
+ {
+ method: "POST",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ query, variables }),
+ },
+ );
+
+ const data = await response.json();
+
+ if (data.errors) {
+ throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
+ }
+
+ return data.data;
+}
+```
+
+### Product Operations
+
+**Create Product:**
+
+```graphql
+mutation CreateProduct($input: ProductInput!) {
+ productCreate(input: $input) {
+ product {
+ id
+ title
+ handle
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "input": {
+ "title": "New Product",
+ "productType": "Apparel",
+ "vendor": "Brand",
+ "status": "ACTIVE",
+ "variants": [
+ { "price": "29.99", "sku": "SKU-001", "inventoryQuantity": 100 }
+ ]
+ }
+}
+```
+
+**Update Product:**
+
+```graphql
+mutation UpdateProduct($input: ProductInput!) {
+ productUpdate(input: $input) {
+ product {
+ id
+ title
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+**Query Products:**
+
+```graphql
+query GetProducts($first: Int!, $query: String) {
+ products(first: $first, query: $query) {
+ edges {
+ node {
+ id
+ title
+ status
+ variants(first: 5) {
+ edges {
+ node {
+ id
+ price
+ inventoryQuantity
+ }
+ }
+ }
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}
+```
+
+### Order Operations
+
+**Query Orders:**
+
+```graphql
+query GetOrders($first: Int!) {
+ orders(first: $first) {
+ edges {
+ node {
+ id
+ name
+ createdAt
+ displayFinancialStatus
+ totalPriceSet {
+ shopMoney {
+ amount
+ currencyCode
+ }
+ }
+ customer {
+ email
+ firstName
+ lastName
+ }
+ }
+ }
+ }
+}
+```
+
+**Fulfill Order:**
+
+```graphql
+mutation FulfillOrder($fulfillment: FulfillmentInput!) {
+ fulfillmentCreate(fulfillment: $fulfillment) {
+ fulfillment {
+ id
+ status
+ trackingInfo {
+ number
+ url
+ }
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+## Webhooks
+
+### Configuration
+
+In `shopify.app.toml`:
+
+```toml
+[webhooks]
+api_version = "2025-01"
+
+[[webhooks.subscriptions]]
+topics = ["orders/create"]
+uri = "/webhooks/orders/create"
+
+[[webhooks.subscriptions]]
+topics = ["products/update"]
+uri = "/webhooks/products/update"
+
+[[webhooks.subscriptions]]
+topics = ["app/uninstalled"]
+uri = "/webhooks/app/uninstalled"
+
+# GDPR mandatory webhooks
+[webhooks.privacy_compliance]
+customer_data_request_url = "/webhooks/gdpr/data-request"
+customer_deletion_url = "/webhooks/gdpr/customer-deletion"
+shop_deletion_url = "/webhooks/gdpr/shop-deletion"
+```
+
+### Webhook Handler
+
+```javascript
+import crypto from "crypto";
+
+function verifyWebhook(req) {
+ const hmac = req.headers["x-shopify-hmac-sha256"];
+ const body = req.rawBody; // Raw body buffer
+
+ const hash = crypto
+ .createHmac("sha256", process.env.SHOPIFY_API_SECRET)
+ .update(body, "utf8")
+ .digest("base64");
+
+ return hmac === hash;
+}
+
+app.post("/webhooks/orders/create", async (req, res) => {
+ if (!verifyWebhook(req)) {
+ return res.status(401).send("Unauthorized");
+ }
+
+ const order = req.body;
+ console.log("New order:", order.id, order.name);
+
+ // Process order...
+
+ res.status(200).send("OK");
+});
+```
+
+### Common Webhook Topics
+
+**Orders:**
+
+- `orders/create`, `orders/updated`, `orders/delete`
+- `orders/paid`, `orders/cancelled`, `orders/fulfilled`
+
+**Products:**
+
+- `products/create`, `products/update`, `products/delete`
+
+**Customers:**
+
+- `customers/create`, `customers/update`, `customers/delete`
+
+**Inventory:**
+
+- `inventory_levels/update`
+
+**App:**
+
+- `app/uninstalled` (critical for cleanup)
+
+## Billing Integration
+
+### App Charges
+
+**One-time Charge:**
+
+```graphql
+mutation CreateCharge($input: AppPurchaseOneTimeInput!) {
+ appPurchaseOneTimeCreate(input: $input) {
+ appPurchaseOneTime {
+ id
+ name
+ price {
+ amount
+ }
+ status
+ confirmationUrl
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "input": {
+ "name": "Premium Feature",
+ "price": { "amount": 49.99, "currencyCode": "USD" },
+ "returnUrl": "https://your-app.com/billing/callback"
+ }
+}
+```
+
+**Recurring Charge (Subscription):**
+
+```graphql
+mutation CreateSubscription(
+ $name: String!
+ $returnUrl: URL!
+ $lineItems: [AppSubscriptionLineItemInput!]!
+ $trialDays: Int
+) {
+ appSubscriptionCreate(
+ name: $name
+ returnUrl: $returnUrl
+ lineItems: $lineItems
+ trialDays: $trialDays
+ ) {
+ appSubscription {
+ id
+ name
+ status
+ }
+ confirmationUrl
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "name": "Monthly Subscription",
+ "returnUrl": "https://your-app.com/billing/callback",
+ "trialDays": 7,
+ "lineItems": [
+ {
+ "plan": {
+ "appRecurringPricingDetails": {
+ "price": { "amount": 29.99, "currencyCode": "USD" },
+ "interval": "EVERY_30_DAYS"
+ }
+ }
+ }
+ ]
+}
+```
+
+**Usage-based Billing:**
+
+```graphql
+mutation CreateUsageCharge(
+ $subscriptionLineItemId: ID!
+ $price: MoneyInput!
+ $description: String!
+) {
+ appUsageRecordCreate(
+ subscriptionLineItemId: $subscriptionLineItemId
+ price: $price
+ description: $description
+ ) {
+ appUsageRecord {
+ id
+ price {
+ amount
+ currencyCode
+ }
+ description
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "subscriptionLineItemId": "gid://shopify/AppSubscriptionLineItem/123",
+ "price": { "amount": "5.00", "currencyCode": "USD" },
+ "description": "100 API calls used"
+}
+```
+
+## Metafields
+
+### Create/Update Metafields
+
+```graphql
+mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
+ metafieldsSet(metafields: $metafields) {
+ metafields {
+ id
+ namespace
+ key
+ value
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "metafields": [
+ {
+ "ownerId": "gid://shopify/Product/123",
+ "namespace": "custom",
+ "key": "instructions",
+ "value": "Handle with care",
+ "type": "single_line_text_field"
+ }
+ ]
+}
+```
+
+**Metafield Types:**
+
+- `single_line_text_field`, `multi_line_text_field`
+- `number_integer`, `number_decimal`
+- `date`, `date_time`
+- `url`, `json`
+- `file_reference`, `product_reference`
+
+## Rate Limiting
+
+### GraphQL Cost-Based Limits
+
+**Limits:**
+
+- Available points: 2000
+- Restore rate: 100 points/second
+- Max query cost: 2000
+
+**Check Cost:**
+
+```javascript
+const response = await graphqlRequest(shop, token, query);
+const cost = response.extensions?.cost;
+
+console.log(
+ `Cost: ${cost.actualQueryCost}/${cost.throttleStatus.maximumAvailable}`,
+);
+```
+
+**Handle Throttling:**
+
+```javascript
+async function graphqlWithRetry(shop, token, query, retries = 3) {
+ for (let i = 0; i < retries; i++) {
+ try {
+ return await graphqlRequest(shop, token, query);
+ } catch (error) {
+ if (error.message.includes("Throttled") && i < retries - 1) {
+ await sleep(Math.pow(2, i) * 1000); // Exponential backoff
+ continue;
+ }
+ throw error;
+ }
+ }
+}
+```
+
+## Best Practices
+
+**Security:**
+
+- Store credentials in environment variables
+- Verify webhook HMAC signatures
+- Validate OAuth state parameter
+- Use HTTPS for all endpoints
+- Implement rate limiting on your endpoints
+
+**Performance:**
+
+- Cache access tokens securely
+- Use bulk operations for large datasets
+- Implement pagination for queries
+- Monitor GraphQL query costs
+
+**Reliability:**
+
+- Implement exponential backoff for retries
+- Handle webhook delivery failures
+- Log errors for debugging
+- Monitor app health metrics
+
+**Compliance:**
+
+- Implement GDPR webhooks (mandatory)
+- Handle customer data deletion requests
+- Provide data export functionality
+- Follow data retention policies
diff --git a/skills/shopify-development/references/extensions.md b/skills/shopify-development/references/extensions.md
new file mode 100644
index 00000000..8d42267f
--- /dev/null
+++ b/skills/shopify-development/references/extensions.md
@@ -0,0 +1,555 @@
+# Extensions Reference
+
+Guide for building UI extensions and Shopify Functions.
+
+## Checkout UI Extensions
+
+Customize checkout and thank-you pages with native-rendered components.
+
+### Extension Points
+
+**Block Targets (Merchant-Configurable):**
+
+- `purchase.checkout.block.render` - Main checkout
+- `purchase.thank-you.block.render` - Thank you page
+
+**Static Targets (Fixed Position):**
+
+- `purchase.checkout.header.render-after`
+- `purchase.checkout.contact.render-before`
+- `purchase.checkout.shipping-option-list.render-after`
+- `purchase.checkout.payment-method-list.render-after`
+- `purchase.checkout.footer.render-before`
+
+### Setup
+
+```bash
+shopify app generate extension --type checkout_ui_extension
+```
+
+Configuration (`shopify.extension.toml`):
+
+```toml
+api_version = "2026-01"
+name = "gift-message"
+type = "ui_extension"
+
+[[extensions.targeting]]
+target = "purchase.checkout.block.render"
+
+[capabilities]
+network_access = true
+api_access = true
+```
+
+### Basic Example
+
+```javascript
+import {
+ reactExtension,
+ BlockStack,
+ TextField,
+ Checkbox,
+ useApi,
+} from "@shopify/ui-extensions-react/checkout";
+
+export default reactExtension("purchase.checkout.block.render", () => (
+
+));
+
+function Extension() {
+ const [message, setMessage] = useState("");
+ const [isGift, setIsGift] = useState(false);
+ const { applyAttributeChange } = useApi();
+
+ useEffect(() => {
+ if (isGift) {
+ applyAttributeChange({
+ type: "updateAttribute",
+ key: "gift_message",
+ value: message,
+ });
+ }
+ }, [message, isGift]);
+
+ return (
+
+
+ This is a gift
+
+ {isGift && (
+
+ )}
+
+ );
+}
+```
+
+### Common Hooks
+
+**useApi:**
+
+```javascript
+const { extensionPoint, shop, storefront, i18n, sessionToken } = useApi();
+```
+
+**useCartLines:**
+
+```javascript
+const lines = useCartLines();
+lines.forEach((line) => {
+ console.log(line.merchandise.product.title, line.quantity);
+});
+```
+
+**useShippingAddress:**
+
+```javascript
+const address = useShippingAddress();
+console.log(address.city, address.countryCode);
+```
+
+**useApplyCartLinesChange:**
+
+```javascript
+const applyChange = useApplyCartLinesChange();
+
+async function addItem() {
+ await applyChange({
+ type: "addCartLine",
+ merchandiseId: "gid://shopify/ProductVariant/123",
+ quantity: 1,
+ });
+}
+```
+
+### Core Components
+
+**Layout:**
+
+- `BlockStack` - Vertical stacking
+- `InlineStack` - Horizontal layout
+- `Grid`, `GridItem` - Grid layout
+- `View` - Container
+- `Divider` - Separator
+
+**Input:**
+
+- `TextField` - Text input
+- `Checkbox` - Boolean
+- `Select` - Dropdown
+- `DatePicker` - Date selection
+- `Form` - Form wrapper
+
+**Display:**
+
+- `Text`, `Heading` - Typography
+- `Banner` - Messages
+- `Badge` - Status
+- `Image` - Images
+- `Link` - Hyperlinks
+- `List`, `ListItem` - Lists
+
+**Interactive:**
+
+- `Button` - Actions
+- `Modal` - Overlays
+- `Pressable` - Click areas
+
+## Admin UI Extensions
+
+Extend Shopify admin interface.
+
+### Admin Action
+
+Custom actions on resource pages.
+
+```bash
+shopify app generate extension --type admin_action
+```
+
+```javascript
+import {
+ reactExtension,
+ AdminAction,
+ Button,
+} from "@shopify/ui-extensions-react/admin";
+
+export default reactExtension("admin.product-details.action.render", () => (
+
+));
+
+function Extension() {
+ const { data } = useData();
+
+ async function handleExport() {
+ const response = await fetch("/api/export", {
+ method: "POST",
+ body: JSON.stringify({ productId: data.product.id }),
+ });
+ console.log("Exported:", await response.json());
+ }
+
+ return (
+ Export}
+ />
+ );
+}
+```
+
+**Targets:**
+
+- `admin.product-details.action.render`
+- `admin.order-details.action.render`
+- `admin.customer-details.action.render`
+
+### Admin Block
+
+Embedded content in admin pages.
+
+```javascript
+import {
+ reactExtension,
+ BlockStack,
+ Text,
+ Badge,
+} from "@shopify/ui-extensions-react/admin";
+
+export default reactExtension("admin.product-details.block.render", () => (
+
+));
+
+function Extension() {
+ const { data } = useData();
+ const [analytics, setAnalytics] = useState(null);
+
+ useEffect(() => {
+ fetchAnalytics(data.product.id).then(setAnalytics);
+ }, []);
+
+ return (
+
+ Product Analytics
+ Views: {analytics?.views || 0}
+ Conversions: {analytics?.conversions || 0}
+
+ {analytics?.trending ? "Trending" : "Normal"}
+
+
+ );
+}
+```
+
+**Targets:**
+
+- `admin.product-details.block.render`
+- `admin.order-details.block.render`
+- `admin.customer-details.block.render`
+
+## POS UI Extensions
+
+Customize Point of Sale experience.
+
+### Smart Grid Tile
+
+Quick access action on POS home screen.
+
+```javascript
+import {
+ reactExtension,
+ SmartGridTile,
+} from "@shopify/ui-extensions-react/pos";
+
+export default reactExtension("pos.home.tile.render", () => );
+
+function Extension() {
+ function handlePress() {
+ // Navigate to custom workflow
+ }
+
+ return (
+
+ );
+}
+```
+
+### POS Modal
+
+Full-screen workflow.
+
+```javascript
+import {
+ reactExtension,
+ Screen,
+ BlockStack,
+ Button,
+ TextField,
+} from "@shopify/ui-extensions-react/pos";
+
+export default reactExtension("pos.home.modal.render", () => );
+
+function Extension() {
+ const { navigation } = useApi();
+ const [amount, setAmount] = useState("");
+
+ function handleIssue() {
+ // Issue gift card
+ navigation.pop();
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+## Customer Account Extensions
+
+Customize customer account pages.
+
+### Order Status Extension
+
+```javascript
+import {
+ reactExtension,
+ BlockStack,
+ Text,
+ Button,
+} from "@shopify/ui-extensions-react/customer-account";
+
+export default reactExtension(
+ "customer-account.order-status.block.render",
+ () => ,
+);
+
+function Extension() {
+ const { order } = useApi();
+
+ function handleReturn() {
+ // Initiate return
+ }
+
+ return (
+
+ Need to return?
+ Start return for order {order.name}
+
+
+ );
+}
+```
+
+**Targets:**
+
+- `customer-account.order-status.block.render`
+- `customer-account.order-index.block.render`
+- `customer-account.profile.block.render`
+
+## Shopify Functions
+
+Serverless backend customization.
+
+### Function Types
+
+**Discounts:**
+
+- `order_discount` - Order-level discounts
+- `product_discount` - Product-specific discounts
+- `shipping_discount` - Shipping discounts
+
+**Payment Customization:**
+
+- Hide/rename/reorder payment methods
+
+**Delivery Customization:**
+
+- Custom shipping options
+- Delivery rules
+
+**Validation:**
+
+- Cart validation rules
+- Checkout validation
+
+### Create Function
+
+```bash
+shopify app generate extension --type function
+```
+
+### Order Discount Function
+
+```javascript
+// input.graphql
+query Input {
+ cart {
+ lines {
+ quantity
+ merchandise {
+ ... on ProductVariant {
+ product {
+ hasTag(tag: "bulk-discount")
+ }
+ }
+ }
+ }
+ }
+}
+
+// function.js
+export default function orderDiscount(input) {
+ const targets = input.cart.lines
+ .filter(line => line.merchandise.product.hasTag)
+ .map(line => ({
+ productVariant: { id: line.merchandise.id }
+ }));
+
+ if (targets.length === 0) {
+ return { discounts: [] };
+ }
+
+ return {
+ discounts: [{
+ targets,
+ value: {
+ percentage: {
+ value: 10 // 10% discount
+ }
+ }
+ }]
+ };
+}
+```
+
+### Payment Customization Function
+
+```javascript
+export default function paymentCustomization(input) {
+ const hidePaymentMethods = input.cart.lines.some(
+ (line) => line.merchandise.product.hasTag,
+ );
+
+ if (!hidePaymentMethods) {
+ return { operations: [] };
+ }
+
+ return {
+ operations: [
+ {
+ hide: {
+ paymentMethodId: "gid://shopify/PaymentMethod/123",
+ },
+ },
+ ],
+ };
+}
+```
+
+### Validation Function
+
+```javascript
+export default function cartValidation(input) {
+ const errors = [];
+
+ // Max 5 items per cart
+ if (input.cart.lines.length > 5) {
+ errors.push({
+ localizedMessage: "Maximum 5 items allowed per order",
+ target: "cart",
+ });
+ }
+
+ // Min $50 for wholesale
+ const isWholesale = input.cart.lines.some(
+ (line) => line.merchandise.product.hasTag,
+ );
+
+ if (isWholesale && input.cart.cost.totalAmount.amount < 50) {
+ errors.push({
+ localizedMessage: "Wholesale orders require $50 minimum",
+ target: "cart",
+ });
+ }
+
+ return { errors };
+}
+```
+
+## Network Requests
+
+Extensions can call external APIs.
+
+```javascript
+import { useApi } from "@shopify/ui-extensions-react/checkout";
+
+function Extension() {
+ const { sessionToken } = useApi();
+
+ async function fetchData() {
+ const token = await sessionToken.get();
+
+ const response = await fetch("https://your-app.com/api/data", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ return await response.json();
+ }
+}
+```
+
+## Best Practices
+
+**Performance:**
+
+- Lazy load data
+- Memoize expensive computations
+- Use loading states
+- Minimize re-renders
+
+**UX:**
+
+- Provide clear error messages
+- Show loading indicators
+- Validate inputs
+- Support keyboard navigation
+
+**Security:**
+
+- Verify session tokens on backend
+- Sanitize user input
+- Use HTTPS for all requests
+- Don't expose sensitive data
+
+**Testing:**
+
+- Test on development stores
+- Verify mobile/desktop
+- Check accessibility
+- Test edge cases
+
+## Resources
+
+- Checkout Extensions: https://shopify.dev/docs/api/checkout-extensions
+- Admin Extensions: https://shopify.dev/docs/apps/admin/extensions
+- Functions: https://shopify.dev/docs/apps/functions
+- Components: https://shopify.dev/docs/api/checkout-ui-extensions/components
diff --git a/skills/shopify-development/references/themes.md b/skills/shopify-development/references/themes.md
new file mode 100644
index 00000000..2fc1c2b9
--- /dev/null
+++ b/skills/shopify-development/references/themes.md
@@ -0,0 +1,498 @@
+# Themes Reference
+
+Guide for developing Shopify themes with Liquid templating.
+
+## Liquid Templating
+
+### Syntax Basics
+
+**Objects (Output):**
+```liquid
+{{ product.title }}
+{{ product.price | money }}
+{{ customer.email }}
+```
+
+**Tags (Logic):**
+```liquid
+{% if product.available %}
+
+{% else %}
+ Sold Out
+{% endif %}
+
+{% for product in collection.products %}
+ {{ product.title }}
+{% endfor %}
+
+{% case product.type %}
+ {% when 'Clothing' %}
+ Apparel
+ {% when 'Shoes' %}
+ Footwear
+ {% else %}
+ Other
+{% endcase %}
+```
+
+**Filters (Transform):**
+```liquid
+{{ product.title | upcase }}
+{{ product.price | money }}
+{{ product.description | strip_html | truncate: 100 }}
+{{ product.image | img_url: 'medium' }}
+{{ 'now' | date: '%B %d, %Y' }}
+```
+
+### Common Objects
+
+**Product:**
+```liquid
+{{ product.id }}
+{{ product.title }}
+{{ product.handle }}
+{{ product.description }}
+{{ product.price }}
+{{ product.compare_at_price }}
+{{ product.available }}
+{{ product.type }}
+{{ product.vendor }}
+{{ product.tags }}
+{{ product.images }}
+{{ product.variants }}
+{{ product.featured_image }}
+{{ product.url }}
+```
+
+**Collection:**
+```liquid
+{{ collection.title }}
+{{ collection.handle }}
+{{ collection.description }}
+{{ collection.products }}
+{{ collection.products_count }}
+{{ collection.image }}
+{{ collection.url }}
+```
+
+**Cart:**
+```liquid
+{{ cart.item_count }}
+{{ cart.total_price }}
+{{ cart.items }}
+{{ cart.note }}
+{{ cart.attributes }}
+```
+
+**Customer:**
+```liquid
+{{ customer.email }}
+{{ customer.first_name }}
+{{ customer.last_name }}
+{{ customer.orders_count }}
+{{ customer.total_spent }}
+{{ customer.addresses }}
+{{ customer.default_address }}
+```
+
+**Shop:**
+```liquid
+{{ shop.name }}
+{{ shop.email }}
+{{ shop.domain }}
+{{ shop.currency }}
+{{ shop.money_format }}
+{{ shop.enabled_payment_types }}
+```
+
+### Common Filters
+
+**String:**
+- `upcase`, `downcase`, `capitalize`
+- `strip_html`, `strip_newlines`
+- `truncate: 100`, `truncatewords: 20`
+- `replace: 'old', 'new'`
+
+**Number:**
+- `money` - Format currency
+- `round`, `ceil`, `floor`
+- `times`, `divided_by`, `plus`, `minus`
+
+**Array:**
+- `join: ', '`
+- `first`, `last`
+- `size`
+- `map: 'property'`
+- `where: 'property', 'value'`
+
+**URL:**
+- `img_url: 'size'` - Image URL
+- `url_for_type`, `url_for_vendor`
+- `link_to`, `link_to_type`
+
+**Date:**
+- `date: '%B %d, %Y'`
+
+## Theme Architecture
+
+### Directory Structure
+
+```
+theme/
+├── assets/ # CSS, JS, images
+├── config/ # Theme settings
+│ ├── settings_schema.json
+│ └── settings_data.json
+├── layout/ # Base templates
+│ └── theme.liquid
+├── locales/ # Translations
+│ └── en.default.json
+├── sections/ # Reusable blocks
+│ ├── header.liquid
+│ ├── footer.liquid
+│ └── product-grid.liquid
+├── snippets/ # Small components
+│ ├── product-card.liquid
+│ └── icon.liquid
+└── templates/ # Page templates
+ ├── index.json
+ ├── product.json
+ ├── collection.json
+ └── cart.liquid
+```
+
+### Layout
+
+Base template wrapping all pages (`layout/theme.liquid`):
+
+```liquid
+
+
+
+
+
+ {{ page_title }}
+
+ {{ content_for_header }}
+
+
+
+
+ {% section 'header' %}
+
+
+ {{ content_for_layout }}
+
+
+ {% section 'footer' %}
+
+
+
+
+```
+
+### Templates
+
+Page-specific structures (`templates/product.json`):
+
+```json
+{
+ "sections": {
+ "main": {
+ "type": "product-template",
+ "settings": {
+ "show_vendor": true,
+ "show_quantity_selector": true
+ }
+ },
+ "recommendations": {
+ "type": "product-recommendations"
+ }
+ },
+ "order": ["main", "recommendations"]
+}
+```
+
+Legacy format (`templates/product.liquid`):
+```liquid
+
+
+

+
+
+
+
{{ product.title }}
+
{{ product.price | money }}
+
+ {% form 'product', product %}
+
+
+
+ {% endform %}
+
+
+```
+
+### Sections
+
+Reusable content blocks (`sections/product-grid.liquid`):
+
+```liquid
+
+ {% for product in section.settings.collection.products %}
+
+ {% endfor %}
+
+
+{% schema %}
+{
+ "name": "Product Grid",
+ "settings": [
+ {
+ "type": "collection",
+ "id": "collection",
+ "label": "Collection"
+ },
+ {
+ "type": "range",
+ "id": "products_per_row",
+ "min": 2,
+ "max": 5,
+ "step": 1,
+ "default": 4,
+ "label": "Products per row"
+ }
+ ],
+ "presets": [
+ {
+ "name": "Product Grid"
+ }
+ ]
+}
+{% endschema %}
+```
+
+### Snippets
+
+Small reusable components (`snippets/product-card.liquid`):
+
+```liquid
+
+```
+
+Include snippet:
+```liquid
+{% render 'product-card', product: product %}
+```
+
+## Development Workflow
+
+### Setup
+
+```bash
+# Initialize new theme
+shopify theme init
+
+# Choose Dawn (reference theme) or blank
+```
+
+### Local Development
+
+```bash
+# Start local server
+shopify theme dev
+
+# Preview at http://localhost:9292
+# Changes auto-sync to development theme
+```
+
+### Pull Theme
+
+```bash
+# Pull live theme
+shopify theme pull --live
+
+# Pull specific theme
+shopify theme pull --theme=123456789
+
+# Pull only templates
+shopify theme pull --only=templates
+```
+
+### Push Theme
+
+```bash
+# Push to development theme
+shopify theme push --development
+
+# Create new unpublished theme
+shopify theme push --unpublished
+
+# Push specific files
+shopify theme push --only=sections,snippets
+```
+
+### Theme Check
+
+Lint theme code:
+```bash
+shopify theme check
+shopify theme check --auto-correct
+```
+
+## Common Patterns
+
+### Product Form with Variants
+
+```liquid
+{% form 'product', product %}
+ {% unless product.has_only_default_variant %}
+ {% for option in product.options_with_values %}
+
+
+
+
+ {% endfor %}
+ {% endunless %}
+
+
+
+
+
+{% endform %}
+```
+
+### Pagination
+
+```liquid
+{% paginate collection.products by 12 %}
+ {% for product in collection.products %}
+ {% render 'product-card', product: product %}
+ {% endfor %}
+
+ {% if paginate.pages > 1 %}
+
+ {% endif %}
+{% endpaginate %}
+```
+
+### Cart AJAX
+
+```javascript
+// Add to cart
+fetch('/cart/add.js', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: variantId,
+ quantity: 1
+ })
+})
+.then(res => res.json())
+.then(item => console.log('Added:', item));
+
+// Get cart
+fetch('/cart.js')
+ .then(res => res.json())
+ .then(cart => console.log('Cart:', cart));
+
+// Update cart
+fetch('/cart/change.js', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: lineItemKey,
+ quantity: 2
+ })
+})
+.then(res => res.json());
+```
+
+## Metafields in Themes
+
+Access custom data:
+
+```liquid
+{{ product.metafields.custom.care_instructions }}
+{{ product.metafields.custom.material.value }}
+
+{% if product.metafields.custom.featured %}
+ Featured
+{% endif %}
+```
+
+## Best Practices
+
+**Performance:**
+- Optimize images (use appropriate sizes)
+- Minimize Liquid logic complexity
+- Use lazy loading for images
+- Defer non-critical JavaScript
+
+**Accessibility:**
+- Use semantic HTML
+- Include alt text for images
+- Support keyboard navigation
+- Ensure sufficient color contrast
+
+**SEO:**
+- Use descriptive page titles
+- Include meta descriptions
+- Structure content with headings
+- Implement schema markup
+
+**Code Quality:**
+- Follow Shopify theme guidelines
+- Use consistent naming conventions
+- Comment complex logic
+- Keep sections focused and reusable
+
+## Resources
+
+- Theme Development: https://shopify.dev/docs/themes
+- Liquid Reference: https://shopify.dev/docs/api/liquid
+- Dawn Theme: https://github.com/Shopify/dawn
+- Theme Check: https://shopify.dev/docs/themes/tools/theme-check
diff --git a/skills/shopify-development/scripts/.gitignore b/skills/shopify-development/scripts/.gitignore
new file mode 100644
index 00000000..8abb6f18
--- /dev/null
+++ b/skills/shopify-development/scripts/.gitignore
@@ -0,0 +1,49 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Testing
+.coverage
+.pytest_cache/
+htmlcov/
+.tox/
+.nox/
+coverage.xml
+*.cover
+*.py,cover
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/skills/shopify-development/scripts/requirements.txt b/skills/shopify-development/scripts/requirements.txt
new file mode 100644
index 00000000..4613a2ba
--- /dev/null
+++ b/skills/shopify-development/scripts/requirements.txt
@@ -0,0 +1,19 @@
+# Shopify Skill Dependencies
+# Python 3.10+ required
+
+# No Python package dependencies - uses only standard library
+
+# Testing dependencies (dev)
+pytest>=8.0.0
+pytest-cov>=4.1.0
+pytest-mock>=3.12.0
+
+# Note: This script requires the Shopify CLI tool
+# Install Shopify CLI:
+# npm install -g @shopify/cli @shopify/theme
+# or via Homebrew (macOS):
+# brew tap shopify/shopify
+# brew install shopify-cli
+#
+# Authenticate with:
+# shopify auth login
diff --git a/skills/shopify-development/scripts/shopify_graphql.py b/skills/shopify-development/scripts/shopify_graphql.py
new file mode 100644
index 00000000..ec8af3cb
--- /dev/null
+++ b/skills/shopify-development/scripts/shopify_graphql.py
@@ -0,0 +1,428 @@
+#!/usr/bin/env python3
+"""
+Shopify GraphQL Utilities
+
+Helper functions for common Shopify GraphQL operations.
+Provides query templates, pagination helpers, and rate limit handling.
+
+Usage:
+ from shopify_graphql import ShopifyGraphQL
+
+ client = ShopifyGraphQL(shop_domain, access_token)
+ products = client.get_products(first=10)
+"""
+
+import os
+import time
+import json
+from typing import Dict, List, Optional, Any, Generator
+from dataclasses import dataclass
+from urllib.request import Request, urlopen
+from urllib.error import HTTPError
+
+
+# API Configuration
+API_VERSION = "2026-01"
+MAX_RETRIES = 3
+RETRY_DELAY = 1.0 # seconds
+
+
+@dataclass
+class GraphQLResponse:
+ """Container for GraphQL response data."""
+ data: Optional[Dict[str, Any]] = None
+ errors: Optional[List[Dict[str, Any]]] = None
+ extensions: Optional[Dict[str, Any]] = None
+
+ @property
+ def is_success(self) -> bool:
+ return self.errors is None or len(self.errors) == 0
+
+ @property
+ def query_cost(self) -> Optional[int]:
+ """Get the actual query cost from extensions."""
+ if self.extensions and 'cost' in self.extensions:
+ return self.extensions['cost'].get('actualQueryCost')
+ return None
+
+
+class ShopifyGraphQL:
+ """
+ Shopify GraphQL API client with built-in utilities.
+
+ Features:
+ - Query templates for common operations
+ - Automatic pagination
+ - Rate limit handling with exponential backoff
+ - Response parsing helpers
+ """
+
+ def __init__(self, shop_domain: str, access_token: str):
+ """
+ Initialize the GraphQL client.
+
+ Args:
+ shop_domain: Store domain (e.g., 'my-store.myshopify.com')
+ access_token: Admin API access token
+ """
+ self.shop_domain = shop_domain.replace('https://', '').replace('http://', '')
+ self.access_token = access_token
+ self.base_url = f"https://{self.shop_domain}/admin/api/{API_VERSION}/graphql.json"
+
+ def execute(self, query: str, variables: Optional[Dict] = None) -> GraphQLResponse:
+ """
+ Execute a GraphQL query/mutation.
+
+ Args:
+ query: GraphQL query string
+ variables: Query variables
+
+ Returns:
+ GraphQLResponse object
+ """
+ payload = {"query": query}
+ if variables:
+ payload["variables"] = variables
+
+ headers = {
+ "Content-Type": "application/json",
+ "X-Shopify-Access-Token": self.access_token
+ }
+
+ for attempt in range(MAX_RETRIES):
+ try:
+ request = Request(
+ self.base_url,
+ data=json.dumps(payload).encode('utf-8'),
+ headers=headers,
+ method='POST'
+ )
+
+ with urlopen(request, timeout=30) as response:
+ result = json.loads(response.read().decode('utf-8'))
+ return GraphQLResponse(
+ data=result.get('data'),
+ errors=result.get('errors'),
+ extensions=result.get('extensions')
+ )
+
+ except HTTPError as e:
+ if e.code == 429: # Rate limited
+ delay = RETRY_DELAY * (2 ** attempt)
+ print(f"Rate limited. Retrying in {delay}s...")
+ time.sleep(delay)
+ continue
+ raise
+ except Exception as e:
+ if attempt == MAX_RETRIES - 1:
+ raise
+ time.sleep(RETRY_DELAY)
+
+ return GraphQLResponse(errors=[{"message": "Max retries exceeded"}])
+
+ # ==================== Query Templates ====================
+
+ def get_products(
+ self,
+ first: int = 10,
+ query: Optional[str] = None,
+ after: Optional[str] = None
+ ) -> GraphQLResponse:
+ """
+ Query products with pagination.
+
+ Args:
+ first: Number of products to fetch (max 250)
+ query: Optional search query
+ after: Cursor for pagination
+ """
+ gql = """
+ query GetProducts($first: Int!, $query: String, $after: String) {
+ products(first: $first, query: $query, after: $after) {
+ edges {
+ node {
+ id
+ title
+ handle
+ status
+ totalInventory
+ variants(first: 5) {
+ edges {
+ node {
+ id
+ title
+ price
+ inventoryQuantity
+ sku
+ }
+ }
+ }
+ }
+ cursor
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ """
+ return self.execute(gql, {"first": first, "query": query, "after": after})
+
+ def get_orders(
+ self,
+ first: int = 10,
+ query: Optional[str] = None,
+ after: Optional[str] = None
+ ) -> GraphQLResponse:
+ """
+ Query orders with pagination.
+
+ Args:
+ first: Number of orders to fetch (max 250)
+ query: Optional search query (e.g., "financial_status:paid")
+ after: Cursor for pagination
+ """
+ gql = """
+ query GetOrders($first: Int!, $query: String, $after: String) {
+ orders(first: $first, query: $query, after: $after) {
+ edges {
+ node {
+ id
+ name
+ createdAt
+ displayFinancialStatus
+ displayFulfillmentStatus
+ totalPriceSet {
+ shopMoney { amount currencyCode }
+ }
+ customer {
+ id
+ firstName
+ lastName
+ }
+ lineItems(first: 5) {
+ edges {
+ node {
+ title
+ quantity
+ }
+ }
+ }
+ }
+ cursor
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ """
+ return self.execute(gql, {"first": first, "query": query, "after": after})
+
+ def get_customers(
+ self,
+ first: int = 10,
+ query: Optional[str] = None,
+ after: Optional[str] = None
+ ) -> GraphQLResponse:
+ """
+ Query customers with pagination.
+
+ Args:
+ first: Number of customers to fetch (max 250)
+ query: Optional search query
+ after: Cursor for pagination
+ """
+ gql = """
+ query GetCustomers($first: Int!, $query: String, $after: String) {
+ customers(first: $first, query: $query, after: $after) {
+ edges {
+ node {
+ id
+ firstName
+ lastName
+ displayName
+ defaultEmailAddress {
+ emailAddress
+ }
+ numberOfOrders
+ amountSpent {
+ amount
+ currencyCode
+ }
+ }
+ cursor
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ """
+ return self.execute(gql, {"first": first, "query": query, "after": after})
+
+ def set_metafields(self, metafields: List[Dict]) -> GraphQLResponse:
+ """
+ Set metafields on resources.
+
+ Args:
+ metafields: List of metafield inputs, each containing:
+ - ownerId: Resource GID
+ - namespace: Metafield namespace
+ - key: Metafield key
+ - value: Metafield value
+ - type: Metafield type
+ """
+ gql = """
+ mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
+ metafieldsSet(metafields: $metafields) {
+ metafields {
+ id
+ namespace
+ key
+ value
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ """
+ return self.execute(gql, {"metafields": metafields})
+
+ # ==================== Pagination Helpers ====================
+
+ def paginate_products(
+ self,
+ batch_size: int = 50,
+ query: Optional[str] = None
+ ) -> Generator[Dict, None, None]:
+ """
+ Generator that yields all products with automatic pagination.
+
+ Args:
+ batch_size: Products per request (max 250)
+ query: Optional search query
+
+ Yields:
+ Product dictionaries
+ """
+ cursor = None
+ while True:
+ response = self.get_products(first=batch_size, query=query, after=cursor)
+
+ if not response.is_success or not response.data:
+ break
+
+ products = response.data.get('products', {})
+ edges = products.get('edges', [])
+
+ for edge in edges:
+ yield edge['node']
+
+ page_info = products.get('pageInfo', {})
+ if not page_info.get('hasNextPage'):
+ break
+
+ cursor = page_info.get('endCursor')
+
+ def paginate_orders(
+ self,
+ batch_size: int = 50,
+ query: Optional[str] = None
+ ) -> Generator[Dict, None, None]:
+ """
+ Generator that yields all orders with automatic pagination.
+
+ Args:
+ batch_size: Orders per request (max 250)
+ query: Optional search query
+
+ Yields:
+ Order dictionaries
+ """
+ cursor = None
+ while True:
+ response = self.get_orders(first=batch_size, query=query, after=cursor)
+
+ if not response.is_success or not response.data:
+ break
+
+ orders = response.data.get('orders', {})
+ edges = orders.get('edges', [])
+
+ for edge in edges:
+ yield edge['node']
+
+ page_info = orders.get('pageInfo', {})
+ if not page_info.get('hasNextPage'):
+ break
+
+ cursor = page_info.get('endCursor')
+
+
+# ==================== Utility Functions ====================
+
+def extract_id(gid: str) -> str:
+ """
+ Extract numeric ID from Shopify GID.
+
+ Args:
+ gid: Global ID (e.g., 'gid://shopify/Product/123')
+
+ Returns:
+ Numeric ID string (e.g., '123')
+ """
+ return gid.split('/')[-1] if gid else ''
+
+
+def build_gid(resource_type: str, id: str) -> str:
+ """
+ Build Shopify GID from resource type and ID.
+
+ Args:
+ resource_type: Resource type (e.g., 'Product', 'Order')
+ id: Numeric ID
+
+ Returns:
+ Global ID (e.g., 'gid://shopify/Product/123')
+ """
+ return f"gid://shopify/{resource_type}/{id}"
+
+
+# ==================== Example Usage ====================
+
+def main():
+ """Example usage of ShopifyGraphQL client."""
+ import os
+
+ # Load from environment
+ shop = os.environ.get('SHOP_DOMAIN', 'your-store.myshopify.com')
+ token = os.environ.get('SHOPIFY_ACCESS_TOKEN', '')
+
+ if not token:
+ print("Set SHOPIFY_ACCESS_TOKEN environment variable")
+ return
+
+ client = ShopifyGraphQL(shop, token)
+
+ # Example: Get first 5 products
+ print("Fetching products...")
+ response = client.get_products(first=5)
+
+ if response.is_success:
+ products = response.data['products']['edges']
+ for edge in products:
+ product = edge['node']
+ print(f" - {product['title']} ({product['status']})")
+ print(f"\nQuery cost: {response.query_cost}")
+ else:
+ print(f"Errors: {response.errors}")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/skills/shopify-development/scripts/shopify_init.py b/skills/shopify-development/scripts/shopify_init.py
new file mode 100644
index 00000000..f0c664e1
--- /dev/null
+++ b/skills/shopify-development/scripts/shopify_init.py
@@ -0,0 +1,441 @@
+#!/usr/bin/env python3
+"""
+Shopify Project Initialization Script
+
+Interactive script to scaffold Shopify apps, extensions, or themes.
+Supports environment variable loading from multiple locations.
+"""
+
+import os
+import sys
+import json
+import subprocess
+from pathlib import Path
+from typing import Dict, Optional, List
+from dataclasses import dataclass
+
+
+@dataclass
+class EnvConfig:
+ """Environment configuration container."""
+ shopify_api_key: Optional[str] = None
+ shopify_api_secret: Optional[str] = None
+ shop_domain: Optional[str] = None
+ scopes: Optional[str] = None
+
+
+class EnvLoader:
+ """Load environment variables from multiple sources in priority order."""
+
+ @staticmethod
+ def load_env_file(filepath: Path) -> Dict[str, str]:
+ """
+ Load environment variables from .env file.
+
+ Args:
+ filepath: Path to .env file
+
+ Returns:
+ Dictionary of environment variables
+ """
+ env_vars = {}
+ if not filepath.exists():
+ return env_vars
+
+ try:
+ with open(filepath, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith('#') and '=' in line:
+ key, value = line.split('=', 1)
+ env_vars[key.strip()] = value.strip().strip('"').strip("'")
+ except Exception as e:
+ print(f"Warning: Failed to load {filepath}: {e}")
+
+ return env_vars
+
+ @staticmethod
+ def get_env_paths(skill_dir: Path) -> List[Path]:
+ """
+ Get list of .env file paths in priority order.
+
+ Works with any AI tool directory structure:
+ - .agent/skills/ (universal)
+ - .claude/skills/ (Claude Code)
+ - .gemini/skills/ (Gemini CLI)
+ - .cursor/skills/ (Cursor)
+
+ Priority: process.env > skill/.env > skills/.env > agent_dir/.env
+
+ Args:
+ skill_dir: Path to skill directory
+
+ Returns:
+ List of .env file paths
+ """
+ paths = []
+
+ # skill/.env
+ skill_env = skill_dir / '.env'
+ if skill_env.exists():
+ paths.append(skill_env)
+
+ # skills/.env
+ skills_env = skill_dir.parent / '.env'
+ if skills_env.exists():
+ paths.append(skills_env)
+
+ # agent_dir/.env (e.g., .agent, .claude, .gemini, .cursor)
+ agent_env = skill_dir.parent.parent / '.env'
+ if agent_env.exists():
+ paths.append(agent_env)
+
+ return paths
+
+ @staticmethod
+ def load_config(skill_dir: Path) -> EnvConfig:
+ """
+ Load configuration from environment variables.
+
+ Works with any AI tool directory structure.
+ Priority: process.env > skill/.env > skills/.env > agent_dir/.env
+
+ Args:
+ skill_dir: Path to skill directory
+
+ Returns:
+ EnvConfig object
+ """
+ config = EnvConfig()
+
+ # Load from .env files (reverse priority order)
+ for env_path in reversed(EnvLoader.get_env_paths(skill_dir)):
+ env_vars = EnvLoader.load_env_file(env_path)
+ if 'SHOPIFY_API_KEY' in env_vars:
+ config.shopify_api_key = env_vars['SHOPIFY_API_KEY']
+ if 'SHOPIFY_API_SECRET' in env_vars:
+ config.shopify_api_secret = env_vars['SHOPIFY_API_SECRET']
+ if 'SHOP_DOMAIN' in env_vars:
+ config.shop_domain = env_vars['SHOP_DOMAIN']
+ if 'SCOPES' in env_vars:
+ config.scopes = env_vars['SCOPES']
+
+ # Override with process environment (highest priority)
+ if 'SHOPIFY_API_KEY' in os.environ:
+ config.shopify_api_key = os.environ['SHOPIFY_API_KEY']
+ if 'SHOPIFY_API_SECRET' in os.environ:
+ config.shopify_api_secret = os.environ['SHOPIFY_API_SECRET']
+ if 'SHOP_DOMAIN' in os.environ:
+ config.shop_domain = os.environ['SHOP_DOMAIN']
+ if 'SCOPES' in os.environ:
+ config.scopes = os.environ['SCOPES']
+
+ return config
+
+
+class ShopifyInitializer:
+ """Initialize Shopify projects."""
+
+ def __init__(self, config: EnvConfig):
+ """
+ Initialize ShopifyInitializer.
+
+ Args:
+ config: Environment configuration
+ """
+ self.config = config
+
+ def prompt(self, message: str, default: Optional[str] = None) -> str:
+ """
+ Prompt user for input.
+
+ Args:
+ message: Prompt message
+ default: Default value
+
+ Returns:
+ User input or default
+ """
+ if default:
+ message = f"{message} [{default}]"
+ user_input = input(f"{message}: ").strip()
+ return user_input if user_input else (default or '')
+
+ def select_option(self, message: str, options: List[str]) -> str:
+ """
+ Prompt user to select from options.
+
+ Args:
+ message: Prompt message
+ options: List of options
+
+ Returns:
+ Selected option
+ """
+ print(f"\n{message}")
+ for i, option in enumerate(options, 1):
+ print(f"{i}. {option}")
+
+ while True:
+ try:
+ choice = int(input("Select option: ").strip())
+ if 1 <= choice <= len(options):
+ return options[choice - 1]
+ print(f"Please select 1-{len(options)}")
+ except (ValueError, KeyboardInterrupt):
+ print("Invalid input")
+
+ def check_cli_installed(self) -> bool:
+ """
+ Check if Shopify CLI is installed.
+
+ Returns:
+ True if installed, False otherwise
+ """
+ try:
+ result = subprocess.run(
+ ['shopify', 'version'],
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ return result.returncode == 0
+ except (subprocess.SubprocessError, FileNotFoundError):
+ return False
+
+ def create_app_config(self, project_dir: Path, app_name: str, scopes: str) -> None:
+ """
+ Create shopify.app.toml configuration file.
+
+ Args:
+ project_dir: Project directory
+ app_name: Application name
+ scopes: Access scopes
+ """
+ config_content = f"""# Shopify App Configuration
+name = "{app_name}"
+client_id = "{self.config.shopify_api_key or 'YOUR_API_KEY'}"
+application_url = "https://your-app.com"
+embedded = true
+
+[build]
+automatically_update_urls_on_dev = true
+dev_store_url = "{self.config.shop_domain or 'your-store.myshopify.com'}"
+
+[access_scopes]
+scopes = "{scopes}"
+
+[webhooks]
+api_version = "2026-01"
+
+[[webhooks.subscriptions]]
+topics = ["app/uninstalled"]
+uri = "/webhooks/app/uninstalled"
+
+[webhooks.privacy_compliance]
+customer_data_request_url = "/webhooks/gdpr/data-request"
+customer_deletion_url = "/webhooks/gdpr/customer-deletion"
+shop_deletion_url = "/webhooks/gdpr/shop-deletion"
+"""
+ config_path = project_dir / 'shopify.app.toml'
+ config_path.write_text(config_content)
+ print(f"✓ Created {config_path}")
+
+ def create_extension_config(self, project_dir: Path, extension_name: str, extension_type: str) -> None:
+ """
+ Create shopify.extension.toml configuration file.
+
+ Args:
+ project_dir: Project directory
+ extension_name: Extension name
+ extension_type: Extension type
+ """
+ target_map = {
+ 'checkout': 'purchase.checkout.block.render',
+ 'admin_action': 'admin.product-details.action.render',
+ 'admin_block': 'admin.product-details.block.render',
+ 'pos': 'pos.home.tile.render',
+ 'function': 'function',
+ 'customer_account': 'customer-account.order-status.block.render',
+ 'theme_app': 'theme-app-extension'
+ }
+
+ config_content = f"""name = "{extension_name}"
+type = "ui_extension"
+handle = "{extension_name.lower().replace(' ', '-')}"
+
+[extension_points]
+api_version = "2026-01"
+
+[[extension_points.targets]]
+target = "{target_map.get(extension_type, 'purchase.checkout.block.render')}"
+
+[capabilities]
+network_access = true
+api_access = true
+"""
+ config_path = project_dir / 'shopify.extension.toml'
+ config_path.write_text(config_content)
+ print(f"✓ Created {config_path}")
+
+ def create_readme(self, project_dir: Path, project_type: str, project_name: str) -> None:
+ """
+ Create README.md file.
+
+ Args:
+ project_dir: Project directory
+ project_type: Project type (app/extension/theme)
+ project_name: Project name
+ """
+ content = f"""# {project_name}
+
+Shopify {project_type.capitalize()} project.
+
+## Setup
+
+```bash
+# Install dependencies
+npm install
+
+# Start development
+shopify {project_type} dev
+```
+
+## Deployment
+
+```bash
+# Deploy to Shopify
+shopify {project_type} deploy
+```
+
+## Resources
+
+- [Shopify Documentation](https://shopify.dev/docs)
+- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli)
+"""
+ readme_path = project_dir / 'README.md'
+ readme_path.write_text(content)
+ print(f"✓ Created {readme_path}")
+
+ def init_app(self) -> None:
+ """Initialize Shopify app project."""
+ print("\n=== Shopify App Initialization ===\n")
+
+ app_name = self.prompt("App name", "my-shopify-app")
+ scopes = self.prompt("Access scopes", self.config.scopes or "read_products,write_products")
+
+ project_dir = Path.cwd() / app_name
+ project_dir.mkdir(exist_ok=True)
+
+ print(f"\nCreating app in {project_dir}...")
+
+ self.create_app_config(project_dir, app_name, scopes)
+ self.create_readme(project_dir, "app", app_name)
+
+ # Create basic package.json
+ package_json = {
+ "name": app_name.lower().replace(' ', '-'),
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "shopify app dev",
+ "deploy": "shopify app deploy"
+ }
+ }
+ (project_dir / 'package.json').write_text(json.dumps(package_json, indent=2))
+ print(f"✓ Created package.json")
+
+ print(f"\n✓ App '{app_name}' initialized successfully!")
+ print(f"\nNext steps:")
+ print(f" cd {app_name}")
+ print(f" npm install")
+ print(f" shopify app dev")
+
+ def init_extension(self) -> None:
+ """Initialize Shopify extension project."""
+ print("\n=== Shopify Extension Initialization ===\n")
+
+ extension_types = [
+ 'checkout',
+ 'admin_action',
+ 'admin_block',
+ 'pos',
+ 'function',
+ 'customer_account',
+ 'theme_app'
+ ]
+ extension_type = self.select_option("Select extension type", extension_types)
+
+ extension_name = self.prompt("Extension name", "my-extension")
+
+ project_dir = Path.cwd() / extension_name
+ project_dir.mkdir(exist_ok=True)
+
+ print(f"\nCreating extension in {project_dir}...")
+
+ self.create_extension_config(project_dir, extension_name, extension_type)
+ self.create_readme(project_dir, "extension", extension_name)
+
+ print(f"\n✓ Extension '{extension_name}' initialized successfully!")
+ print(f"\nNext steps:")
+ print(f" cd {extension_name}")
+ print(f" shopify app dev")
+
+ def init_theme(self) -> None:
+ """Initialize Shopify theme project."""
+ print("\n=== Shopify Theme Initialization ===\n")
+
+ theme_name = self.prompt("Theme name", "my-theme")
+
+ print(f"\nInitializing theme '{theme_name}'...")
+ print("\nRecommended: Use 'shopify theme init' for full theme scaffolding")
+ print(f"\nRun: shopify theme init {theme_name}")
+
+ def run(self) -> None:
+ """Run interactive initialization."""
+ print("=" * 60)
+ print("Shopify Project Initializer")
+ print("=" * 60)
+
+ # Check CLI
+ if not self.check_cli_installed():
+ print("\n⚠ Shopify CLI not found!")
+ print("Install: npm install -g @shopify/cli@latest")
+ sys.exit(1)
+
+ # Select project type
+ project_types = ['app', 'extension', 'theme']
+ project_type = self.select_option("Select project type", project_types)
+
+ # Initialize based on type
+ if project_type == 'app':
+ self.init_app()
+ elif project_type == 'extension':
+ self.init_extension()
+ elif project_type == 'theme':
+ self.init_theme()
+
+
+def main() -> None:
+ """Main entry point."""
+ try:
+ # Get skill directory
+ script_dir = Path(__file__).parent
+ skill_dir = script_dir.parent
+
+ # Load configuration
+ config = EnvLoader.load_config(skill_dir)
+
+ # Initialize project
+ initializer = ShopifyInitializer(config)
+ initializer.run()
+
+ except KeyboardInterrupt:
+ print("\n\nAborted.")
+ sys.exit(0)
+ except Exception as e:
+ print(f"\n✗ Error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/skills/shopify-development/scripts/tests/test_shopify_init.py b/skills/shopify-development/scripts/tests/test_shopify_init.py
new file mode 100644
index 00000000..bcebb790
--- /dev/null
+++ b/skills/shopify-development/scripts/tests/test_shopify_init.py
@@ -0,0 +1,379 @@
+"""
+Tests for shopify_init.py
+
+Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing
+"""
+
+import os
+import sys
+import json
+import pytest
+import subprocess
+from pathlib import Path
+from unittest.mock import Mock, patch, mock_open, MagicMock
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer
+
+
+class TestEnvLoader:
+ """Test EnvLoader class."""
+
+ def test_load_env_file_success(self, tmp_path):
+ """Test loading valid .env file."""
+ env_file = tmp_path / ".env"
+ env_file.write_text("""
+SHOPIFY_API_KEY=test_key
+SHOPIFY_API_SECRET=test_secret
+SHOP_DOMAIN=test.myshopify.com
+# Comment line
+SCOPES=read_products,write_products
+""")
+
+ result = EnvLoader.load_env_file(env_file)
+
+ assert result['SHOPIFY_API_KEY'] == 'test_key'
+ assert result['SHOPIFY_API_SECRET'] == 'test_secret'
+ assert result['SHOP_DOMAIN'] == 'test.myshopify.com'
+ assert result['SCOPES'] == 'read_products,write_products'
+
+ def test_load_env_file_with_quotes(self, tmp_path):
+ """Test loading .env file with quoted values."""
+ env_file = tmp_path / ".env"
+ env_file.write_text("""
+SHOPIFY_API_KEY="test_key"
+SHOPIFY_API_SECRET='test_secret'
+""")
+
+ result = EnvLoader.load_env_file(env_file)
+
+ assert result['SHOPIFY_API_KEY'] == 'test_key'
+ assert result['SHOPIFY_API_SECRET'] == 'test_secret'
+
+ def test_load_env_file_nonexistent(self, tmp_path):
+ """Test loading non-existent .env file."""
+ result = EnvLoader.load_env_file(tmp_path / "nonexistent.env")
+ assert result == {}
+
+ def test_load_env_file_invalid_format(self, tmp_path):
+ """Test loading .env file with invalid lines."""
+ env_file = tmp_path / ".env"
+ env_file.write_text("""
+VALID_KEY=value
+INVALID_LINE_NO_EQUALS
+ANOTHER_VALID=test
+""")
+
+ result = EnvLoader.load_env_file(env_file)
+
+ assert result['VALID_KEY'] == 'value'
+ assert result['ANOTHER_VALID'] == 'test'
+ assert 'INVALID_LINE_NO_EQUALS' not in result
+
+ def test_get_env_paths(self, tmp_path):
+ """Test getting .env file paths from universal directory structure."""
+ # Create directory structure (works with .agent, .claude, .gemini, .cursor)
+ agent_dir = tmp_path / ".agent"
+ skills_dir = agent_dir / "skills"
+ skill_dir = skills_dir / "shopify"
+
+ skill_dir.mkdir(parents=True)
+
+ # Create .env files at each level
+ (skill_dir / ".env").write_text("SKILL=1")
+ (skills_dir / ".env").write_text("SKILLS=1")
+ (agent_dir / ".env").write_text("AGENT=1")
+
+ paths = EnvLoader.get_env_paths(skill_dir)
+
+ assert len(paths) == 3
+ assert skill_dir / ".env" in paths
+ assert skills_dir / ".env" in paths
+ assert agent_dir / ".env" in paths
+
+ def test_load_config_priority(self, tmp_path, monkeypatch):
+ """Test configuration loading priority across different AI tool directories."""
+ skill_dir = tmp_path / "skill"
+ skills_dir = tmp_path
+ agent_dir = tmp_path.parent # Could be .agent, .claude, .gemini, .cursor
+
+ skill_dir.mkdir(parents=True)
+
+ (skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key")
+ (skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com")
+
+ monkeypatch.setenv("SHOPIFY_API_KEY", "process_key")
+
+ config = EnvLoader.load_config(skill_dir)
+
+ assert config.shopify_api_key == "process_key"
+ # Shop domain from skills/.env
+ assert config.shop_domain == "skills.myshopify.com"
+
+ def test_load_config_no_files(self, tmp_path):
+ """Test configuration loading with no .env files."""
+ config = EnvLoader.load_config(tmp_path)
+
+ assert config.shopify_api_key is None
+ assert config.shopify_api_secret is None
+ assert config.shop_domain is None
+ assert config.scopes is None
+
+
+class TestShopifyInitializer:
+ """Test ShopifyInitializer class."""
+
+ @pytest.fixture
+ def config(self):
+ """Create test config."""
+ return EnvConfig(
+ shopify_api_key="test_key",
+ shopify_api_secret="test_secret",
+ shop_domain="test.myshopify.com",
+ scopes="read_products,write_products"
+ )
+
+ @pytest.fixture
+ def initializer(self, config):
+ """Create initializer instance."""
+ return ShopifyInitializer(config)
+
+ def test_prompt_with_default(self, initializer):
+ """Test prompt with default value."""
+ with patch('builtins.input', return_value=''):
+ result = initializer.prompt("Test", "default_value")
+ assert result == "default_value"
+
+ def test_prompt_with_input(self, initializer):
+ """Test prompt with user input."""
+ with patch('builtins.input', return_value='user_input'):
+ result = initializer.prompt("Test", "default_value")
+ assert result == "user_input"
+
+ def test_select_option_valid(self, initializer):
+ """Test select option with valid choice."""
+ options = ['app', 'extension', 'theme']
+ with patch('builtins.input', return_value='2'):
+ result = initializer.select_option("Choose", options)
+ assert result == 'extension'
+
+ def test_select_option_invalid_then_valid(self, initializer):
+ """Test select option with invalid then valid choice."""
+ options = ['app', 'extension']
+ with patch('builtins.input', side_effect=['5', 'invalid', '1']):
+ result = initializer.select_option("Choose", options)
+ assert result == 'app'
+
+ def test_check_cli_installed_success(self, initializer):
+ """Test CLI installed check - success."""
+ mock_result = Mock()
+ mock_result.returncode = 0
+
+ with patch('subprocess.run', return_value=mock_result):
+ assert initializer.check_cli_installed() is True
+
+ def test_check_cli_installed_failure(self, initializer):
+ """Test CLI installed check - failure."""
+ with patch('subprocess.run', side_effect=FileNotFoundError):
+ assert initializer.check_cli_installed() is False
+
+ def test_create_app_config(self, initializer, tmp_path):
+ """Test creating app configuration file."""
+ initializer.create_app_config(tmp_path, "test-app", "read_products")
+
+ config_file = tmp_path / "shopify.app.toml"
+ assert config_file.exists()
+
+ content = config_file.read_text()
+ assert 'name = "test-app"' in content
+ assert 'scopes = "read_products"' in content
+ assert 'client_id = "test_key"' in content
+
+ def test_create_extension_config(self, initializer, tmp_path):
+ """Test creating extension configuration file."""
+ initializer.create_extension_config(tmp_path, "test-ext", "checkout")
+
+ config_file = tmp_path / "shopify.extension.toml"
+ assert config_file.exists()
+
+ content = config_file.read_text()
+ assert 'name = "test-ext"' in content
+ assert 'purchase.checkout.block.render' in content
+
+ def test_create_extension_config_admin_action(self, initializer, tmp_path):
+ """Test creating admin action extension config."""
+ initializer.create_extension_config(tmp_path, "admin-ext", "admin_action")
+
+ config_file = tmp_path / "shopify.extension.toml"
+ content = config_file.read_text()
+ assert 'admin.product-details.action.render' in content
+
+ def test_create_readme(self, initializer, tmp_path):
+ """Test creating README file."""
+ initializer.create_readme(tmp_path, "app", "Test App")
+
+ readme_file = tmp_path / "README.md"
+ assert readme_file.exists()
+
+ content = readme_file.read_text()
+ assert '# Test App' in content
+ assert 'shopify app dev' in content
+
+ @patch('builtins.input')
+ @patch('builtins.print')
+ def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
+ """Test app initialization."""
+ monkeypatch.chdir(tmp_path)
+
+ # Mock user inputs
+ mock_input.side_effect = ['my-app', 'read_products,write_products']
+
+ initializer.init_app()
+
+ # Check directory created
+ app_dir = tmp_path / "my-app"
+ assert app_dir.exists()
+
+ # Check files created
+ assert (app_dir / "shopify.app.toml").exists()
+ assert (app_dir / "README.md").exists()
+ assert (app_dir / "package.json").exists()
+
+ # Check package.json content
+ package_json = json.loads((app_dir / "package.json").read_text())
+ assert package_json['name'] == 'my-app'
+ assert 'dev' in package_json['scripts']
+
+ @patch('builtins.input')
+ @patch('builtins.print')
+ def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
+ """Test extension initialization."""
+ monkeypatch.chdir(tmp_path)
+
+ # Mock user inputs: type selection (1 = checkout), name
+ mock_input.side_effect = ['1', 'my-extension']
+
+ initializer.init_extension()
+
+ # Check directory and files created
+ ext_dir = tmp_path / "my-extension"
+ assert ext_dir.exists()
+ assert (ext_dir / "shopify.extension.toml").exists()
+ assert (ext_dir / "README.md").exists()
+
+ @patch('builtins.input')
+ @patch('builtins.print')
+ def test_init_theme(self, mock_print, mock_input, initializer):
+ """Test theme initialization."""
+ mock_input.return_value = 'my-theme'
+
+ initializer.init_theme()
+
+ assert mock_print.called
+
+ @patch('builtins.print')
+ def test_run_no_cli(self, mock_print, initializer):
+ """Test run when CLI not installed."""
+ with patch.object(initializer, 'check_cli_installed', return_value=False):
+ with pytest.raises(SystemExit) as exc_info:
+ initializer.run()
+ assert exc_info.value.code == 1
+
+ @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
+ @patch.object(ShopifyInitializer, 'init_app')
+ @patch('builtins.input')
+ @patch('builtins.print')
+ def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer):
+ """Test run with app selection."""
+ mock_input.return_value = '1' # Select app
+
+ initializer.run()
+
+ mock_init_app.assert_called_once()
+
+ @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
+ @patch.object(ShopifyInitializer, 'init_extension')
+ @patch('builtins.input')
+ @patch('builtins.print')
+ def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer):
+ """Test run with extension selection."""
+ mock_input.return_value = '2' # Select extension
+
+ initializer.run()
+
+ mock_init_ext.assert_called_once()
+
+
+class TestMain:
+ """Test main function."""
+
+ @patch('shopify_init.ShopifyInitializer')
+ @patch('shopify_init.EnvLoader')
+ def test_main_success(self, mock_loader, mock_initializer):
+ """Test main function success path."""
+ from shopify_init import main
+
+ mock_config = Mock()
+ mock_loader.load_config.return_value = mock_config
+
+ mock_init_instance = Mock()
+ mock_initializer.return_value = mock_init_instance
+
+ with patch('builtins.print'):
+ main()
+
+ mock_init_instance.run.assert_called_once()
+
+ @patch('shopify_init.ShopifyInitializer')
+ @patch('sys.exit')
+ def test_main_keyboard_interrupt(self, mock_exit, mock_initializer):
+ """Test main function with keyboard interrupt."""
+ from shopify_init import main
+
+ mock_initializer.return_value.run.side_effect = KeyboardInterrupt
+
+ with patch('builtins.print'):
+ main()
+
+ mock_exit.assert_called_with(0)
+
+ @patch('shopify_init.ShopifyInitializer')
+ @patch('sys.exit')
+ def test_main_exception(self, mock_exit, mock_initializer):
+ """Test main function with exception."""
+ from shopify_init import main
+
+ mock_initializer.return_value.run.side_effect = Exception("Test error")
+
+ with patch('builtins.print'):
+ main()
+
+ mock_exit.assert_called_with(1)
+
+
+class TestEnvConfig:
+ """Test EnvConfig dataclass."""
+
+ def test_env_config_defaults(self):
+ """Test EnvConfig default values."""
+ config = EnvConfig()
+
+ assert config.shopify_api_key is None
+ assert config.shopify_api_secret is None
+ assert config.shop_domain is None
+ assert config.scopes is None
+
+ def test_env_config_with_values(self):
+ """Test EnvConfig with values."""
+ config = EnvConfig(
+ shopify_api_key="key",
+ shopify_api_secret="secret",
+ shop_domain="test.myshopify.com",
+ scopes="read_products"
+ )
+
+ assert config.shopify_api_key == "key"
+ assert config.shopify_api_secret == "secret"
+ assert config.shop_domain == "test.myshopify.com"
+ assert config.scopes == "read_products"