@pylonsync/create-pylon 0.3.274 → 0.3.275
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/bin/create-pylon.js +80 -0
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +339 -0
- package/templates/agency/.env.example +12 -0
- package/templates/agency/AGENTS.md +61 -0
- package/templates/agency/README.md +90 -0
- package/templates/agency/app/auth-form.tsx +129 -0
- package/templates/agency/app/contact-form.tsx +258 -0
- package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
- package/templates/agency/app/dashboard/page.tsx +70 -0
- package/templates/agency/app/error.tsx +26 -0
- package/templates/agency/app/globals.css +148 -0
- package/templates/agency/app/layout.tsx +174 -0
- package/templates/agency/app/login/page.tsx +39 -0
- package/templates/agency/app/not-found.tsx +19 -0
- package/templates/agency/app/page.tsx +207 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app.ts +135 -0
- package/templates/agency/components/marketing.tsx +148 -0
- package/templates/agency/components/section-scroller.tsx +35 -0
- package/templates/agency/components/ui/button.tsx +56 -0
- package/templates/agency/components/ui/card.tsx +90 -0
- package/templates/agency/components.json +20 -0
- package/templates/agency/functions/bookInquiry.ts +42 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +27 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +239 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +34 -0
- package/templates/agency/tsconfig.json +18 -0
- package/templates/ai-chat/.env.example +33 -0
- package/templates/ai-chat/AGENTS.md +61 -0
- package/templates/ai-chat/README.md +99 -0
- package/templates/ai-chat/app/auth-form.tsx +124 -0
- package/templates/ai-chat/app/chat-client.tsx +414 -0
- package/templates/ai-chat/app/error.tsx +26 -0
- package/templates/ai-chat/app/globals.css +148 -0
- package/templates/ai-chat/app/layout.tsx +75 -0
- package/templates/ai-chat/app/login/page.tsx +39 -0
- package/templates/ai-chat/app/not-found.tsx +19 -0
- package/templates/ai-chat/app/page.tsx +23 -0
- package/templates/ai-chat/app.ts +121 -0
- package/templates/ai-chat/components.json +20 -0
- package/templates/ai-chat/gitignore +10 -0
- package/templates/ai-chat/lib/site.config.ts +103 -0
- package/templates/ai-chat/lib/utils.ts +10 -0
- package/templates/ai-chat/package.json +34 -0
- package/templates/ai-chat/tsconfig.json +18 -0
- package/templates/ai-studio/.env.example +19 -0
- package/templates/ai-studio/AGENTS.md +61 -0
- package/templates/ai-studio/README.md +83 -0
- package/templates/ai-studio/app/auth-form.tsx +124 -0
- package/templates/ai-studio/app/error.tsx +26 -0
- package/templates/ai-studio/app/globals.css +148 -0
- package/templates/ai-studio/app/layout.tsx +75 -0
- package/templates/ai-studio/app/login/page.tsx +39 -0
- package/templates/ai-studio/app/not-found.tsx +19 -0
- package/templates/ai-studio/app/page.tsx +34 -0
- package/templates/ai-studio/app/studio-client.tsx +214 -0
- package/templates/ai-studio/app.ts +108 -0
- package/templates/ai-studio/components.json +20 -0
- package/templates/ai-studio/functions/_getGeneration.ts +25 -0
- package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
- package/templates/ai-studio/functions/generate.ts +42 -0
- package/templates/ai-studio/functions/pollGeneration.ts +134 -0
- package/templates/ai-studio/gitignore +10 -0
- package/templates/ai-studio/lib/site.config.ts +80 -0
- package/templates/ai-studio/lib/studio.ts +52 -0
- package/templates/ai-studio/lib/utils.ts +10 -0
- package/templates/ai-studio/package.json +34 -0
- package/templates/ai-studio/tsconfig.json +18 -0
- package/templates/creator/.env.example +12 -0
- package/templates/creator/AGENTS.md +61 -0
- package/templates/creator/README.md +67 -0
- package/templates/creator/app/auth-form.tsx +129 -0
- package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/creator/app/dashboard/page.tsx +70 -0
- package/templates/creator/app/error.tsx +26 -0
- package/templates/creator/app/globals.css +148 -0
- package/templates/creator/app/layout.tsx +160 -0
- package/templates/creator/app/login/page.tsx +39 -0
- package/templates/creator/app/newsletter-signup.tsx +162 -0
- package/templates/creator/app/not-found.tsx +19 -0
- package/templates/creator/app/page.tsx +160 -0
- package/templates/creator/app/robots.ts +12 -0
- package/templates/creator/app/sitemap.ts +9 -0
- package/templates/creator/app.ts +134 -0
- package/templates/creator/components/marketing.tsx +148 -0
- package/templates/creator/components/section-scroller.tsx +35 -0
- package/templates/creator/components/ui/button.tsx +56 -0
- package/templates/creator/components/ui/card.tsx +90 -0
- package/templates/creator/components.json +20 -0
- package/templates/creator/functions/subscribe.ts +82 -0
- package/templates/creator/functions/subscriberStats.ts +75 -0
- package/templates/creator/gitignore +10 -0
- package/templates/creator/lib/owner.ts +26 -0
- package/templates/creator/lib/site.config.ts +173 -0
- package/templates/creator/lib/stats.ts +30 -0
- package/templates/creator/lib/utils.ts +10 -0
- package/templates/creator/package.json +34 -0
- package/templates/creator/tsconfig.json +18 -0
- package/templates/default/app/layout.tsx +26 -27
- package/templates/default/app/page.tsx +90 -274
- package/templates/default/lib/products.ts +9 -122
- package/templates/default/lib/site.config.ts +739 -0
- package/templates/default/lib/site.ts +14 -261
- package/templates/directory/.env.example +12 -0
- package/templates/directory/AGENTS.md +61 -0
- package/templates/directory/README.md +80 -0
- package/templates/directory/app/auth-form.tsx +129 -0
- package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
- package/templates/directory/app/dashboard/page.tsx +70 -0
- package/templates/directory/app/directory-browse.tsx +328 -0
- package/templates/directory/app/error.tsx +26 -0
- package/templates/directory/app/globals.css +148 -0
- package/templates/directory/app/layout.tsx +171 -0
- package/templates/directory/app/login/page.tsx +39 -0
- package/templates/directory/app/not-found.tsx +19 -0
- package/templates/directory/app/page.tsx +50 -0
- package/templates/directory/app/robots.ts +12 -0
- package/templates/directory/app/sitemap.ts +9 -0
- package/templates/directory/app/submit/page.tsx +30 -0
- package/templates/directory/app/submit-form.tsx +151 -0
- package/templates/directory/app.ts +146 -0
- package/templates/directory/components/marketing.tsx +148 -0
- package/templates/directory/components/section-scroller.tsx +35 -0
- package/templates/directory/components/ui/button.tsx +56 -0
- package/templates/directory/components/ui/card.tsx +90 -0
- package/templates/directory/components.json +20 -0
- package/templates/directory/functions/approveSubmission.ts +45 -0
- package/templates/directory/functions/rejectSubmission.ts +20 -0
- package/templates/directory/functions/seedListings.ts +33 -0
- package/templates/directory/functions/submissionsForOwner.ts +29 -0
- package/templates/directory/functions/submitListing.ts +63 -0
- package/templates/directory/functions/upvote.ts +24 -0
- package/templates/directory/gitignore +10 -0
- package/templates/directory/lib/directory.ts +45 -0
- package/templates/directory/lib/owner.ts +26 -0
- package/templates/directory/lib/site.config.ts +130 -0
- package/templates/directory/lib/utils.ts +10 -0
- package/templates/directory/package.json +34 -0
- package/templates/directory/tsconfig.json +18 -0
- package/templates/local-service/.env.example +12 -0
- package/templates/local-service/AGENTS.md +61 -0
- package/templates/local-service/README.md +82 -0
- package/templates/local-service/app/auth-form.tsx +129 -0
- package/templates/local-service/app/booking-widget.tsx +399 -0
- package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
- package/templates/local-service/app/dashboard/page.tsx +63 -0
- package/templates/local-service/app/error.tsx +26 -0
- package/templates/local-service/app/globals.css +148 -0
- package/templates/local-service/app/layout.tsx +151 -0
- package/templates/local-service/app/login/page.tsx +39 -0
- package/templates/local-service/app/not-found.tsx +19 -0
- package/templates/local-service/app/page.tsx +233 -0
- package/templates/local-service/app/robots.ts +12 -0
- package/templates/local-service/app/sitemap.ts +9 -0
- package/templates/local-service/app.ts +131 -0
- package/templates/local-service/components/marketing.tsx +162 -0
- package/templates/local-service/components/section-scroller.tsx +35 -0
- package/templates/local-service/components/ui/button.tsx +56 -0
- package/templates/local-service/components/ui/card.tsx +90 -0
- package/templates/local-service/components.json +20 -0
- package/templates/local-service/functions/bookingsForOwner.ts +30 -0
- package/templates/local-service/functions/cancelBooking.ts +27 -0
- package/templates/local-service/functions/confirmBooking.ts +18 -0
- package/templates/local-service/functions/createBooking.ts +98 -0
- package/templates/local-service/gitignore +10 -0
- package/templates/local-service/lib/booking.ts +24 -0
- package/templates/local-service/lib/owner.ts +26 -0
- package/templates/local-service/lib/site.config.ts +232 -0
- package/templates/local-service/lib/slots.ts +97 -0
- package/templates/local-service/lib/utils.ts +10 -0
- package/templates/local-service/package.json +34 -0
- package/templates/local-service/tsconfig.json +18 -0
- package/templates/marketplace/.env.example +9 -0
- package/templates/marketplace/AGENTS.md +61 -0
- package/templates/marketplace/README.md +78 -0
- package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
- package/templates/marketplace/app/error.tsx +26 -0
- package/templates/marketplace/app/globals.css +64 -0
- package/templates/marketplace/app/layout.tsx +60 -0
- package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
- package/templates/marketplace/app/me/page.tsx +15 -0
- package/templates/marketplace/app/not-found.tsx +20 -0
- package/templates/marketplace/app/page.tsx +159 -0
- package/templates/marketplace/app/robots.ts +12 -0
- package/templates/marketplace/app/sell/page.tsx +26 -0
- package/templates/marketplace/app/sitemap.ts +14 -0
- package/templates/marketplace/app.ts +190 -0
- package/templates/marketplace/client/AuthNav.tsx +46 -0
- package/templates/marketplace/client/LiveTicker.tsx +104 -0
- package/templates/marketplace/client/LoginCard.tsx +130 -0
- package/templates/marketplace/client/MarketProvider.tsx +148 -0
- package/templates/marketplace/client/MyMarket.tsx +180 -0
- package/templates/marketplace/client/OfferPanel.tsx +355 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
- package/templates/marketplace/client/SellForm.tsx +160 -0
- package/templates/marketplace/client/WatchButton.tsx +88 -0
- package/templates/marketplace/client/market.ts +341 -0
- package/templates/marketplace/functions/buyNow.ts +78 -0
- package/templates/marketplace/functions/makeOffer.ts +65 -0
- package/templates/marketplace/functions/respondToOffer.ts +62 -0
- package/templates/marketplace/functions/seedMarket.ts +90 -0
- package/templates/marketplace/gitignore +10 -0
- package/templates/marketplace/package.json +35 -0
- package/templates/marketplace/tsconfig.json +14 -0
- package/templates/marketplace/ui/badge.tsx +30 -0
- package/templates/marketplace/ui/button.tsx +49 -0
- package/templates/marketplace/ui/card.tsx +48 -0
- package/templates/marketplace/ui/input.tsx +17 -0
- package/templates/marketplace/ui/label.tsx +18 -0
- package/templates/marketplace/ui/textarea.tsx +17 -0
- package/templates/marketplace/ui/tokens.css +32 -0
- package/templates/marketplace/ui/utils.ts +6 -0
- package/templates/restaurant/.env.example +12 -0
- package/templates/restaurant/AGENTS.md +61 -0
- package/templates/restaurant/README.md +77 -0
- package/templates/restaurant/app/auth-form.tsx +129 -0
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
- package/templates/restaurant/app/dashboard/page.tsx +59 -0
- package/templates/restaurant/app/error.tsx +26 -0
- package/templates/restaurant/app/globals.css +148 -0
- package/templates/restaurant/app/layout.tsx +151 -0
- package/templates/restaurant/app/login/page.tsx +39 -0
- package/templates/restaurant/app/not-found.tsx +19 -0
- package/templates/restaurant/app/page.tsx +194 -0
- package/templates/restaurant/app/reservation-widget.tsx +359 -0
- package/templates/restaurant/app/robots.ts +12 -0
- package/templates/restaurant/app/sitemap.ts +9 -0
- package/templates/restaurant/app.ts +115 -0
- package/templates/restaurant/components/marketing.tsx +162 -0
- package/templates/restaurant/components/section-scroller.tsx +35 -0
- package/templates/restaurant/components/ui/button.tsx +56 -0
- package/templates/restaurant/components/ui/card.tsx +90 -0
- package/templates/restaurant/components.json +20 -0
- package/templates/restaurant/functions/cancelReservation.ts +26 -0
- package/templates/restaurant/functions/confirmReservation.ts +17 -0
- package/templates/restaurant/functions/createReservation.ts +92 -0
- package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
- package/templates/restaurant/gitignore +10 -0
- package/templates/restaurant/lib/owner.ts +26 -0
- package/templates/restaurant/lib/reservation.ts +22 -0
- package/templates/restaurant/lib/site.config.ts +218 -0
- package/templates/restaurant/lib/slots.ts +55 -0
- package/templates/restaurant/lib/utils.ts +10 -0
- package/templates/restaurant/package.json +34 -0
- package/templates/restaurant/tsconfig.json +18 -0
- package/templates/shop/.env.example +32 -0
- package/templates/shop/AGENTS.md +61 -0
- package/templates/shop/README.md +102 -0
- package/templates/shop/app/auth-form.tsx +129 -0
- package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
- package/templates/shop/app/dashboard/page.tsx +59 -0
- package/templates/shop/app/error.tsx +26 -0
- package/templates/shop/app/globals.css +148 -0
- package/templates/shop/app/layout.tsx +160 -0
- package/templates/shop/app/login/page.tsx +39 -0
- package/templates/shop/app/not-found.tsx +19 -0
- package/templates/shop/app/page.tsx +95 -0
- package/templates/shop/app/robots.ts +12 -0
- package/templates/shop/app/shop-client.tsx +436 -0
- package/templates/shop/app/sitemap.ts +9 -0
- package/templates/shop/app/success/page.tsx +33 -0
- package/templates/shop/app.ts +134 -0
- package/templates/shop/components/marketing.tsx +96 -0
- package/templates/shop/components/section-scroller.tsx +35 -0
- package/templates/shop/components/ui/button.tsx +56 -0
- package/templates/shop/components/ui/card.tsx +90 -0
- package/templates/shop/components.json +20 -0
- package/templates/shop/functions/cancelOrder.ts +33 -0
- package/templates/shop/functions/checkout.ts +130 -0
- package/templates/shop/functions/fulfillOrder.ts +17 -0
- package/templates/shop/functions/markGroupPaid.ts +26 -0
- package/templates/shop/functions/ordersForOwner.ts +28 -0
- package/templates/shop/functions/releaseGroup.ts +36 -0
- package/templates/shop/functions/reserveCart.ts +87 -0
- package/templates/shop/functions/restockProduct.ts +23 -0
- package/templates/shop/functions/seedProducts.ts +30 -0
- package/templates/shop/functions/stripeWebhook.ts +72 -0
- package/templates/shop/gitignore +10 -0
- package/templates/shop/lib/owner.ts +26 -0
- package/templates/shop/lib/shop.ts +45 -0
- package/templates/shop/lib/site.config.ts +198 -0
- package/templates/shop/lib/utils.ts +10 -0
- package/templates/shop/package.json +35 -0
- package/templates/shop/tsconfig.json +18 -0
- package/templates/waitlist/.env.example +12 -0
- package/templates/waitlist/AGENTS.md +61 -0
- package/templates/waitlist/README.md +81 -0
- package/templates/waitlist/app/auth-form.tsx +129 -0
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/waitlist/app/dashboard/page.tsx +70 -0
- package/templates/waitlist/app/error.tsx +26 -0
- package/templates/waitlist/app/globals.css +148 -0
- package/templates/waitlist/app/layout.tsx +158 -0
- package/templates/waitlist/app/login/page.tsx +39 -0
- package/templates/waitlist/app/not-found.tsx +19 -0
- package/templates/waitlist/app/page.tsx +119 -0
- package/templates/waitlist/app/robots.ts +12 -0
- package/templates/waitlist/app/sitemap.ts +9 -0
- package/templates/waitlist/app/waitlist-hero.tsx +219 -0
- package/templates/waitlist/app.ts +134 -0
- package/templates/waitlist/components/marketing.tsx +96 -0
- package/templates/waitlist/components/ui/button.tsx +56 -0
- package/templates/waitlist/components/ui/card.tsx +90 -0
- package/templates/waitlist/components.json +20 -0
- package/templates/waitlist/functions/joinWaitlist.ts +82 -0
- package/templates/waitlist/functions/waitlistStats.ts +75 -0
- package/templates/waitlist/gitignore +10 -0
- package/templates/waitlist/lib/owner.ts +26 -0
- package/templates/waitlist/lib/site.config.ts +178 -0
- package/templates/waitlist/lib/stats.ts +30 -0
- package/templates/waitlist/lib/utils.ts +10 -0
- package/templates/waitlist/package.json +34 -0
- package/templates/waitlist/tsconfig.json +18 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
15
|
+
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"tailwindcss": "^4.3.0",
|
|
19
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
20
|
+
"tw-animate-css": "^1.2.0",
|
|
21
|
+
"class-variance-authority": "^0.7.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"tailwind-merge": "^2.5.0",
|
|
24
|
+
"lucide-react": "^0.460.0",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2022", "DOM"],
|
|
11
|
+
"types": ["react", "react-dom", "node"],
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*", "functions/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
|
|
2
|
+
#
|
|
3
|
+
# The app boots with no config — you can sign in and create chats — but the
|
|
4
|
+
# assistant can't reply until you point it at an LLM provider. Streaming runs
|
|
5
|
+
# through the built-in POST /api/ai/stream endpoint, so your API key stays on
|
|
6
|
+
# the server and never reaches the browser.
|
|
7
|
+
|
|
8
|
+
# ── AI provider (required for the assistant to reply) ────────────────────────
|
|
9
|
+
# PYLON_AI_PROVIDER "anthropic" | "openai" | "custom"
|
|
10
|
+
# PYLON_AI_API_KEY your provider key (sk-… / sk-ant-…)
|
|
11
|
+
# PYLON_AI_MODEL the default model used when none is selected
|
|
12
|
+
# PYLON_AI_PROVIDER=anthropic
|
|
13
|
+
# PYLON_AI_API_KEY=sk-ant-...
|
|
14
|
+
# PYLON_AI_MODEL=claude-sonnet-4-6
|
|
15
|
+
|
|
16
|
+
# ── Model switching ──────────────────────────────────────────────────────────
|
|
17
|
+
# The chat's model picker (lib/site.config.ts → chat.models) sends a `model` per
|
|
18
|
+
# request. For security, client-chosen models must be allow-listed here
|
|
19
|
+
# (comma-separated) — otherwise the override is refused and the chat shows a
|
|
20
|
+
# notice. List exactly the model ids you expose in the picker:
|
|
21
|
+
# PYLON_AI_MODELS_ALLOWED=claude-sonnet-4-6,claude-opus-4-8,claude-haiku-4-5
|
|
22
|
+
|
|
23
|
+
# ── Multiple PROVIDERS in one picker (optional) ──────────────────────────────
|
|
24
|
+
# /api/ai/stream talks to ONE provider. To offer Anthropic + OpenAI + others in
|
|
25
|
+
# the same picker, route through an OpenAI-compatible gateway like OpenRouter:
|
|
26
|
+
# PYLON_AI_PROVIDER=custom
|
|
27
|
+
# PYLON_AI_BASE_URL=https://openrouter.ai/api/v1
|
|
28
|
+
# PYLON_AI_API_KEY=<openrouter key>
|
|
29
|
+
# then set chat.models (config) + PYLON_AI_MODELS_ALLOWED to that gateway's slugs
|
|
30
|
+
# (e.g. anthropic/claude-sonnet-4, openai/gpt-4o, google/gemini-2.5-pro).
|
|
31
|
+
|
|
32
|
+
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
33
|
+
# SITE_URL=https://yourchat.com
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# AGENTS.md — working in a Pylon project
|
|
2
|
+
|
|
3
|
+
Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like framework for realtime apps: you declare entities, policies, and server functions in TypeScript, and a single Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR — one process, one port. The full API reference is at **/llms-full.txt** (served at `/llms-full.txt`; in the repo at `apps/web/public/llms-full.txt`). Read it before guessing an API name.
|
|
4
|
+
|
|
5
|
+
## Directory conventions
|
|
6
|
+
|
|
7
|
+
**Unified SSR app:**
|
|
8
|
+
- `app.ts` — data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
+
- `app/` — file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
|
+
- `app/globals.css` — Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
|
+
- `functions/` — server functions, one per file, `default`-exported.
|
|
12
|
+
- `.pylon/` — local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
13
|
+
|
|
14
|
+
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated — do not hand-edit.
|
|
15
|
+
|
|
16
|
+
## The core authoring loop
|
|
17
|
+
|
|
18
|
+
1. **Define an entity** — `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update — use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
|
|
19
|
+
2. **Write a policy** — `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
|
|
20
|
+
3. **Author a function** in `functions/<name>.ts` — `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
|
|
21
|
+
4. **Read it on the client** — `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
|
|
22
|
+
|
|
23
|
+
## Key gotchas
|
|
24
|
+
|
|
25
|
+
- **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
|
|
26
|
+
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
|
|
27
|
+
- **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
|
|
28
|
+
- **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
|
|
29
|
+
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
|
|
30
|
+
- **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
31
|
+
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
32
|
+
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
|
|
33
|
+
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` — `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
|
+
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
|
+
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready — status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
+
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` — use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
|
+
- **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
|
|
39
|
+
|
|
40
|
+
## Use the CLI — don't guess
|
|
41
|
+
|
|
42
|
+
| Need | Command |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Run the app (SSR + API, hot reload, one port `:4321`) | `pylon dev` (or `npm run dev`) |
|
|
45
|
+
| Regenerate manifest + typed client | `pylon codegen` (Swift client: `pylon codegen client --target swift`) |
|
|
46
|
+
| Validate / diff / push schema | `pylon schema check` \| `diff` \| `push` |
|
|
47
|
+
| Migrations | `pylon migrate create <name>` \| `plan` \| `apply` |
|
|
48
|
+
| Lint policies (PYL001–PYL004) | `pylon lint --strict` |
|
|
49
|
+
| Tests | `pylon test` |
|
|
50
|
+
| Adversarial security probe | `pylon test:security` |
|
|
51
|
+
| Inspect cloud request logs (agent-safe) | `pylon logs --json --limit 50` |
|
|
52
|
+
| Inspect data / entities | `pylon data entities` \| `pylon data list <Entity>` |
|
|
53
|
+
| Call a function | `pylon fn <name> key=value` |
|
|
54
|
+
| Health snapshot | `pylon status` |
|
|
55
|
+
| Build for prod | `pylon build` |
|
|
56
|
+
| Deploy (Pylon Cloud by default) | `pylon deploy` |
|
|
57
|
+
| Look up an error code | `pylon explain <CODE>` |
|
|
58
|
+
|
|
59
|
+
`--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
|
|
60
|
+
|
|
61
|
+
For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **/llms-full.txt**.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A streaming **AI chat** app built with [Pylon](https://pylonsync.com) — token
|
|
4
|
+
streaming, multi-conversation history, and realtime cross-tab sync, all from one
|
|
5
|
+
binary on one port. No Next.js, no separate API server.
|
|
6
|
+
|
|
7
|
+
Tokens stream from the built-in `POST /api/ai/stream` endpoint, so your provider
|
|
8
|
+
API key never reaches the browser. Conversations are sync-backed and
|
|
9
|
+
owner-scoped — open two tabs and a chat you send in one shows up in the other.
|
|
10
|
+
|
|
11
|
+
## Develop
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
__RUN_DEV__
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open http://localhost:4321. You can create chats right away; to get **replies**,
|
|
18
|
+
point it at an LLM provider (see below). Then **open a second tab** — your
|
|
19
|
+
conversations and messages stay in lockstep.
|
|
20
|
+
|
|
21
|
+
## Configure the model
|
|
22
|
+
|
|
23
|
+
The assistant replies only once you set a provider (the app boots fine without
|
|
24
|
+
it and shows a friendly notice):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# .env
|
|
28
|
+
PYLON_AI_PROVIDER=anthropic # or "openai" / "custom"
|
|
29
|
+
PYLON_AI_API_KEY=sk-ant-...
|
|
30
|
+
PYLON_AI_MODEL=claude-sonnet-4-6 # default when none is picked
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`/api/ai/stream` is auth-gated (it uses your signed-in session) and rate-limited
|
|
34
|
+
per user, so a drive-by caller can't burn your budget.
|
|
35
|
+
|
|
36
|
+
### Switching models / providers
|
|
37
|
+
|
|
38
|
+
The composer has a **model picker** — its options live in `lib/site.config.ts`
|
|
39
|
+
(`chat.models`, each with a provider label), and the chosen id is sent per
|
|
40
|
+
request. Client-chosen models must be allow-listed server-side:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PYLON_AI_MODELS_ALLOWED=claude-sonnet-4-6,claude-opus-4-8,gpt-4o
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`/api/ai/stream` talks to **one** provider. To offer models from **multiple
|
|
47
|
+
providers** in one picker, route through an OpenAI-compatible gateway like
|
|
48
|
+
[OpenRouter](https://openrouter.ai):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
PYLON_AI_PROVIDER=custom
|
|
52
|
+
PYLON_AI_BASE_URL=https://openrouter.ai/api/v1
|
|
53
|
+
PYLON_AI_API_KEY=<openrouter key>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
then set `chat.models` + `PYLON_AI_MODELS_ALLOWED` to the gateway's slugs
|
|
57
|
+
(`anthropic/claude-sonnet-4`, `openai/gpt-4o`, `google/gemini-2.5-pro`, …).
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
- **Streaming.** `app/chat-client.tsx` POSTs the conversation to
|
|
62
|
+
`/api/ai/stream` and reads the SSE response (`data: {choices:[{delta:…}]}` …
|
|
63
|
+
`data: [DONE]`), appending tokens to the live assistant bubble.
|
|
64
|
+
- **Realtime history.** `Conversation` + `Message` are owner-scoped entities
|
|
65
|
+
read with `db.useQuery` — private to each user and synced across their tabs.
|
|
66
|
+
Messages are written with optimistic `db.insert` (userId is stamped from the
|
|
67
|
+
session via `field.owner()`), so no custom write functions are needed.
|
|
68
|
+
- **Sign-in required.** Chats are tied to your account, so the home page
|
|
69
|
+
redirects unauthenticated visitors to `/login`. Once signed in, your history
|
|
70
|
+
follows you across tabs and devices.
|
|
71
|
+
|
|
72
|
+
## Privacy
|
|
73
|
+
|
|
74
|
+
`Conversation` and `Message` policies are owner-scoped (`auth.userId ==
|
|
75
|
+
data.userId`) — you can only ever read or write your own. `User.passwordHash` is
|
|
76
|
+
`serverOnly`. The provider key lives only on the server.
|
|
77
|
+
|
|
78
|
+
## Rebrand it
|
|
79
|
+
|
|
80
|
+
Everything brand-specific — name, colors, the assistant's system prompt, the
|
|
81
|
+
empty-state copy, and starter prompts — lives in **`lib/site.config.ts`**.
|
|
82
|
+
|
|
83
|
+
## Layout
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
app.ts Conversation + Message (owner-scoped) + User
|
|
87
|
+
lib/site.config.ts brand + system prompt + suggestions (edit this)
|
|
88
|
+
app/page.tsx renders the chat island
|
|
89
|
+
app/chat-client.tsx sidebar + thread + streaming via /api/ai/stream
|
|
90
|
+
app/login/ optional sign-in (carries history across devices)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Deploy
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pylon deploy
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Docs: https://docs.pylonsync.com
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
passwordLogin,
|
|
6
|
+
passwordRegister,
|
|
7
|
+
persistSession,
|
|
8
|
+
ApiError,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
|
|
11
|
+
// The email/password form — one form, two modes. It calls the built-in auth API
|
|
12
|
+
// directly (`passwordLogin` / `passwordRegister` POST to `/api/auth/password/*`),
|
|
13
|
+
// then `persistSession` writes the freshly-minted token to local storage so the
|
|
14
|
+
// sync engine authenticates as the real account on the next load. We then do a
|
|
15
|
+
// full navigation to "/" so the SSR runtime re-resolves auth from the HttpOnly
|
|
16
|
+
// cookie and renders the chat.
|
|
17
|
+
//
|
|
18
|
+
// Sign-in is REQUIRED to use the chat: conversations are owner-scoped to your
|
|
19
|
+
// account, so the home page redirects unauthenticated visitors here.
|
|
20
|
+
export function AuthForm() {
|
|
21
|
+
const [mode, setMode] = useState<"login" | "register">("login");
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [password, setPassword] = useState("");
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [pending, setPending] = useState(false);
|
|
26
|
+
|
|
27
|
+
async function onSubmit(e: React.FormEvent) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
setError(null);
|
|
30
|
+
setPending(true);
|
|
31
|
+
try {
|
|
32
|
+
const session =
|
|
33
|
+
mode === "login"
|
|
34
|
+
? await passwordLogin({ email, password })
|
|
35
|
+
: await passwordRegister({ email, password });
|
|
36
|
+
// Make this session authoritative, replacing any anonymous guest token.
|
|
37
|
+
persistSession(session);
|
|
38
|
+
window.location.assign("/");
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setError(messageFor(err));
|
|
41
|
+
setPending(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-5">
|
|
47
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
48
|
+
<label className="block">
|
|
49
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
|
|
50
|
+
<input
|
|
51
|
+
type="email"
|
|
52
|
+
value={email}
|
|
53
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
54
|
+
required
|
|
55
|
+
autoComplete="email"
|
|
56
|
+
placeholder="you@yourbusiness.com"
|
|
57
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
<label className="block">
|
|
61
|
+
<span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
|
|
62
|
+
<input
|
|
63
|
+
type="password"
|
|
64
|
+
value={password}
|
|
65
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
66
|
+
required
|
|
67
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
68
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
69
|
+
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
|
|
70
|
+
/>
|
|
71
|
+
</label>
|
|
72
|
+
{error ? (
|
|
73
|
+
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
|
|
74
|
+
{error}
|
|
75
|
+
</p>
|
|
76
|
+
) : null}
|
|
77
|
+
<button
|
|
78
|
+
type="submit"
|
|
79
|
+
disabled={pending}
|
|
80
|
+
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
|
|
81
|
+
>
|
|
82
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
83
|
+
</button>
|
|
84
|
+
</form>
|
|
85
|
+
|
|
86
|
+
<p className="text-center text-[13px] text-zinc-500">
|
|
87
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => {
|
|
91
|
+
setMode(mode === "login" ? "register" : "login");
|
|
92
|
+
setError(null);
|
|
93
|
+
}}
|
|
94
|
+
className="font-medium text-zinc-900 underline underline-offset-2"
|
|
95
|
+
>
|
|
96
|
+
{mode === "login" ? "Create an account" : "Sign in"}
|
|
97
|
+
</button>
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Map the framework's auth error codes to friendly copy. `ApiError` carries a
|
|
104
|
+
// stable `.code` so you branch on the code, not the message.
|
|
105
|
+
function messageFor(err: unknown): string {
|
|
106
|
+
if (err instanceof ApiError) {
|
|
107
|
+
switch (err.code) {
|
|
108
|
+
case "INVALID_CREDENTIALS":
|
|
109
|
+
return "Wrong email or password.";
|
|
110
|
+
case "USER_EXISTS":
|
|
111
|
+
return "That email is already registered — sign in instead.";
|
|
112
|
+
case "WEAK_PASSWORD":
|
|
113
|
+
return "Pick a longer password — at least 10 characters.";
|
|
114
|
+
case "PWNED_PASSWORD":
|
|
115
|
+
return "That password has appeared in a known data breach. Choose a different one.";
|
|
116
|
+
case "RATE_LIMITED":
|
|
117
|
+
return "Too many attempts — try again in a minute.";
|
|
118
|
+
default:
|
|
119
|
+
return err.message;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (err instanceof Error) return err.message;
|
|
123
|
+
return "Something went wrong. Try again.";
|
|
124
|
+
}
|