@doswiftly/cli 0.1.1
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/README.md +357 -0
- package/bin/doswiftly.js +2 -0
- package/dist/commands/auth-github.d.ts +6 -0
- package/dist/commands/auth-github.d.ts.map +1 -0
- package/dist/commands/auth-github.js +89 -0
- package/dist/commands/auth-github.js.map +1 -0
- package/dist/commands/auth-token.d.ts +12 -0
- package/dist/commands/auth-token.d.ts.map +1 -0
- package/dist/commands/auth-token.js +43 -0
- package/dist/commands/auth-token.js.map +1 -0
- package/dist/commands/auth.d.ts +22 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +348 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +234 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +104 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/deploy.d.ts +37 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +580 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +83 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +363 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/domain.d.ts +13 -0
- package/dist/commands/domain.d.ts.map +1 -0
- package/dist/commands/domain.js +128 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/env.d.ts +25 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +228 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1028 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect.d.ts +12 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +162 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/migrate.d.ts +18 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +355 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/preview.d.ts +29 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +199 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/proxy.d.ts +9 -0
- package/dist/commands/proxy.d.ts.map +1 -0
- package/dist/commands/proxy.js +37 -0
- package/dist/commands/proxy.js.map +1 -0
- package/dist/commands/sdk.d.ts +5 -0
- package/dist/commands/sdk.d.ts.map +1 -0
- package/dist/commands/sdk.js +82 -0
- package/dist/commands/sdk.js.map +1 -0
- package/dist/commands/template.d.ts +107 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +1309 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/types.d.ts +5 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +82 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +103 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/upgrade.d.ts +18 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +55 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/verify.d.ts +5 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +232 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +60 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config/types.d.ts +173 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +48 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +416 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-url.d.ts +14 -0
- package/dist/lib/api-url.d.ts.map +1 -0
- package/dist/lib/api-url.js +24 -0
- package/dist/lib/api-url.js.map +1 -0
- package/dist/lib/api.d.ts +67 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +36 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +39 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +195 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/env-storage.d.ts +140 -0
- package/dist/lib/env-storage.d.ts.map +1 -0
- package/dist/lib/env-storage.js +464 -0
- package/dist/lib/env-storage.js.map +1 -0
- package/dist/lib/errors.d.ts +61 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +204 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/i18n.d.ts +99 -0
- package/dist/lib/i18n.d.ts.map +1 -0
- package/dist/lib/i18n.js +184 -0
- package/dist/lib/i18n.js.map +1 -0
- package/dist/lib/logger.d.ts +95 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +168 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/package-manager.d.ts +91 -0
- package/dist/lib/package-manager.d.ts.map +1 -0
- package/dist/lib/package-manager.js +205 -0
- package/dist/lib/package-manager.js.map +1 -0
- package/dist/lib/proxy-server.d.ts +24 -0
- package/dist/lib/proxy-server.d.ts.map +1 -0
- package/dist/lib/proxy-server.js +173 -0
- package/dist/lib/proxy-server.js.map +1 -0
- package/dist/lib/select-with-back.d.ts +34 -0
- package/dist/lib/select-with-back.d.ts.map +1 -0
- package/dist/lib/select-with-back.js +94 -0
- package/dist/lib/select-with-back.js.map +1 -0
- package/dist/lib/shared-api-client.d.ts +40 -0
- package/dist/lib/shared-api-client.d.ts.map +1 -0
- package/dist/lib/shared-api-client.js +92 -0
- package/dist/lib/shared-api-client.js.map +1 -0
- package/dist/lib/wizard-engine.d.ts +128 -0
- package/dist/lib/wizard-engine.d.ts.map +1 -0
- package/dist/lib/wizard-engine.js +168 -0
- package/dist/lib/wizard-engine.js.map +1 -0
- package/package.json +85 -0
- package/templates/storefront-minimal/.env.example +10 -0
- package/templates/storefront-minimal/.github/workflows/build-template.yml +109 -0
- package/templates/storefront-minimal/app/globals.css +18 -0
- package/templates/storefront-minimal/app/layout.tsx +26 -0
- package/templates/storefront-minimal/app/page.tsx +93 -0
- package/templates/storefront-minimal/lib/graphql-client.ts +23 -0
- package/templates/storefront-minimal/next.config.ts +15 -0
- package/templates/storefront-minimal/open-next.config.ts +3 -0
- package/templates/storefront-minimal/package.json +30 -0
- package/templates/storefront-minimal/postcss.config.mjs +5 -0
- package/templates/storefront-minimal/tailwind.config.ts +14 -0
- package/templates/storefront-minimal/tsconfig.json +27 -0
- package/templates/storefront-minimal/wrangler.toml +9 -0
- package/templates/storefront-nextjs/.env.example +68 -0
- package/templates/storefront-nextjs/.github/workflows/build-template.yml +109 -0
- package/templates/storefront-nextjs/.github/workflows/deploy.yml +25 -0
- package/templates/storefront-nextjs/.github/workflows/preview.yml +22 -0
- package/templates/storefront-nextjs/README.md +520 -0
- package/templates/storefront-nextjs/app/account/orders/page.tsx +216 -0
- package/templates/storefront-nextjs/app/account/page.tsx +167 -0
- package/templates/storefront-nextjs/app/auth/login/page.tsx +135 -0
- package/templates/storefront-nextjs/app/auth/register/page.tsx +228 -0
- package/templates/storefront-nextjs/app/cart/page.tsx +263 -0
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +200 -0
- package/templates/storefront-nextjs/app/categories/page.tsx +58 -0
- package/templates/storefront-nextjs/app/checkout/page.tsx +351 -0
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +158 -0
- package/templates/storefront-nextjs/app/collections/page.tsx +61 -0
- package/templates/storefront-nextjs/app/globals.css +98 -0
- package/templates/storefront-nextjs/app/layout.tsx +39 -0
- package/templates/storefront-nextjs/app/page.tsx +136 -0
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +119 -0
- package/templates/storefront-nextjs/app/products/page.tsx +107 -0
- package/templates/storefront-nextjs/app/search/page.tsx +127 -0
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +94 -0
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +77 -0
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +29 -0
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +217 -0
- package/templates/storefront-nextjs/components/commerce/pagination.tsx +62 -0
- package/templates/storefront-nextjs/components/commerce/product-actions.tsx +135 -0
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +109 -0
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +375 -0
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +178 -0
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +64 -0
- package/templates/storefront-nextjs/components/commerce/variant-selector.tsx +210 -0
- package/templates/storefront-nextjs/components/layout/footer.tsx +107 -0
- package/templates/storefront-nextjs/components/layout/header.tsx +104 -0
- package/templates/storefront-nextjs/components/providers.tsx +62 -0
- package/templates/storefront-nextjs/lib/auth/routes.ts +52 -0
- package/templates/storefront-nextjs/lib/currency.tsx +140 -0
- package/templates/storefront-nextjs/lib/format.ts +159 -0
- package/templates/storefront-nextjs/lib/graphql-queries.ts +629 -0
- package/templates/storefront-nextjs/lib/hooks.ts +30 -0
- package/templates/storefront-nextjs/middleware.ts +80 -0
- package/templates/storefront-nextjs/next.config.ts +37 -0
- package/templates/storefront-nextjs/open-next.config.ts +3 -0
- package/templates/storefront-nextjs/package.dev.json +30 -0
- package/templates/storefront-nextjs/package.json +32 -0
- package/templates/storefront-nextjs/package.json.template +32 -0
- package/templates/storefront-nextjs/postcss.config.mjs +8 -0
- package/templates/storefront-nextjs/tailwind.config.ts +111 -0
- package/templates/storefront-nextjs/tsconfig.json +27 -0
- package/templates/storefront-nextjs/wrangler.toml +9 -0
- package/templates/storefront-nextjs-shadcn/.env.example +68 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/build-template.yml +109 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +25 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +22 -0
- package/templates/storefront-nextjs-shadcn/CART_INTEGRATION.md +282 -0
- package/templates/storefront-nextjs-shadcn/CLAUDE.md +96 -0
- package/templates/storefront-nextjs-shadcn/GRAPHQL_DOCUMENT_NAMES.md +190 -0
- package/templates/storefront-nextjs-shadcn/GRAPHQL_ERROR_HANDLING.md +263 -0
- package/templates/storefront-nextjs-shadcn/GRAPHQL_FIXES_SUMMARY.md +135 -0
- package/templates/storefront-nextjs-shadcn/GRAPHQL_INTEGRATION_COMPLETE.md +142 -0
- package/templates/storefront-nextjs-shadcn/INTEGRATION_CHECKLIST.md +448 -0
- package/templates/storefront-nextjs-shadcn/PRODUCT_DETAIL_PAGE_IMPLEMENTATION.md +307 -0
- package/templates/storefront-nextjs-shadcn/README.md +195 -0
- package/templates/storefront-nextjs-shadcn/THEME_CUSTOMIZATION.md +245 -0
- package/templates/storefront-nextjs-shadcn/app/about/page.tsx +34 -0
- package/templates/storefront-nextjs-shadcn/app/account/addresses/page.tsx +215 -0
- package/templates/storefront-nextjs-shadcn/app/account/loyalty/page.tsx +484 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/page.tsx +128 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/tracking/page.tsx +206 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/page.tsx +80 -0
- package/templates/storefront-nextjs-shadcn/app/account/page.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/app/account/settings/page.tsx +195 -0
- package/templates/storefront-nextjs-shadcn/app/api/auth/clear-token/route.ts +87 -0
- package/templates/storefront-nextjs-shadcn/app/api/auth/set-token/route.ts +125 -0
- package/templates/storefront-nextjs-shadcn/app/auth/forgot-password/page.tsx +131 -0
- package/templates/storefront-nextjs-shadcn/app/auth/login/page.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/app/auth/register/page.tsx +20 -0
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/page.tsx +323 -0
- package/templates/storefront-nextjs-shadcn/app/blog/page.tsx +159 -0
- package/templates/storefront-nextjs-shadcn/app/brands/[slug]/page.tsx +170 -0
- package/templates/storefront-nextjs-shadcn/app/brands/page.tsx +73 -0
- package/templates/storefront-nextjs-shadcn/app/cart/page.tsx +165 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/page.tsx +78 -0
- package/templates/storefront-nextjs-shadcn/app/categories/page.tsx +75 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/page.tsx +1752 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/success/[orderId]/page.tsx +256 -0
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/page.tsx +74 -0
- package/templates/storefront-nextjs-shadcn/app/collections/page.tsx +75 -0
- package/templates/storefront-nextjs-shadcn/app/contact/page.tsx +114 -0
- package/templates/storefront-nextjs-shadcn/app/error.tsx +90 -0
- package/templates/storefront-nextjs-shadcn/app/globals.css +125 -0
- package/templates/storefront-nextjs-shadcn/app/layout.tsx +57 -0
- package/templates/storefront-nextjs-shadcn/app/not-found.tsx +68 -0
- package/templates/storefront-nextjs-shadcn/app/page.tsx +21 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/page.tsx +246 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/product-client.tsx +343 -0
- package/templates/storefront-nextjs-shadcn/app/products/page.tsx +25 -0
- package/templates/storefront-nextjs-shadcn/app/products/products-client.tsx +192 -0
- package/templates/storefront-nextjs-shadcn/app/returns/page.tsx +77 -0
- package/templates/storefront-nextjs-shadcn/app/robots.ts +53 -0
- package/templates/storefront-nextjs-shadcn/app/search/page.tsx +16 -0
- package/templates/storefront-nextjs-shadcn/app/search/search-client.tsx +47 -0
- package/templates/storefront-nextjs-shadcn/app/shipping/page.tsx +62 -0
- package/templates/storefront-nextjs-shadcn/app/sitemap.ts +144 -0
- package/templates/storefront-nextjs-shadcn/app/wishlist/page.tsx +179 -0
- package/templates/storefront-nextjs-shadcn/codegen.ts +51 -0
- package/templates/storefront-nextjs-shadcn/components/account/address-form.tsx +348 -0
- package/templates/storefront-nextjs-shadcn/components/account/address-list.tsx +144 -0
- package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +258 -0
- package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +132 -0
- package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +188 -0
- package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +305 -0
- package/templates/storefront-nextjs-shadcn/components/blog/blog-card.tsx +240 -0
- package/templates/storefront-nextjs-shadcn/components/blog/blog-sidebar.tsx +177 -0
- package/templates/storefront-nextjs-shadcn/components/blog/index.ts +8 -0
- package/templates/storefront-nextjs-shadcn/components/brand/brand-card.tsx +119 -0
- package/templates/storefront-nextjs-shadcn/components/brand/brand-grid.tsx +64 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +140 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +48 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +112 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +84 -0
- package/templates/storefront-nextjs-shadcn/components/cart/index.ts +17 -0
- package/templates/storefront-nextjs-shadcn/components/cart/promo-code-input.tsx +121 -0
- package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +162 -0
- package/templates/storefront-nextjs-shadcn/components/checkout/index.ts +25 -0
- package/templates/storefront-nextjs-shadcn/components/checkout/payment-method-card.tsx +187 -0
- package/templates/storefront-nextjs-shadcn/components/checkout/payment-step.tsx +160 -0
- package/templates/storefront-nextjs-shadcn/components/checkout/tax-breakdown.tsx +154 -0
- package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +225 -0
- package/templates/storefront-nextjs-shadcn/components/commerce/pagination.tsx +62 -0
- package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +158 -0
- package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +174 -0
- package/templates/storefront-nextjs-shadcn/components/commerce/variant-selector.tsx +210 -0
- package/templates/storefront-nextjs-shadcn/components/common/category-card.tsx +97 -0
- package/templates/storefront-nextjs-shadcn/components/common/collection-card.tsx +187 -0
- package/templates/storefront-nextjs-shadcn/components/common/price-display.tsx +151 -0
- package/templates/storefront-nextjs-shadcn/components/common/social-share.tsx +166 -0
- package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +245 -0
- package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +246 -0
- package/templates/storefront-nextjs-shadcn/components/discount/index.ts +19 -0
- package/templates/storefront-nextjs-shadcn/components/error/error-boundary.tsx +113 -0
- package/templates/storefront-nextjs-shadcn/components/error/index.ts +7 -0
- package/templates/storefront-nextjs-shadcn/components/filters/attribute-filter.tsx +153 -0
- package/templates/storefront-nextjs-shadcn/components/filters/checkbox-group-filter.tsx +167 -0
- package/templates/storefront-nextjs-shadcn/components/filters/color-swatch-filter.tsx +176 -0
- package/templates/storefront-nextjs-shadcn/components/filters/dynamic-attribute-filters.tsx +220 -0
- package/templates/storefront-nextjs-shadcn/components/filters/index.ts +36 -0
- package/templates/storefront-nextjs-shadcn/components/filters/range-slider-filter.tsx +193 -0
- package/templates/storefront-nextjs-shadcn/components/filters/toggle-filter.tsx +132 -0
- package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-balance.tsx +321 -0
- package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +309 -0
- package/templates/storefront-nextjs-shadcn/components/gift-card/index.ts +24 -0
- package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +72 -0
- package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/components/home/featured-products.tsx +85 -0
- package/templates/storefront-nextjs-shadcn/components/home/hero-section.tsx +34 -0
- package/templates/storefront-nextjs-shadcn/components/home/index.ts +8 -0
- package/templates/storefront-nextjs-shadcn/components/home/newsletter-signup.tsx +108 -0
- package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +133 -0
- package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +341 -0
- package/templates/storefront-nextjs-shadcn/components/layout/footer.tsx +128 -0
- package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +147 -0
- package/templates/storefront-nextjs-shadcn/components/layout/index.ts +9 -0
- package/templates/storefront-nextjs-shadcn/components/layout/mobile-menu.tsx +211 -0
- package/templates/storefront-nextjs-shadcn/components/layout/navigation.tsx +95 -0
- package/templates/storefront-nextjs-shadcn/components/layout/theme-switcher.tsx +192 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/index.ts +11 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-balance.tsx +93 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-history.tsx +177 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +250 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/rewards-catalog.tsx +217 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/tier-badge.tsx +106 -0
- package/templates/storefront-nextjs-shadcn/components/loyalty/tier-progress.tsx +131 -0
- package/templates/storefront-nextjs-shadcn/components/order/delivery-estimate.tsx +196 -0
- package/templates/storefront-nextjs-shadcn/components/order/index.ts +11 -0
- package/templates/storefront-nextjs-shadcn/components/order/order-tracking.tsx +200 -0
- package/templates/storefront-nextjs-shadcn/components/order/shipment-card.tsx +407 -0
- package/templates/storefront-nextjs-shadcn/components/order/tracking-status.tsx +222 -0
- package/templates/storefront-nextjs-shadcn/components/order/tracking-timeline.tsx +205 -0
- package/templates/storefront-nextjs-shadcn/components/product/add-to-cart-button.tsx +161 -0
- package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +250 -0
- package/templates/storefront-nextjs-shadcn/components/product/discount-badge.tsx +196 -0
- package/templates/storefront-nextjs-shadcn/components/product/index.ts +41 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +147 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +217 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-gallery.tsx +143 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-grid.tsx +83 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +155 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-price.tsx +158 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-quantity-selector.tsx +111 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-reviews.tsx +238 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +58 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-variant-selector.tsx +169 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-card.tsx +220 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +338 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-summary.tsx +143 -0
- package/templates/storefront-nextjs-shadcn/components/product/sale-countdown.tsx +166 -0
- package/templates/storefront-nextjs-shadcn/components/product/savings-display.tsx +213 -0
- package/templates/storefront-nextjs-shadcn/components/product/similar-products.tsx +57 -0
- package/templates/storefront-nextjs-shadcn/components/product/stock-indicator.tsx +91 -0
- package/templates/storefront-nextjs-shadcn/components/providers/currency-provider.tsx +103 -0
- package/templates/storefront-nextjs-shadcn/components/providers/index.ts +8 -0
- package/templates/storefront-nextjs-shadcn/components/providers/query-provider.tsx +260 -0
- package/templates/storefront-nextjs-shadcn/components/providers/theme-provider.tsx +13 -0
- package/templates/storefront-nextjs-shadcn/components/returns/index.ts +26 -0
- package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +608 -0
- package/templates/storefront-nextjs-shadcn/components/returns/return-status-card.tsx +554 -0
- package/templates/storefront-nextjs-shadcn/components/search/index.ts +8 -0
- package/templates/storefront-nextjs-shadcn/components/search/search-bar.tsx +140 -0
- package/templates/storefront-nextjs-shadcn/components/search/search-results.tsx +58 -0
- package/templates/storefront-nextjs-shadcn/components/search/search-suggestions.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/components/seo/index.ts +12 -0
- package/templates/storefront-nextjs-shadcn/components/seo/json-ld.tsx +56 -0
- package/templates/storefront-nextjs-shadcn/components/seo/product-json-ld.ts +167 -0
- package/templates/storefront-nextjs-shadcn/components/shipping/index.ts +16 -0
- package/templates/storefront-nextjs-shadcn/components/shipping/shipping-method-selector.tsx +337 -0
- package/templates/storefront-nextjs-shadcn/components/ui/accordion.tsx +153 -0
- package/templates/storefront-nextjs-shadcn/components/ui/alert.tsx +59 -0
- package/templates/storefront-nextjs-shadcn/components/ui/badge.tsx +34 -0
- package/templates/storefront-nextjs-shadcn/components/ui/button.tsx +51 -0
- package/templates/storefront-nextjs-shadcn/components/ui/card.tsx +77 -0
- package/templates/storefront-nextjs-shadcn/components/ui/checkbox.tsx +30 -0
- package/templates/storefront-nextjs-shadcn/components/ui/dialog.tsx +137 -0
- package/templates/storefront-nextjs-shadcn/components/ui/empty-state.tsx +207 -0
- package/templates/storefront-nextjs-shadcn/components/ui/index.ts +67 -0
- package/templates/storefront-nextjs-shadcn/components/ui/input.tsx +65 -0
- package/templates/storefront-nextjs-shadcn/components/ui/label.tsx +26 -0
- package/templates/storefront-nextjs-shadcn/components/ui/pagination.tsx +205 -0
- package/templates/storefront-nextjs-shadcn/components/ui/radio-group.tsx +44 -0
- package/templates/storefront-nextjs-shadcn/components/ui/select.tsx +160 -0
- package/templates/storefront-nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/storefront-nextjs-shadcn/components/ui/skeleton.tsx +20 -0
- package/templates/storefront-nextjs-shadcn/components/ui/spinner.tsx +82 -0
- package/templates/storefront-nextjs-shadcn/components/ui/tabs.tsx +119 -0
- package/templates/storefront-nextjs-shadcn/components/ui/toast.tsx +96 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/index.ts +9 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-button.tsx +148 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +47 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-item.tsx +165 -0
- package/templates/storefront-nextjs-shadcn/components.json +19 -0
- package/templates/storefront-nextjs-shadcn/generated/.gitkeep +2 -0
- package/templates/storefront-nextjs-shadcn/graphql/.gitkeep +31 -0
- package/templates/storefront-nextjs-shadcn/graphql/collections.example.ts +168 -0
- package/templates/storefront-nextjs-shadcn/graphql/products.example.ts +160 -0
- package/templates/storefront-nextjs-shadcn/hooks/index.ts +9 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth.ts +310 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +286 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +110 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-filter-params.test.ts +173 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-filter-params.ts +298 -0
- package/templates/storefront-nextjs-shadcn/lib/auth/cookies.ts +220 -0
- package/templates/storefront-nextjs-shadcn/lib/auth/routes.ts +57 -0
- package/templates/storefront-nextjs-shadcn/lib/config.ts +46 -0
- package/templates/storefront-nextjs-shadcn/lib/currency/IMPLEMENTATION_SUMMARY.md +254 -0
- package/templates/storefront-nextjs-shadcn/lib/currency/README.md +464 -0
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.test.ts +328 -0
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.ts +295 -0
- package/templates/storefront-nextjs-shadcn/lib/currency/index.ts +27 -0
- package/templates/storefront-nextjs-shadcn/lib/format.test.ts +397 -0
- package/templates/storefront-nextjs-shadcn/lib/format.ts +226 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/client.ts +109 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +1183 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +267 -0
- package/templates/storefront-nextjs-shadcn/lib/hooks.ts +30 -0
- package/templates/storefront-nextjs-shadcn/lib/theme/theme-config.ts +89 -0
- package/templates/storefront-nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/storefront-nextjs-shadcn/next.config.ts +47 -0
- package/templates/storefront-nextjs-shadcn/open-next.config.ts +3 -0
- package/templates/storefront-nextjs-shadcn/package.dev.json +30 -0
- package/templates/storefront-nextjs-shadcn/package.json +60 -0
- package/templates/storefront-nextjs-shadcn/package.json.template +46 -0
- package/templates/storefront-nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/storefront-nextjs-shadcn/proxy.ts +80 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/apple-pay.svg +8 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/bank-transfer.svg +10 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/blik.svg +6 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/cash-on-delivery.svg +11 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/google-pay.svg +11 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/mastercard.svg +7 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/paypal.svg +7 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/payu.svg +7 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/przelewy24.svg +7 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/stripe.svg +4 -0
- package/templates/storefront-nextjs-shadcn/public/icons/payment/visa.svg +5 -0
- package/templates/storefront-nextjs-shadcn/stores/auth-store.ts +66 -0
- package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +56 -0
- package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +184 -0
- package/templates/storefront-nextjs-shadcn/stores/currency-store.ts +103 -0
- package/templates/storefront-nextjs-shadcn/stores/wishlist-store.ts +291 -0
- package/templates/storefront-nextjs-shadcn/tailwind.config.ts +111 -0
- package/templates/storefront-nextjs-shadcn/tsconfig.json +27 -0
- package/templates/storefront-nextjs-shadcn/wrangler.toml +9 -0
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { execSync, execFileSync } from 'child_process';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import * as p from '@clack/prompts';
|
|
8
|
+
import { createSharedApiClient } from '../lib/shared-api-client.js';
|
|
9
|
+
import { logger } from '../lib/logger.js';
|
|
10
|
+
import { getStoredRole } from './auth.js';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
/**
|
|
14
|
+
* Check that the current user has SaaS developer role.
|
|
15
|
+
* Exits with error if the role is insufficient.
|
|
16
|
+
* This is a UX improvement — the backend also rejects unauthorized requests.
|
|
17
|
+
*/
|
|
18
|
+
function requireSaasDeveloperRole() {
|
|
19
|
+
const role = getStoredRole()?.toLowerCase();
|
|
20
|
+
if (role && role !== 'admin' && role !== 'saas_developer') {
|
|
21
|
+
console.log(chalk.red('\n Error: This command requires SaaS developer role.'));
|
|
22
|
+
console.log(chalk.gray(' Your current role: ' + role + '\n'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function apiRequest(endpoint, options = {}) {
|
|
27
|
+
// Template operations are platform-level, not shop-level
|
|
28
|
+
// They don't require doswiftly.config.ts with shop.slug
|
|
29
|
+
const client = createSharedApiClient({ requireShopSlug: false });
|
|
30
|
+
logger.debug(`API Request: ${options.method || 'GET'} ${endpoint}`);
|
|
31
|
+
try {
|
|
32
|
+
const response = await client.request({
|
|
33
|
+
url: endpoint,
|
|
34
|
+
method: (options.method || 'GET'),
|
|
35
|
+
data: options.body ? JSON.parse(options.body) : undefined,
|
|
36
|
+
});
|
|
37
|
+
return response.data;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// Extract meaningful error message from axios error response
|
|
41
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
42
|
+
const axiosError = error;
|
|
43
|
+
const serverMessage = axiosError.response?.data?.message;
|
|
44
|
+
if (serverMessage) {
|
|
45
|
+
throw new Error(serverMessage);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List all available templates from the registry.
|
|
53
|
+
*/
|
|
54
|
+
export async function templateListCommand() {
|
|
55
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Template Registry\n'));
|
|
56
|
+
const spinner = ora('Fetching templates...').start();
|
|
57
|
+
try {
|
|
58
|
+
const templates = await apiRequest('/cli/templates');
|
|
59
|
+
spinner.stop();
|
|
60
|
+
if (!templates || templates.length === 0) {
|
|
61
|
+
console.log(chalk.yellow(' No templates available.\n'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
65
|
+
console.log(chalk.bold(' Name'.padEnd(25)) +
|
|
66
|
+
chalk.bold('Version'.padEnd(12)) +
|
|
67
|
+
chalk.bold('UI Library'.padEnd(15)) +
|
|
68
|
+
chalk.bold('Status'.padEnd(12)) +
|
|
69
|
+
chalk.bold('Description'));
|
|
70
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
71
|
+
for (const t of templates) {
|
|
72
|
+
const statusColor = t.status === 'PUBLISHED' ? chalk.green : chalk.yellow;
|
|
73
|
+
console.log(` ${chalk.cyan(t.name.padEnd(25))}` +
|
|
74
|
+
`${chalk.gray(t.version.padEnd(12))}` +
|
|
75
|
+
`${chalk.gray((t.uiLibrary || '-').padEnd(15))}` +
|
|
76
|
+
`${statusColor(t.status.padEnd(12))}` +
|
|
77
|
+
`${chalk.gray((t.description || '').slice(0, 30))}`);
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
80
|
+
console.log('');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
spinner.fail('Failed to fetch templates');
|
|
84
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Show template details.
|
|
90
|
+
*/
|
|
91
|
+
export async function templateInfoCommand(name) {
|
|
92
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Template Info\n'));
|
|
93
|
+
if (!name) {
|
|
94
|
+
console.log(chalk.red(' Error: Template name required\n'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const spinner = ora(`Fetching template '${name}'...`).start();
|
|
98
|
+
try {
|
|
99
|
+
const t = await apiRequest(`/cli/templates/${encodeURIComponent(name)}`);
|
|
100
|
+
spinner.stop();
|
|
101
|
+
console.log(` ${chalk.bold('Name:')} ${chalk.cyan(t.name)}`);
|
|
102
|
+
console.log(` ${chalk.bold('Version:')} ${t.version}`);
|
|
103
|
+
console.log(` ${chalk.bold('Status:')} ${t.status}`);
|
|
104
|
+
console.log(` ${chalk.bold('Visibility:')} ${t.visibility}`);
|
|
105
|
+
if (t.description) {
|
|
106
|
+
console.log(` ${chalk.bold('Description:')} ${t.description}`);
|
|
107
|
+
}
|
|
108
|
+
if (t.uiLibrary) {
|
|
109
|
+
console.log(` ${chalk.bold('UI Library:')} ${t.uiLibrary}`);
|
|
110
|
+
}
|
|
111
|
+
if (t.features.length > 0) {
|
|
112
|
+
console.log(` ${chalk.bold('Features:')} ${t.features.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
if (t.repoUrl) {
|
|
115
|
+
console.log(` ${chalk.bold('Repository:')} ${chalk.cyan(t.repoUrl)}`);
|
|
116
|
+
}
|
|
117
|
+
if (t.previewUrl) {
|
|
118
|
+
console.log(` ${chalk.bold('Preview:')} ${chalk.cyan(t.previewUrl)}`);
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
spinner.fail('Failed to fetch template');
|
|
124
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read doswiftly-template.json manifest from current directory.
|
|
130
|
+
*/
|
|
131
|
+
function readTemplateManifest() {
|
|
132
|
+
if (!existsSync('doswiftly-template.json'))
|
|
133
|
+
return null;
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(readFileSync('doswiftly-template.json', 'utf-8'));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Detect git remote URL from the current directory.
|
|
143
|
+
* Converts SSH URLs to HTTPS for degit compatibility.
|
|
144
|
+
*/
|
|
145
|
+
function detectGitRemoteUrl() {
|
|
146
|
+
try {
|
|
147
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
148
|
+
const url = execSync('git remote get-url origin', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
149
|
+
if (!url)
|
|
150
|
+
return null;
|
|
151
|
+
// Convert SSH to HTTPS
|
|
152
|
+
if (url.startsWith('git@github.com:')) {
|
|
153
|
+
return url.replace('git@github.com:', 'https://github.com/').replace(/\.git$/, '');
|
|
154
|
+
}
|
|
155
|
+
return url.replace(/\.git$/, '');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if git is initialized in the current directory.
|
|
163
|
+
*/
|
|
164
|
+
function isGitInitialized() {
|
|
165
|
+
try {
|
|
166
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Register a new template in the registry (SaaS developer only).
|
|
175
|
+
*
|
|
176
|
+
* Auto-detects:
|
|
177
|
+
* 1. Template name/description/uiLibrary from doswiftly-template.json
|
|
178
|
+
* 2. Git remote URL from `git remote get-url origin`
|
|
179
|
+
* 3. Falls back to CLI flags if auto-detect fails
|
|
180
|
+
*/
|
|
181
|
+
export async function templateRegisterCommand(options = {}) {
|
|
182
|
+
requireSaasDeveloperRole();
|
|
183
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Register Template\n'));
|
|
184
|
+
// Validate project structure
|
|
185
|
+
if (!existsSync('package.json')) {
|
|
186
|
+
console.log(chalk.red(' Error: No package.json found. Run this from a template project root.\n'));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
// Check git is initialized
|
|
190
|
+
if (!isGitInitialized()) {
|
|
191
|
+
console.log(chalk.red(' Error: Git is not initialized in this directory.'));
|
|
192
|
+
console.log(chalk.gray(' Run "git init" or "doswiftly init --create-template" first.\n'));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
// Auto-detect from manifest
|
|
196
|
+
const manifest = readTemplateManifest();
|
|
197
|
+
if (manifest) {
|
|
198
|
+
console.log(chalk.gray(' Auto-detected doswiftly-template.json'));
|
|
199
|
+
}
|
|
200
|
+
const name = options.name || manifest?.name || null;
|
|
201
|
+
const description = options.description || manifest?.description || undefined;
|
|
202
|
+
const uiLibrary = options.uiLibrary || manifest?.uiLibrary || undefined;
|
|
203
|
+
if (!name) {
|
|
204
|
+
console.log(chalk.red(' Error: Template name required.'));
|
|
205
|
+
console.log(chalk.gray(' Use --name or add "name" to doswiftly-template.json\n'));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
// Auto-detect git remote URL
|
|
209
|
+
let repoUrl = options.repoUrl || detectGitRemoteUrl();
|
|
210
|
+
if (repoUrl) {
|
|
211
|
+
console.log(chalk.gray(` Git remote: ${repoUrl}`));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log(chalk.yellow(' Warning: No git remote detected. Use --repo-url to specify.'));
|
|
215
|
+
}
|
|
216
|
+
// Show summary
|
|
217
|
+
console.log(chalk.gray('\n Registration summary:'));
|
|
218
|
+
console.log(chalk.gray(` Name: ${chalk.cyan(name)}`));
|
|
219
|
+
if (description)
|
|
220
|
+
console.log(chalk.gray(` Description: ${description}`));
|
|
221
|
+
if (repoUrl)
|
|
222
|
+
console.log(chalk.gray(` Repo URL: ${repoUrl}`));
|
|
223
|
+
if (uiLibrary)
|
|
224
|
+
console.log(chalk.gray(` UI Library: ${uiLibrary}`));
|
|
225
|
+
console.log('');
|
|
226
|
+
const spinner = ora(`Registering template '${name}'...`).start();
|
|
227
|
+
try {
|
|
228
|
+
const template = await apiRequest('/cli/templates', {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
name,
|
|
232
|
+
description,
|
|
233
|
+
repoUrl,
|
|
234
|
+
uiLibrary,
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
spinner.succeed(`Template '${template.name}' registered`);
|
|
238
|
+
console.log(chalk.gray(`\n ID: ${template.id}`));
|
|
239
|
+
console.log(chalk.gray(` Status: ${template.status}`));
|
|
240
|
+
console.log(chalk.gray('\n Next steps:'));
|
|
241
|
+
console.log(chalk.gray(' 1. Set version in package.json (e.g., "version": "1.0.0")'));
|
|
242
|
+
console.log(chalk.gray(' 2. Publish: doswiftly template publish'));
|
|
243
|
+
console.log(chalk.gray(' 3. Set visibility: doswiftly template visibility ' + template.name + ' PUBLIC\n'));
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
spinner.fail('Failed to register template');
|
|
247
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Update template metadata (SaaS developer only).
|
|
253
|
+
*/
|
|
254
|
+
export async function templateUpdateCommand(nameOrId, options = {}) {
|
|
255
|
+
requireSaasDeveloperRole();
|
|
256
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Update Template\n'));
|
|
257
|
+
if (!nameOrId) {
|
|
258
|
+
console.log(chalk.red(' Error: Template name or ID required\n'));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const body = {};
|
|
262
|
+
if (options.description !== undefined)
|
|
263
|
+
body.description = options.description;
|
|
264
|
+
if (options.repoUrl !== undefined)
|
|
265
|
+
body.repoUrl = options.repoUrl;
|
|
266
|
+
if (options.uiLibrary !== undefined)
|
|
267
|
+
body.uiLibrary = options.uiLibrary;
|
|
268
|
+
if (options.previewUrl !== undefined)
|
|
269
|
+
body.previewUrl = options.previewUrl;
|
|
270
|
+
if (options.visibility !== undefined)
|
|
271
|
+
body.visibility = options.visibility;
|
|
272
|
+
if (Object.keys(body).length === 0) {
|
|
273
|
+
console.log(chalk.yellow(' No fields to update. Use --description, --repo-url, --ui-library, --preview-url, or --visibility.\n'));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
const spinner = ora(`Updating template '${nameOrId}'...`).start();
|
|
277
|
+
try {
|
|
278
|
+
const template = await apiRequest(`/cli/templates/${encodeURIComponent(nameOrId)}`, {
|
|
279
|
+
method: 'PUT',
|
|
280
|
+
body: JSON.stringify(body),
|
|
281
|
+
});
|
|
282
|
+
spinner.succeed(`Template '${template.name}' updated`);
|
|
283
|
+
console.log(chalk.gray(`\n Name: ${template.name}`));
|
|
284
|
+
if (template.description)
|
|
285
|
+
console.log(chalk.gray(` Description: ${template.description}`));
|
|
286
|
+
if (template.repoUrl)
|
|
287
|
+
console.log(chalk.gray(` Repository: ${template.repoUrl}`));
|
|
288
|
+
console.log(chalk.gray(` Visibility: ${template.visibility}`));
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
spinner.fail('Failed to update template');
|
|
293
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Read version from package.json in current directory.
|
|
299
|
+
*/
|
|
300
|
+
function readPackageVersion() {
|
|
301
|
+
if (!existsSync('package.json'))
|
|
302
|
+
return null;
|
|
303
|
+
try {
|
|
304
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
|
|
305
|
+
return pkg.version || null;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Publish template with version from package.json (SaaS developer only).
|
|
313
|
+
*
|
|
314
|
+
* Reads version from:
|
|
315
|
+
* 1. package.json (preferred - npm ecosystem standard)
|
|
316
|
+
* 2. doswiftly-template.json (fallback)
|
|
317
|
+
*
|
|
318
|
+
* This ensures single source of truth for versioning.
|
|
319
|
+
*/
|
|
320
|
+
export async function templatePublishCommand(nameOrId) {
|
|
321
|
+
requireSaasDeveloperRole();
|
|
322
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Publish Template\n'));
|
|
323
|
+
// Auto-detect from manifest if nameOrId not provided
|
|
324
|
+
const manifest = readTemplateManifest();
|
|
325
|
+
const resolvedName = nameOrId || manifest?.name || null;
|
|
326
|
+
if (!resolvedName) {
|
|
327
|
+
console.log(chalk.red(' Error: Template name required.'));
|
|
328
|
+
console.log(chalk.gray(' Provide name as argument or run from a directory with doswiftly-template.json\n'));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
// Read version from package.json (primary) or doswiftly-template.json (fallback)
|
|
332
|
+
const packageVersion = readPackageVersion();
|
|
333
|
+
const manifestVersion = manifest?.version;
|
|
334
|
+
const version = packageVersion || manifestVersion;
|
|
335
|
+
if (!version) {
|
|
336
|
+
console.log(chalk.red(' Error: No version found.'));
|
|
337
|
+
console.log(chalk.gray(' Set "version" in package.json (recommended) or doswiftly-template.json\n'));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
// Show version source for transparency
|
|
341
|
+
const versionSource = packageVersion ? 'package.json' : 'doswiftly-template.json';
|
|
342
|
+
console.log(chalk.gray(` Version: ${chalk.cyan(version)} (from ${versionSource})`));
|
|
343
|
+
console.log('');
|
|
344
|
+
const spinner = ora(`Publishing template '${resolvedName}' v${version}...`).start();
|
|
345
|
+
try {
|
|
346
|
+
const template = await apiRequest(`/cli/templates/${encodeURIComponent(resolvedName)}/version`, {
|
|
347
|
+
method: 'PUT',
|
|
348
|
+
body: JSON.stringify({ version }),
|
|
349
|
+
});
|
|
350
|
+
spinner.succeed(`Template '${template.name}' v${template.version} published`);
|
|
351
|
+
console.log(chalk.gray(`\n Status: ${template.status}`));
|
|
352
|
+
console.log(chalk.gray(` Visibility: ${template.visibility}`));
|
|
353
|
+
if (template.visibility === 'PRIVATE') {
|
|
354
|
+
console.log(chalk.yellow('\n Note: Template is PRIVATE. Set visibility to PUBLIC for users to see it:'));
|
|
355
|
+
console.log(chalk.cyan(` doswiftly template visibility ${template.name} PUBLIC`));
|
|
356
|
+
}
|
|
357
|
+
console.log('');
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
spinner.fail('Failed to publish template');
|
|
361
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* @deprecated Use templatePublishCommand instead.
|
|
367
|
+
* Kept for backward compatibility - redirects to publish command.
|
|
368
|
+
*/
|
|
369
|
+
export async function templateVersionCommand(nameOrId, version) {
|
|
370
|
+
console.log(chalk.yellow('\n ⚠️ "template version" is deprecated. Use "template publish" instead.'));
|
|
371
|
+
console.log(chalk.gray(' The version is now read automatically from package.json.\n'));
|
|
372
|
+
// For backward compatibility, still allow manual version if provided
|
|
373
|
+
requireSaasDeveloperRole();
|
|
374
|
+
const spinner = ora(`Publishing template '${nameOrId}' v${version}...`).start();
|
|
375
|
+
try {
|
|
376
|
+
const template = await apiRequest(`/cli/templates/${encodeURIComponent(nameOrId)}/version`, {
|
|
377
|
+
method: 'PUT',
|
|
378
|
+
body: JSON.stringify({ version }),
|
|
379
|
+
});
|
|
380
|
+
spinner.succeed(`Template '${template.name}' v${template.version} published`);
|
|
381
|
+
console.log(chalk.gray(`\n Status: ${template.status}`));
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
spinner.fail('Failed to publish template');
|
|
386
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Unpublish a template (SaaS developer only).
|
|
392
|
+
*/
|
|
393
|
+
export async function templateUnpublishCommand(name) {
|
|
394
|
+
requireSaasDeveloperRole();
|
|
395
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Unpublish Template\n'));
|
|
396
|
+
if (!name) {
|
|
397
|
+
console.log(chalk.red(' Error: Template name required\n'));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const spinner = ora(`Unpublishing template '${name}'...`).start();
|
|
401
|
+
try {
|
|
402
|
+
await apiRequest(`/cli/templates/${encodeURIComponent(name)}/unpublish`, {
|
|
403
|
+
method: 'POST',
|
|
404
|
+
});
|
|
405
|
+
spinner.succeed(`Template '${name}' unpublished`);
|
|
406
|
+
console.log(chalk.yellow('\n Template has been deprecated and will no longer appear in listings.\n'));
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
spinner.fail('Failed to unpublish template');
|
|
410
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Set template visibility (SaaS developer only).
|
|
416
|
+
* Convenience wrapper around templateUpdateCommand.
|
|
417
|
+
*/
|
|
418
|
+
export async function templateVisibilityCommand(nameOrId, visibility) {
|
|
419
|
+
if (!nameOrId) {
|
|
420
|
+
console.log(chalk.red('\n Error: Template name or ID required\n'));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const normalizedVis = visibility?.toUpperCase();
|
|
424
|
+
if (!normalizedVis || !['PUBLIC', 'PRIVATE'].includes(normalizedVis)) {
|
|
425
|
+
console.log(chalk.red('\n Error: Visibility must be PUBLIC or PRIVATE\n'));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
return templateUpdateCommand(nameOrId, { visibility: normalizedVis });
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Recursively copy directory with placeholder replacement.
|
|
432
|
+
*/
|
|
433
|
+
function copyTemplateDir(src, dest, replacements) {
|
|
434
|
+
mkdirSync(dest, { recursive: true });
|
|
435
|
+
const entries = readdirSync(src);
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
const srcPath = join(src, entry);
|
|
438
|
+
const destPath = join(dest, entry);
|
|
439
|
+
const stat = statSync(srcPath);
|
|
440
|
+
if (stat.isDirectory()) {
|
|
441
|
+
copyTemplateDir(srcPath, destPath, replacements);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
445
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
446
|
+
content = content.replace(new RegExp(placeholder, 'g'), value);
|
|
447
|
+
}
|
|
448
|
+
writeFileSync(destPath, content);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Recursively read all files from a directory.
|
|
454
|
+
* Returns array of { path, content } for API upload.
|
|
455
|
+
*/
|
|
456
|
+
function readAllFiles(dir, baseDir = dir) {
|
|
457
|
+
const files = [];
|
|
458
|
+
const entries = readdirSync(dir);
|
|
459
|
+
for (const entry of entries) {
|
|
460
|
+
const fullPath = join(dir, entry);
|
|
461
|
+
const relativePath = fullPath.replace(baseDir + '/', '').replace(baseDir + '\\', '');
|
|
462
|
+
const stat = statSync(fullPath);
|
|
463
|
+
if (stat.isDirectory()) {
|
|
464
|
+
// Skip .git directory
|
|
465
|
+
if (entry === '.git')
|
|
466
|
+
continue;
|
|
467
|
+
files.push(...readAllFiles(fullPath, baseDir));
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
try {
|
|
471
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
472
|
+
// Use forward slashes for GitHub API
|
|
473
|
+
files.push({ path: relativePath.replace(/\\/g, '/'), content });
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Skip binary files or unreadable files
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return files;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Create a new template with optional GitHub repository creation.
|
|
484
|
+
* Full flow: scaffold → create GitHub repo (optional) → register → provide next steps.
|
|
485
|
+
*/
|
|
486
|
+
export async function templateCreateCommand(options = {}) {
|
|
487
|
+
requireSaasDeveloperRole();
|
|
488
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Create Template\n'));
|
|
489
|
+
// Step 1: Template name
|
|
490
|
+
let templateName = options.name;
|
|
491
|
+
if (!templateName) {
|
|
492
|
+
const nameResult = await p.text({
|
|
493
|
+
message: 'Template name:',
|
|
494
|
+
placeholder: 'my-awesome-template',
|
|
495
|
+
validate: (v) => {
|
|
496
|
+
if (!v)
|
|
497
|
+
return 'Name is required';
|
|
498
|
+
if (!/^[a-z0-9-]+$/.test(v))
|
|
499
|
+
return 'Use lowercase letters, numbers, and hyphens only';
|
|
500
|
+
if (v.length < 3)
|
|
501
|
+
return 'At least 3 characters';
|
|
502
|
+
return undefined;
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (p.isCancel(nameResult)) {
|
|
506
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
507
|
+
process.exit(0);
|
|
508
|
+
}
|
|
509
|
+
templateName = nameResult;
|
|
510
|
+
}
|
|
511
|
+
// Validate name format
|
|
512
|
+
if (!/^[a-z0-9-]+$/.test(templateName)) {
|
|
513
|
+
console.log(chalk.red('\n Error: Template name must be lowercase, alphanumeric, and may contain hyphens.\n'));
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
// Step 2: Description
|
|
517
|
+
let description = options.description;
|
|
518
|
+
if (!description) {
|
|
519
|
+
const descResult = await p.text({
|
|
520
|
+
message: 'Template description (optional):',
|
|
521
|
+
placeholder: `DoSwiftly storefront template: ${templateName}`,
|
|
522
|
+
});
|
|
523
|
+
if (p.isCancel(descResult)) {
|
|
524
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
525
|
+
process.exit(0);
|
|
526
|
+
}
|
|
527
|
+
description = descResult || `DoSwiftly storefront template: ${templateName}`;
|
|
528
|
+
}
|
|
529
|
+
// Step 3: Base template selection
|
|
530
|
+
let baseTemplate = options.base;
|
|
531
|
+
if (!baseTemplate) {
|
|
532
|
+
const baseResult = await p.select({
|
|
533
|
+
message: 'Base template to start from:',
|
|
534
|
+
options: [
|
|
535
|
+
{
|
|
536
|
+
value: 'storefront-nextjs-shadcn',
|
|
537
|
+
label: 'shadcn/ui (Recommended)',
|
|
538
|
+
hint: 'Next.js + shadcn/ui + Tailwind CSS',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
value: 'storefront-minimal',
|
|
542
|
+
label: 'Minimal (Next.js only)',
|
|
543
|
+
hint: 'Minimal Next.js starter',
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
if (p.isCancel(baseResult)) {
|
|
548
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
549
|
+
process.exit(0);
|
|
550
|
+
}
|
|
551
|
+
baseTemplate = baseResult;
|
|
552
|
+
}
|
|
553
|
+
// UI Library mapping
|
|
554
|
+
const uiLibraryMap = {
|
|
555
|
+
'storefront-nextjs-shadcn': 'shadcn/ui',
|
|
556
|
+
'storefront-minimal': 'none',
|
|
557
|
+
};
|
|
558
|
+
const uiLibrary = uiLibraryMap[baseTemplate] || 'none';
|
|
559
|
+
// Step 4: GitHub repository creation
|
|
560
|
+
let createGitHubRepo = false;
|
|
561
|
+
let repoInfo = null;
|
|
562
|
+
let githubUsername;
|
|
563
|
+
if (!options.skipGithub) {
|
|
564
|
+
const githubResult = await p.confirm({
|
|
565
|
+
message: 'Create GitHub repository in DoSwiftly organization?',
|
|
566
|
+
initialValue: true,
|
|
567
|
+
});
|
|
568
|
+
if (p.isCancel(githubResult)) {
|
|
569
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
570
|
+
process.exit(0);
|
|
571
|
+
}
|
|
572
|
+
createGitHubRepo = githubResult;
|
|
573
|
+
// Ask for GitHub username to add as collaborator (optional - not needed for org owners)
|
|
574
|
+
if (createGitHubRepo) {
|
|
575
|
+
const usernameResult = await p.text({
|
|
576
|
+
message: 'GitHub username to add as collaborator (Enter to skip):',
|
|
577
|
+
placeholder: ' ',
|
|
578
|
+
defaultValue: '',
|
|
579
|
+
validate: (v) => {
|
|
580
|
+
if (!v || v.trim() === '')
|
|
581
|
+
return undefined; // Optional
|
|
582
|
+
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(v.trim())) {
|
|
583
|
+
return 'Invalid GitHub username format';
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
if (p.isCancel(usernameResult)) {
|
|
589
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
const trimmed = usernameResult?.trim();
|
|
593
|
+
githubUsername = trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Create target directory
|
|
597
|
+
const targetDir = templateName;
|
|
598
|
+
if (existsSync(targetDir)) {
|
|
599
|
+
console.log(chalk.red(`\n Error: Directory "${targetDir}" already exists.\n`));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
// Step 5: Create GitHub repo if requested
|
|
603
|
+
if (createGitHubRepo) {
|
|
604
|
+
const repoSpinner = ora('Creating GitHub repository...').start();
|
|
605
|
+
try {
|
|
606
|
+
repoInfo = await apiRequest('/cli/templates/create-repo', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
body: JSON.stringify({ name: templateName, description, githubUsername }),
|
|
609
|
+
});
|
|
610
|
+
if (repoInfo.collaboratorAdded) {
|
|
611
|
+
repoSpinner.succeed(`GitHub repository created: ${chalk.cyan(repoInfo.fullName)} ` +
|
|
612
|
+
`(${chalk.green(repoInfo.collaboratorUsername)} added as admin)`);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
repoSpinner.succeed(`GitHub repository created: ${chalk.cyan(repoInfo.fullName)}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
repoSpinner.fail('Failed to create GitHub repository');
|
|
620
|
+
const errorMsg = error.message;
|
|
621
|
+
// Check if it's a configuration/installation issue
|
|
622
|
+
if (errorMsg.includes('not configured') || errorMsg.includes('not installed')) {
|
|
623
|
+
console.log(chalk.yellow('\n GitHub integration is not available.'));
|
|
624
|
+
console.log(chalk.gray(' You can continue without GitHub integration.\n'));
|
|
625
|
+
const continueResult = await p.confirm({
|
|
626
|
+
message: 'Continue without GitHub integration?',
|
|
627
|
+
initialValue: true,
|
|
628
|
+
});
|
|
629
|
+
if (p.isCancel(continueResult) || !continueResult) {
|
|
630
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
631
|
+
process.exit(0);
|
|
632
|
+
}
|
|
633
|
+
createGitHubRepo = false;
|
|
634
|
+
}
|
|
635
|
+
else if (errorMsg.includes('already exists')) {
|
|
636
|
+
console.log(chalk.red(`\n ${errorMsg}\n`));
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
else if (errorMsg.includes('permission') || errorMsg.includes('403')) {
|
|
640
|
+
// Permission-related error - offer to continue without GitHub
|
|
641
|
+
console.log(chalk.yellow('\n GitHub App does not have required permissions.'));
|
|
642
|
+
console.log(chalk.gray(` ${errorMsg}\n`));
|
|
643
|
+
const continueResult = await p.confirm({
|
|
644
|
+
message: 'Continue without GitHub integration?',
|
|
645
|
+
initialValue: true,
|
|
646
|
+
});
|
|
647
|
+
if (p.isCancel(continueResult) || !continueResult) {
|
|
648
|
+
console.log(chalk.yellow('\n Operation cancelled.\n'));
|
|
649
|
+
process.exit(0);
|
|
650
|
+
}
|
|
651
|
+
createGitHubRepo = false;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
console.log(chalk.red(`\n ${errorMsg}\n`));
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Step 6: Scaffold the template locally
|
|
660
|
+
const scaffoldSpinner = ora('Creating template project...').start();
|
|
661
|
+
try {
|
|
662
|
+
const templateDir = join(__dirname, '..', '..', 'templates', baseTemplate);
|
|
663
|
+
if (!existsSync(templateDir)) {
|
|
664
|
+
throw new Error(`Base template not found: ${baseTemplate}`);
|
|
665
|
+
}
|
|
666
|
+
// Copy template files with generic placeholders
|
|
667
|
+
const replacements = {
|
|
668
|
+
'\\{\\{PROJECT_NAME\\}\\}': templateName,
|
|
669
|
+
'\\{\\{SHOP_SLUG\\}\\}': '{{SHOP_SLUG}}',
|
|
670
|
+
'\\{\\{API_URL\\}\\}': '{{API_URL}}',
|
|
671
|
+
'\\{\\{SDK_VERSION\\}\\}': '^1.0.0',
|
|
672
|
+
'\\{\\{COMMERCE_SDK_VERSION\\}\\}': '^1.0.0',
|
|
673
|
+
'\\{\\{STOREFRONT_OPS_VERSION\\}\\}': '^1.0.0',
|
|
674
|
+
};
|
|
675
|
+
copyTemplateDir(templateDir, targetDir, replacements);
|
|
676
|
+
// Create doswiftly-template.json manifest
|
|
677
|
+
const manifest = {
|
|
678
|
+
name: templateName,
|
|
679
|
+
description,
|
|
680
|
+
uiLibrary,
|
|
681
|
+
features: [],
|
|
682
|
+
version: '0.1.0',
|
|
683
|
+
};
|
|
684
|
+
writeFileSync(join(targetDir, 'doswiftly-template.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
685
|
+
// Initialize git
|
|
686
|
+
scaffoldSpinner.text = 'Initializing git repository...';
|
|
687
|
+
let gitInitialized = false;
|
|
688
|
+
let localBranch = 'main';
|
|
689
|
+
try {
|
|
690
|
+
execFileSync('git', ['init'], { stdio: 'pipe', cwd: targetDir });
|
|
691
|
+
execFileSync('git', ['add', '-A'], { stdio: 'pipe', cwd: targetDir });
|
|
692
|
+
// Check if git user is configured, if not set temporary values
|
|
693
|
+
try {
|
|
694
|
+
execFileSync('git', ['config', 'user.name'], { stdio: 'pipe', cwd: targetDir });
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
execFileSync('git', ['config', 'user.name', 'DoSwiftly CLI'], { stdio: 'pipe', cwd: targetDir });
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
execFileSync('git', ['config', 'user.email'], { stdio: 'pipe', cwd: targetDir });
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
execFileSync('git', ['config', 'user.email', 'cli@doswiftly.com'], { stdio: 'pipe', cwd: targetDir });
|
|
704
|
+
}
|
|
705
|
+
execFileSync('git', ['commit', '-m', `chore: initialize ${templateName} template`], {
|
|
706
|
+
stdio: 'pipe',
|
|
707
|
+
cwd: targetDir,
|
|
708
|
+
});
|
|
709
|
+
// Get the actual branch name (might be 'master' on older git)
|
|
710
|
+
try {
|
|
711
|
+
localBranch = execSync('git branch --show-current', {
|
|
712
|
+
stdio: 'pipe',
|
|
713
|
+
cwd: targetDir,
|
|
714
|
+
encoding: 'utf-8',
|
|
715
|
+
}).trim() || 'main';
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
localBranch = 'main';
|
|
719
|
+
}
|
|
720
|
+
gitInitialized = true;
|
|
721
|
+
}
|
|
722
|
+
catch (gitError) {
|
|
723
|
+
const err = gitError;
|
|
724
|
+
const errorMsg = err.stderr || err.message || 'Unknown git error';
|
|
725
|
+
scaffoldSpinner.fail('Failed to initialize git');
|
|
726
|
+
console.log(chalk.red(`\n Git error: ${errorMsg.trim()}\n`));
|
|
727
|
+
// Continue without git - user can init manually
|
|
728
|
+
}
|
|
729
|
+
// Add remote if GitHub repo was created
|
|
730
|
+
if (repoInfo) {
|
|
731
|
+
scaffoldSpinner.text = 'Configuring git remote...';
|
|
732
|
+
// Detect if user has SSH configured for GitHub
|
|
733
|
+
let useSSH = false;
|
|
734
|
+
try {
|
|
735
|
+
// Check if SSH key auth works with GitHub
|
|
736
|
+
execSync('ssh -T git@github.com 2>&1 || true', { stdio: 'pipe', encoding: 'utf-8' });
|
|
737
|
+
// Check if user has SSH in their git config or .ssh folder
|
|
738
|
+
const sshTest = execSync('ssh -o BatchMode=yes -o StrictHostKeyChecking=no git@github.com 2>&1 || true', {
|
|
739
|
+
stdio: 'pipe',
|
|
740
|
+
encoding: 'utf-8',
|
|
741
|
+
});
|
|
742
|
+
// GitHub returns "successfully authenticated" even with exit code 1
|
|
743
|
+
if (sshTest.includes('successfully authenticated') || sshTest.includes('Hi ')) {
|
|
744
|
+
useSSH = true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// SSH not configured, use HTTPS
|
|
749
|
+
}
|
|
750
|
+
const remoteUrl = useSSH ? repoInfo.sshUrl : repoInfo.cloneUrl;
|
|
751
|
+
try {
|
|
752
|
+
execSync(`git remote add origin ${remoteUrl}`, {
|
|
753
|
+
stdio: 'pipe',
|
|
754
|
+
cwd: targetDir,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
// Non-fatal
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
scaffoldSpinner.succeed('Template project created');
|
|
762
|
+
// Step 7: Push to GitHub using local git
|
|
763
|
+
if (repoInfo && gitInitialized) {
|
|
764
|
+
const pushSpinner = ora('Pushing to GitHub...').start();
|
|
765
|
+
try {
|
|
766
|
+
// Try to push using local git credentials
|
|
767
|
+
// Use the local branch name, not the remote default (they might differ)
|
|
768
|
+
execSync(`git push -u origin ${localBranch}`, {
|
|
769
|
+
cwd: targetDir,
|
|
770
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
771
|
+
encoding: 'utf-8',
|
|
772
|
+
});
|
|
773
|
+
pushSpinner.succeed('Pushed to GitHub');
|
|
774
|
+
}
|
|
775
|
+
catch (pushError) {
|
|
776
|
+
pushSpinner.fail('Failed to push to GitHub');
|
|
777
|
+
// Extract and show the actual git error
|
|
778
|
+
const err = pushError;
|
|
779
|
+
const gitError = err.stderr || err.message || 'Unknown error';
|
|
780
|
+
console.log(chalk.red('\n Git error:'));
|
|
781
|
+
console.log(chalk.gray(` ${gitError.trim().split('\n').join('\n ')}`));
|
|
782
|
+
// Provide helpful suggestions
|
|
783
|
+
console.log(chalk.yellow('\n Possible solutions:'));
|
|
784
|
+
console.log(chalk.gray(' 1. Run: gh auth login (if you have GitHub CLI)'));
|
|
785
|
+
console.log(chalk.gray(' 2. Configure SSH key for GitHub'));
|
|
786
|
+
console.log(chalk.gray(' 3. Use HTTPS with credential manager'));
|
|
787
|
+
console.log(chalk.yellow('\n Then push manually:'));
|
|
788
|
+
console.log(chalk.cyan(` cd ${templateName} && git push -u origin ${localBranch}\n`));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else if (repoInfo && !gitInitialized) {
|
|
792
|
+
console.log(chalk.yellow('\n Git not initialized. Push manually after fixing git config:'));
|
|
793
|
+
console.log(chalk.cyan(` cd ${templateName} && git init && git add -A && git commit -m "Initial commit" && git push -u origin main\n`));
|
|
794
|
+
}
|
|
795
|
+
// Step 8: Auto-register the template
|
|
796
|
+
if (repoInfo) {
|
|
797
|
+
const registerSpinner = ora('Registering template in registry...').start();
|
|
798
|
+
try {
|
|
799
|
+
const template = await apiRequest('/cli/templates', {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
body: JSON.stringify({
|
|
802
|
+
name: templateName,
|
|
803
|
+
description,
|
|
804
|
+
repoUrl: repoInfo.repoUrl,
|
|
805
|
+
uiLibrary,
|
|
806
|
+
...(repoInfo.repoId && { githubRepoId: repoInfo.repoId }),
|
|
807
|
+
...(repoInfo.ownerId && { githubOrgId: repoInfo.ownerId }),
|
|
808
|
+
}),
|
|
809
|
+
});
|
|
810
|
+
registerSpinner.succeed(`Template registered: ${chalk.cyan(template.name)}`);
|
|
811
|
+
}
|
|
812
|
+
catch (regError) {
|
|
813
|
+
registerSpinner.fail('Failed to register template');
|
|
814
|
+
const errorMsg = regError.message || 'Unknown error';
|
|
815
|
+
console.log(chalk.red(` Error: ${errorMsg}`));
|
|
816
|
+
console.log(chalk.yellow(' You can register manually with:'));
|
|
817
|
+
console.log(chalk.gray(` cd ${templateName} && doswiftly template register\n`));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Show CF Builds status
|
|
821
|
+
if (repoInfo?.buildsSetup) {
|
|
822
|
+
if (repoInfo.buildsSetup.success) {
|
|
823
|
+
console.log(chalk.green('\n Cloudflare Workers Builds configured!'));
|
|
824
|
+
console.log(chalk.gray(` Worker: storefront-template-${templateName}`));
|
|
825
|
+
console.log(chalk.gray(' Auto-deploy: pushes to main/master trigger build + deploy'));
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
console.log(chalk.yellow('\n CF Builds setup failed: ' + (repoInfo.buildsSetup.error || 'unknown')));
|
|
829
|
+
console.log(chalk.gray(` Retry with: doswiftly template setup-builds ${templateName}`));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
scaffoldSpinner.fail('Failed to create template project');
|
|
835
|
+
console.log(chalk.red(`\n ${error.message}\n`));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
// Success summary
|
|
839
|
+
console.log(chalk.green('\n ✓ Template created successfully!\n'));
|
|
840
|
+
console.log(chalk.gray(' Location: ') + chalk.cyan(`${targetDir}/`));
|
|
841
|
+
if (repoInfo) {
|
|
842
|
+
console.log(chalk.gray(' Repository: ') + chalk.cyan(repoInfo.repoUrl));
|
|
843
|
+
}
|
|
844
|
+
console.log(chalk.bold('\n Next steps:\n'));
|
|
845
|
+
console.log(chalk.cyan(` cd ${templateName}`));
|
|
846
|
+
console.log(chalk.gray(' # Customize your template'));
|
|
847
|
+
if (!repoInfo) {
|
|
848
|
+
console.log(chalk.cyan(' git remote add origin <your-github-repo-url>'));
|
|
849
|
+
console.log(chalk.cyan(' git push -u origin main'));
|
|
850
|
+
console.log(chalk.cyan(' doswiftly template register'));
|
|
851
|
+
console.log(chalk.gray(' # Update version in package.json, then:'));
|
|
852
|
+
console.log(chalk.cyan(' doswiftly template publish'));
|
|
853
|
+
}
|
|
854
|
+
else if (repoInfo.buildsSetup?.success) {
|
|
855
|
+
console.log(chalk.gray(' # Make changes, commit and push:'));
|
|
856
|
+
console.log(chalk.cyan(' git add -A && git commit -m "feat: ..." && git push'));
|
|
857
|
+
console.log(chalk.gray(' # CF Builds will auto-build and deploy on push'));
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
console.log(chalk.gray(' # Update version in package.json, then:'));
|
|
861
|
+
console.log(chalk.cyan(' doswiftly template publish'));
|
|
862
|
+
}
|
|
863
|
+
console.log('');
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Validate a template project before publishing.
|
|
867
|
+
* Checks:
|
|
868
|
+
* 1. doswiftly-template.json exists and is valid
|
|
869
|
+
* 2. package.json exists with required fields
|
|
870
|
+
* 3. Required dependencies are present
|
|
871
|
+
* 4. Git remote is configured
|
|
872
|
+
* 5. Build succeeds (optional, with --build flag)
|
|
873
|
+
*/
|
|
874
|
+
export async function templateValidateCommand(options = {}) {
|
|
875
|
+
requireSaasDeveloperRole();
|
|
876
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Validate Template\n'));
|
|
877
|
+
const result = {
|
|
878
|
+
valid: true,
|
|
879
|
+
errors: [],
|
|
880
|
+
warnings: [],
|
|
881
|
+
};
|
|
882
|
+
// Check 1: doswiftly-template.json
|
|
883
|
+
console.log(chalk.gray(' Checking doswiftly-template.json...'));
|
|
884
|
+
const manifest = readTemplateManifest();
|
|
885
|
+
if (!manifest) {
|
|
886
|
+
result.valid = false;
|
|
887
|
+
result.errors.push('doswiftly-template.json not found. Create one with required fields: name, description, uiLibrary');
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
// Validate required fields
|
|
891
|
+
if (!manifest.name) {
|
|
892
|
+
result.valid = false;
|
|
893
|
+
result.errors.push('doswiftly-template.json: "name" field is required');
|
|
894
|
+
}
|
|
895
|
+
else if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
896
|
+
result.valid = false;
|
|
897
|
+
result.errors.push('doswiftly-template.json: "name" must be lowercase, alphanumeric with hyphens');
|
|
898
|
+
}
|
|
899
|
+
if (!manifest.description) {
|
|
900
|
+
result.warnings.push('doswiftly-template.json: "description" is recommended');
|
|
901
|
+
}
|
|
902
|
+
if (!manifest.uiLibrary) {
|
|
903
|
+
result.warnings.push('doswiftly-template.json: "uiLibrary" is recommended (e.g., "shadcn/ui", "none")');
|
|
904
|
+
}
|
|
905
|
+
if (!manifest.version) {
|
|
906
|
+
result.warnings.push('doswiftly-template.json: "version" is recommended');
|
|
907
|
+
}
|
|
908
|
+
console.log(chalk.green(' ✓ doswiftly-template.json exists'));
|
|
909
|
+
}
|
|
910
|
+
// Check 2: package.json
|
|
911
|
+
console.log(chalk.gray(' Checking package.json...'));
|
|
912
|
+
if (!existsSync('package.json')) {
|
|
913
|
+
result.valid = false;
|
|
914
|
+
result.errors.push('package.json not found');
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
try {
|
|
918
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
|
|
919
|
+
if (!pkg.name) {
|
|
920
|
+
result.warnings.push('package.json: "name" field is recommended');
|
|
921
|
+
}
|
|
922
|
+
// Check for required dependencies
|
|
923
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
924
|
+
const requiredDeps = ['next', 'react', 'react-dom'];
|
|
925
|
+
const missingDeps = requiredDeps.filter(dep => !allDeps[dep]);
|
|
926
|
+
if (missingDeps.length > 0) {
|
|
927
|
+
result.warnings.push(`package.json: Missing recommended dependencies: ${missingDeps.join(', ')}`);
|
|
928
|
+
}
|
|
929
|
+
// Check for DoSwiftly SDK
|
|
930
|
+
if (!allDeps['@doswiftly/commerce-sdk'] && !allDeps['@doswiftly/storefront-operations']) {
|
|
931
|
+
result.warnings.push('package.json: Consider adding @doswiftly/commerce-sdk for storefront functionality');
|
|
932
|
+
}
|
|
933
|
+
console.log(chalk.green(' ✓ package.json exists'));
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
result.valid = false;
|
|
937
|
+
result.errors.push('package.json is not valid JSON');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Check 3: Git initialization
|
|
941
|
+
console.log(chalk.gray(' Checking git configuration...'));
|
|
942
|
+
if (!isGitInitialized()) {
|
|
943
|
+
result.warnings.push('Git is not initialized. Run "git init" before publishing');
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
const remoteUrl = detectGitRemoteUrl();
|
|
947
|
+
if (!remoteUrl) {
|
|
948
|
+
result.warnings.push('No git remote configured. Add one with "git remote add origin <url>"');
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
console.log(chalk.green(` ✓ Git remote: ${remoteUrl}`));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Check 4: Required files
|
|
955
|
+
console.log(chalk.gray(' Checking required files...'));
|
|
956
|
+
const recommendedFiles = [
|
|
957
|
+
{ path: 'README.md', required: false },
|
|
958
|
+
{ path: '.gitignore', required: false },
|
|
959
|
+
{ path: 'tsconfig.json', required: false },
|
|
960
|
+
];
|
|
961
|
+
for (const file of recommendedFiles) {
|
|
962
|
+
if (!existsSync(file.path)) {
|
|
963
|
+
if (file.required) {
|
|
964
|
+
result.valid = false;
|
|
965
|
+
result.errors.push(`Required file missing: ${file.path}`);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
result.warnings.push(`Recommended file missing: ${file.path}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Check 5: Placeholder tokens (should not be resolved yet in templates)
|
|
973
|
+
console.log(chalk.gray(' Checking placeholder tokens...'));
|
|
974
|
+
const checkFiles = ['package.json', 'doswiftly-template.json'];
|
|
975
|
+
for (const file of checkFiles) {
|
|
976
|
+
if (existsSync(file)) {
|
|
977
|
+
const content = readFileSync(file, 'utf-8');
|
|
978
|
+
// Templates should contain placeholders like {{SHOP_SLUG}}
|
|
979
|
+
if (content.includes('{{SHOP_SLUG}}') || content.includes('{{API_URL}}')) {
|
|
980
|
+
console.log(chalk.green(` ✓ ${file} contains expected placeholders`));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Check 6: Build test (optional)
|
|
985
|
+
if (options.build) {
|
|
986
|
+
console.log(chalk.gray(' Running build test...'));
|
|
987
|
+
const buildSpinner = ora('Building project...').start();
|
|
988
|
+
try {
|
|
989
|
+
execSync('npm run build', { stdio: 'pipe' });
|
|
990
|
+
buildSpinner.succeed('Build succeeded');
|
|
991
|
+
}
|
|
992
|
+
catch (buildError) {
|
|
993
|
+
buildSpinner.fail('Build failed');
|
|
994
|
+
result.valid = false;
|
|
995
|
+
result.errors.push('Build failed. Fix build errors before publishing.');
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Summary
|
|
999
|
+
console.log('');
|
|
1000
|
+
if (result.errors.length > 0) {
|
|
1001
|
+
console.log(chalk.red.bold(' Errors:'));
|
|
1002
|
+
for (const error of result.errors) {
|
|
1003
|
+
console.log(chalk.red(` ✗ ${error}`));
|
|
1004
|
+
}
|
|
1005
|
+
console.log('');
|
|
1006
|
+
}
|
|
1007
|
+
if (result.warnings.length > 0) {
|
|
1008
|
+
console.log(chalk.yellow.bold(' Warnings:'));
|
|
1009
|
+
for (const warning of result.warnings) {
|
|
1010
|
+
console.log(chalk.yellow(` ⚠ ${warning}`));
|
|
1011
|
+
}
|
|
1012
|
+
console.log('');
|
|
1013
|
+
}
|
|
1014
|
+
if (result.valid && result.errors.length === 0) {
|
|
1015
|
+
console.log(chalk.green.bold(' ✓ Template is valid and ready for publishing!\n'));
|
|
1016
|
+
console.log(chalk.gray(' Next steps:'));
|
|
1017
|
+
console.log(chalk.cyan(' doswiftly template register'));
|
|
1018
|
+
console.log(chalk.cyan(' doswiftly template version <name> 1.0.0\n'));
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
console.log(chalk.red.bold(' ✗ Template validation failed. Fix errors before publishing.\n'));
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// ============================================================================
|
|
1026
|
+
// Template Build Command (OpenNext for Cloudflare)
|
|
1027
|
+
// ============================================================================
|
|
1028
|
+
/**
|
|
1029
|
+
* Build a template for Cloudflare Workers using OpenNext.
|
|
1030
|
+
* Runs: pnpm build && npx @opennextjs/cloudflare build
|
|
1031
|
+
*/
|
|
1032
|
+
export async function templateBuildCommand(options = {}) {
|
|
1033
|
+
requireSaasDeveloperRole();
|
|
1034
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Build Template for Cloudflare\n'));
|
|
1035
|
+
// Check for required files
|
|
1036
|
+
if (!existsSync('package.json')) {
|
|
1037
|
+
console.log(chalk.red(' Error: No package.json found. Run this from a template project root.\n'));
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
// Read manifest for template name
|
|
1041
|
+
const manifest = readTemplateManifest();
|
|
1042
|
+
const templateName = manifest?.name || 'template';
|
|
1043
|
+
console.log(chalk.gray(` Template: ${chalk.cyan(templateName)}`));
|
|
1044
|
+
// Check for @opennextjs/cloudflare
|
|
1045
|
+
let hasOpenNext = false;
|
|
1046
|
+
try {
|
|
1047
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
|
|
1048
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1049
|
+
hasOpenNext = !!allDeps['@opennextjs/cloudflare'] || !!allDeps['@opennextjs/cloudflare'];
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
// Continue without check
|
|
1053
|
+
}
|
|
1054
|
+
if (!hasOpenNext) {
|
|
1055
|
+
console.log(chalk.yellow('\n Warning: @opennextjs/cloudflare not found in dependencies.'));
|
|
1056
|
+
console.log(chalk.gray(' Install it with: pnpm add -D @opennextjs/cloudflare'));
|
|
1057
|
+
console.log(chalk.gray(' Or: npx @opennextjs/cloudflare build (will use npx version)\n'));
|
|
1058
|
+
}
|
|
1059
|
+
// Step 1: Run Next.js build (unless skipped)
|
|
1060
|
+
if (!options.skipNextBuild) {
|
|
1061
|
+
const nextSpinner = ora('Building Next.js application...').start();
|
|
1062
|
+
try {
|
|
1063
|
+
// Try pnpm first, then npm
|
|
1064
|
+
try {
|
|
1065
|
+
execSync('pnpm build', { stdio: 'pipe', cwd: process.cwd() });
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
execSync('npm run build', { stdio: 'pipe', cwd: process.cwd() });
|
|
1069
|
+
}
|
|
1070
|
+
nextSpinner.succeed('Next.js build completed');
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
nextSpinner.fail('Next.js build failed');
|
|
1074
|
+
const err = error;
|
|
1075
|
+
const stderr = err.stderr?.toString() || err.message || '';
|
|
1076
|
+
console.log(chalk.red(`\n ${stderr.split('\n').slice(0, 10).join('\n ')}\n`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
console.log(chalk.gray(' Skipping Next.js build (--skip-next-build)\n'));
|
|
1082
|
+
}
|
|
1083
|
+
// Step 2: Run OpenNext build for Cloudflare
|
|
1084
|
+
const openNextSpinner = ora('Building for Cloudflare Workers (OpenNext)...').start();
|
|
1085
|
+
try {
|
|
1086
|
+
execSync('npx @opennextjs/cloudflare build', { stdio: 'pipe', cwd: process.cwd() });
|
|
1087
|
+
openNextSpinner.succeed('OpenNext build completed');
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
openNextSpinner.fail('OpenNext build failed');
|
|
1091
|
+
const err = error;
|
|
1092
|
+
const stderr = err.stderr?.toString() || err.message || '';
|
|
1093
|
+
console.log(chalk.red(`\n ${stderr.split('\n').slice(0, 10).join('\n ')}\n`));
|
|
1094
|
+
console.log(chalk.yellow(' Make sure @opennextjs/cloudflare is installed:'));
|
|
1095
|
+
console.log(chalk.gray(' pnpm add -D @opennextjs/cloudflare\n'));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
// Check output
|
|
1099
|
+
const workerPath = '.open-next/worker.js';
|
|
1100
|
+
if (!existsSync(workerPath)) {
|
|
1101
|
+
console.log(chalk.red(`\n Error: Worker file not found at ${workerPath}`));
|
|
1102
|
+
console.log(chalk.gray(' The OpenNext build may have failed silently.\n'));
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
const workerStats = statSync(workerPath);
|
|
1106
|
+
const sizeKB = (workerStats.size / 1024).toFixed(1);
|
|
1107
|
+
console.log(chalk.green(`\n ✓ Build successful!`));
|
|
1108
|
+
console.log(chalk.gray(` Worker: ${workerPath} (${sizeKB} KB)`));
|
|
1109
|
+
console.log(chalk.gray(`\n Next: Upload to registry with:`));
|
|
1110
|
+
console.log(chalk.cyan(` doswiftly template upload${manifest?.version ? '' : ' --version 1.0.0'}\n`));
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Upload a built worker bundle to R2 storage.
|
|
1114
|
+
* The bundle can then be deployed to Cloudflare Workers from the Admin Panel.
|
|
1115
|
+
*
|
|
1116
|
+
* Prerequisites:
|
|
1117
|
+
* - Run `doswiftly template build` first
|
|
1118
|
+
* - Template must be registered in the registry
|
|
1119
|
+
*/
|
|
1120
|
+
export async function templateUploadCommand(options = {}) {
|
|
1121
|
+
requireSaasDeveloperRole();
|
|
1122
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Upload Template Bundle\n'));
|
|
1123
|
+
// Read manifest for template name
|
|
1124
|
+
const manifest = readTemplateManifest();
|
|
1125
|
+
const templateName = manifest?.name;
|
|
1126
|
+
if (!templateName) {
|
|
1127
|
+
console.log(chalk.red(' Error: No doswiftly-template.json found or missing "name" field.'));
|
|
1128
|
+
console.log(chalk.gray(' Run this from a template project root.\n'));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
// Determine version
|
|
1132
|
+
const packageVersion = readPackageVersion();
|
|
1133
|
+
const manifestVersion = manifest?.version;
|
|
1134
|
+
const version = options.version || packageVersion || manifestVersion;
|
|
1135
|
+
if (!version) {
|
|
1136
|
+
console.log(chalk.red(' Error: No version found.'));
|
|
1137
|
+
console.log(chalk.gray(' Set "version" in package.json or use --version flag\n'));
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
// Validate version format
|
|
1141
|
+
const versionRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
1142
|
+
if (!versionRegex.test(version)) {
|
|
1143
|
+
console.log(chalk.red(' Error: Version must be semver format (e.g., 1.0.0 or 1.0.0-beta.1)\n'));
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
// Find worker bundle
|
|
1147
|
+
const workerPath = options.workerPath || '.open-next/worker.js';
|
|
1148
|
+
if (!existsSync(workerPath)) {
|
|
1149
|
+
console.log(chalk.red(` Error: Worker bundle not found at ${workerPath}`));
|
|
1150
|
+
console.log(chalk.gray(' Build first with: doswiftly template build\n'));
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
const workerBuffer = readFileSync(workerPath);
|
|
1154
|
+
const sizeKB = (workerBuffer.length / 1024).toFixed(1);
|
|
1155
|
+
console.log(chalk.gray(` Template: ${chalk.cyan(templateName)}`));
|
|
1156
|
+
console.log(chalk.gray(` Version: ${chalk.cyan(version)}`));
|
|
1157
|
+
console.log(chalk.gray(` Bundle: ${workerPath} (${sizeKB} KB)`));
|
|
1158
|
+
console.log('');
|
|
1159
|
+
// Upload to API
|
|
1160
|
+
const spinner = ora('Uploading bundle to R2...').start();
|
|
1161
|
+
try {
|
|
1162
|
+
// Use FormData for multipart upload
|
|
1163
|
+
const FormData = (await import('form-data')).default;
|
|
1164
|
+
const formData = new FormData();
|
|
1165
|
+
formData.append('bundle', workerBuffer, {
|
|
1166
|
+
filename: 'worker.js',
|
|
1167
|
+
contentType: 'application/javascript',
|
|
1168
|
+
});
|
|
1169
|
+
formData.append('version', version);
|
|
1170
|
+
// Get OpenNext version from manifest if available
|
|
1171
|
+
try {
|
|
1172
|
+
const openNextManifestPath = '.open-next/open-next.output.json';
|
|
1173
|
+
if (existsSync(openNextManifestPath)) {
|
|
1174
|
+
const openNextManifest = JSON.parse(readFileSync(openNextManifestPath, 'utf-8'));
|
|
1175
|
+
if (openNextManifest.version) {
|
|
1176
|
+
formData.append('openNextVersion', openNextManifest.version);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
// Non-critical
|
|
1182
|
+
}
|
|
1183
|
+
// Get Node.js version
|
|
1184
|
+
formData.append('nodeVersion', process.version);
|
|
1185
|
+
// Make API request with multipart form data
|
|
1186
|
+
const client = createSharedApiClient({ requireShopSlug: false });
|
|
1187
|
+
const response = await client.request({
|
|
1188
|
+
url: `/cli/templates/${encodeURIComponent(templateName)}/publish`,
|
|
1189
|
+
method: 'POST',
|
|
1190
|
+
data: formData,
|
|
1191
|
+
headers: formData.getHeaders(),
|
|
1192
|
+
maxContentLength: Infinity,
|
|
1193
|
+
maxBodyLength: Infinity,
|
|
1194
|
+
});
|
|
1195
|
+
spinner.succeed('Bundle uploaded successfully');
|
|
1196
|
+
const result = response.data;
|
|
1197
|
+
console.log(chalk.gray(`\n Bundle key: ${result.bundleKey}`));
|
|
1198
|
+
console.log(chalk.gray(` Size: ${(result.bundleSize / 1024).toFixed(1)} KB`));
|
|
1199
|
+
console.log(chalk.gray(` Uploaded: ${new Date(result.uploadedAt).toLocaleString()}`));
|
|
1200
|
+
console.log(chalk.green('\n ✓ Bundle ready for deployment!'));
|
|
1201
|
+
console.log(chalk.gray(' Deploy from Admin Panel: Template Registry → Deploy to Cloudflare\n'));
|
|
1202
|
+
}
|
|
1203
|
+
catch (error) {
|
|
1204
|
+
spinner.fail('Failed to upload bundle');
|
|
1205
|
+
// Extract meaningful error message
|
|
1206
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
1207
|
+
const axiosError = error;
|
|
1208
|
+
const serverMessage = axiosError.response?.data?.message;
|
|
1209
|
+
if (serverMessage) {
|
|
1210
|
+
console.log(chalk.red(`\n ${serverMessage}\n`));
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
console.log(chalk.red(`\n ${axiosError.message || 'Unknown error'}\n`));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
console.log(chalk.red(`\n ${error.message}\n`));
|
|
1218
|
+
}
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Get information about the uploaded bundle and deployment status.
|
|
1224
|
+
*/
|
|
1225
|
+
export async function templateBundleInfoCommand(nameOrId) {
|
|
1226
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Template Bundle Info\n'));
|
|
1227
|
+
// Auto-detect from manifest if not provided
|
|
1228
|
+
const manifest = readTemplateManifest();
|
|
1229
|
+
const resolvedName = nameOrId || manifest?.name || null;
|
|
1230
|
+
if (!resolvedName) {
|
|
1231
|
+
console.log(chalk.red(' Error: Template name required.'));
|
|
1232
|
+
console.log(chalk.gray(' Provide name as argument or run from a directory with doswiftly-template.json\n'));
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
const spinner = ora(`Fetching bundle info for '${resolvedName}'...`).start();
|
|
1236
|
+
try {
|
|
1237
|
+
const info = await apiRequest(`/cli/templates/${encodeURIComponent(resolvedName)}/bundle`);
|
|
1238
|
+
spinner.stop();
|
|
1239
|
+
console.log(` ${chalk.bold('Template:')} ${chalk.cyan(resolvedName)}`);
|
|
1240
|
+
console.log('');
|
|
1241
|
+
if (!info.hasBundleInR2) {
|
|
1242
|
+
console.log(chalk.yellow(' No bundle uploaded yet.'));
|
|
1243
|
+
console.log(chalk.gray('\n Build and upload with:'));
|
|
1244
|
+
console.log(chalk.cyan(' doswiftly template build'));
|
|
1245
|
+
console.log(chalk.cyan(' doswiftly template upload\n'));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
console.log(chalk.bold(' R2 Bundle:'));
|
|
1249
|
+
console.log(` Version: ${info.bundleVersion}`);
|
|
1250
|
+
console.log(` Size: ${info.bundleSize ? (info.bundleSize / 1024).toFixed(1) + ' KB' : '-'}`);
|
|
1251
|
+
console.log(` Uploaded: ${info.bundleUploadedAt ? new Date(info.bundleUploadedAt).toLocaleString() : '-'}`);
|
|
1252
|
+
console.log('');
|
|
1253
|
+
console.log(chalk.bold(' Cloudflare Workers:'));
|
|
1254
|
+
if (info.isDeployedToCloudflare) {
|
|
1255
|
+
console.log(chalk.green(` Status: Deployed`));
|
|
1256
|
+
console.log(` Version: ${info.deployedVersion}`);
|
|
1257
|
+
if (info.needsRedeploy) {
|
|
1258
|
+
console.log(chalk.yellow(` Note: Bundle updated, redeploy needed`));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
console.log(chalk.yellow(` Status: Not deployed`));
|
|
1263
|
+
console.log(chalk.gray(' Deploy from Admin Panel: Template Registry → Deploy to Cloudflare'));
|
|
1264
|
+
}
|
|
1265
|
+
console.log('');
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
spinner.fail('Failed to fetch bundle info');
|
|
1269
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Setup or retry Cloudflare Workers Builds for a template.
|
|
1275
|
+
* Connects the template's GitHub repo to CF Builds for auto-deploy on push.
|
|
1276
|
+
*/
|
|
1277
|
+
export async function templateSetupBuildsCommand(nameOrId) {
|
|
1278
|
+
requireSaasDeveloperRole();
|
|
1279
|
+
console.log(chalk.bold.blue('\nDoSwiftly CLI - Setup CF Workers Builds\n'));
|
|
1280
|
+
// Auto-detect from manifest if not provided
|
|
1281
|
+
const manifest = readTemplateManifest();
|
|
1282
|
+
const resolvedName = nameOrId || manifest?.name || null;
|
|
1283
|
+
if (!resolvedName) {
|
|
1284
|
+
console.log(chalk.red(' Error: Template name required.'));
|
|
1285
|
+
console.log(chalk.gray(' Provide name as argument or run from a directory with doswiftly-template.json\n'));
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
}
|
|
1288
|
+
const spinner = ora(`Setting up CF Builds for '${resolvedName}'...`).start();
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await apiRequest(`/cli/templates/${encodeURIComponent(resolvedName)}/setup-builds`, { method: 'POST' });
|
|
1291
|
+
if (result.success) {
|
|
1292
|
+
spinner.succeed('CF Workers Builds configured!');
|
|
1293
|
+
console.log(chalk.gray(`\n Worker: ${result.workerName}`));
|
|
1294
|
+
console.log(chalk.gray(' Auto-deploy: pushes to main/master trigger build + deploy'));
|
|
1295
|
+
console.log(chalk.gray('\n Changes will be auto-deployed on push. No manual build/upload needed.\n'));
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
spinner.fail('CF Builds setup failed');
|
|
1299
|
+
console.log(chalk.red(`\n ${result.error || 'Unknown error'}\n`));
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
spinner.fail('Failed to setup CF Builds');
|
|
1305
|
+
console.log(chalk.red(`\n${error.message}\n`));
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
//# sourceMappingURL=template.js.map
|