vibecarbon 0.5.0 → 0.6.0
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 +1 -1
- package/carbon/.env.example +3 -0
- package/carbon/.github/workflows/deploy.yml +24 -21
- package/carbon/.github/workflows/vibecarbon-build.yml +14 -0
- package/carbon/Dockerfile +3 -0
- package/carbon/biome.json +1 -1
- package/carbon/cloud-init/k3s/master-init.sh +26 -2
- package/carbon/cloud-init/k3s/supabase-init.sh +22 -2
- package/carbon/cloud-init/k3s/worker-init.sh +22 -2
- package/carbon/content/docs/cli.mdx +1 -1
- package/carbon/docker-compose.yml +6 -0
- package/carbon/k8s/base/app/deployment.yaml +5 -0
- package/carbon/k8s/values/supabase.values.yaml +30 -0
- package/carbon/package.json +34 -36
- package/carbon/scripts/generate-sitemap.ts +18 -3
- package/carbon/src/client/App.tsx +2 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +4 -3
- package/carbon/src/client/components/PricingSection.tsx +44 -1
- package/carbon/src/client/components/SEO.tsx +6 -3
- package/carbon/src/client/components/ui/calendar.tsx +1 -1
- package/carbon/src/client/index.css +9 -1
- package/carbon/src/client/index.html +3 -3
- package/carbon/src/client/locales/en.json +1 -1
- package/carbon/src/client/pages/Pricing.tsx +169 -0
- package/carbon/src/client/pages/settings/Billing.tsx +2 -6
- package/carbon/src/server/index.ts +23 -4
- package/carbon/src/shared/billing-catalog.ts +25 -0
- package/carbon/src/shared/billing-catalog.types.ts +35 -0
- package/carbon/src/shared/pricing.ts +56 -1
- package/package.json +13 -13
- package/src/activate.js +1 -1
- package/src/backup.js +33 -87
- package/src/configure.js +365 -137
- package/src/create.js +5 -2
- package/src/deploy.js +24 -7
- package/src/destroy.js +11 -4
- package/src/failover.js +64 -47
- package/src/lib/backup-format.js +84 -0
- package/src/lib/billing/stripe-catalog.js +152 -0
- package/src/lib/billing/write-catalog.js +123 -0
- package/src/lib/ci-setup.js +11 -1
- package/src/lib/config-registry.js +116 -0
- package/src/lib/deploy/compose/build-args.js +6 -0
- package/src/lib/deploy/compose/ha.js +25 -25
- package/src/lib/deploy/compose/index.js +29 -18
- package/src/lib/deploy/k8s/gitops-deploy.js +38 -14
- package/src/lib/deploy/k8s/k3s.js +20 -10
- package/src/lib/deploy/orchestrator.js +22 -1
- package/src/lib/deploy/prompts.js +21 -2
- package/src/lib/deploy/utils.js +56 -32
- package/src/lib/github-environments.js +29 -0
- package/src/lib/licensing/index.js +8 -3
- package/src/lib/licensing/tiers.js +0 -3
- package/src/lib/pod-backups.js +3 -3
- package/src/lib/providers/base.js +1 -1
- package/src/lib/providers/hetzner.js +74 -82
- package/src/lib/server-types.js +3 -31
- package/src/lib/ssh.js +19 -18
- package/src/lib/walg-backups.js +81 -0
- package/src/restore.js +122 -202
- package/src/scale.js +25 -29
- package/src/status.js +0 -69
- package/src/lib/cost.js +0 -103
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
<meta property="og:type" content="website" />
|
|
14
14
|
<meta property="og:title" content="%VITE_PROJECT_NAME%" />
|
|
15
15
|
<meta property="og:description" content="Build, launch, and scale your SaaS product with auth, billing, teams, email, blog, docs, i18n, and production infrastructure included." />
|
|
16
|
-
<meta property="og:url" content="
|
|
16
|
+
<meta property="og:url" content="%VITE_PUBLIC_URL%" />
|
|
17
17
|
<meta property="og:site_name" content="%VITE_PROJECT_NAME%" />
|
|
18
18
|
<!-- Replace with your own 1200x630 OG image for social sharing previews -->
|
|
19
|
-
<meta property="og:image" content="
|
|
19
|
+
<meta property="og:image" content="%VITE_PUBLIC_URL%/og-image.png" />
|
|
20
20
|
<meta property="og:image:width" content="1200" />
|
|
21
21
|
<meta property="og:image:height" content="630" />
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<meta name="twitter:card" content="summary_large_image" />
|
|
25
25
|
<meta name="twitter:title" content="%VITE_PROJECT_NAME%" />
|
|
26
26
|
<meta name="twitter:description" content="Build, launch, and scale your SaaS product with auth, billing, teams, email, blog, docs, i18n, and production infrastructure included." />
|
|
27
|
-
<meta name="twitter:image" content="
|
|
27
|
+
<meta name="twitter:image" content="%VITE_PUBLIC_URL%/og-image.png" />
|
|
28
28
|
|
|
29
29
|
<!-- RSS -->
|
|
30
30
|
<link rel="alternate" type="application/rss+xml" title="%VITE_PROJECT_NAME% Blog" href="/rss.xml" />
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CatalogTier,
|
|
3
|
+
catalogTiers,
|
|
4
|
+
formatPlanPrice,
|
|
5
|
+
formatTierPrice,
|
|
6
|
+
type Plan,
|
|
7
|
+
plans,
|
|
8
|
+
} from '@shared/pricing';
|
|
9
|
+
import { ArrowRight, Check, X } from 'lucide-react';
|
|
10
|
+
import { Link } from 'react-router-dom';
|
|
11
|
+
import { Wordmark } from '@/components/Logo';
|
|
12
|
+
import { Nav } from '@/components/Nav';
|
|
13
|
+
import { SEO } from '@/components/SEO';
|
|
14
|
+
import { Badge } from '@/components/ui/badge';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
16
|
+
import { cn } from '@/lib/utils';
|
|
17
|
+
|
|
18
|
+
interface PricingCardProps {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
price: string;
|
|
22
|
+
/** Feature rows. Catalog tiers are all included; demo plans mark exclusions. */
|
|
23
|
+
features: { text: string; included: boolean }[];
|
|
24
|
+
ctaLabel: string;
|
|
25
|
+
popular?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function PricingCard({
|
|
29
|
+
name,
|
|
30
|
+
description,
|
|
31
|
+
price,
|
|
32
|
+
features,
|
|
33
|
+
ctaLabel,
|
|
34
|
+
popular = false,
|
|
35
|
+
}: PricingCardProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
'relative flex flex-col rounded-2xl border p-6 transition-shadow',
|
|
40
|
+
popular ? 'border-primary shadow-lg shadow-primary/10' : 'border-border'
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{popular && (
|
|
44
|
+
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
45
|
+
<Badge>Most popular</Badge>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="mb-4">
|
|
50
|
+
<h2 className="text-lg font-semibold">{name}</h2>
|
|
51
|
+
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="mb-6">
|
|
55
|
+
<span className="text-4xl font-bold">{price}</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<ul className="mb-8 flex-1 space-y-3">
|
|
59
|
+
{features.map((feature) => (
|
|
60
|
+
<li key={feature.text} className="flex items-start gap-2.5 text-sm">
|
|
61
|
+
{feature.included ? (
|
|
62
|
+
<Check className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
63
|
+
) : (
|
|
64
|
+
<X className="mt-0.5 size-4 shrink-0 text-muted-foreground/50" />
|
|
65
|
+
)}
|
|
66
|
+
<span className={feature.included ? '' : 'text-muted-foreground/50'}>
|
|
67
|
+
{feature.text}
|
|
68
|
+
</span>
|
|
69
|
+
</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
|
|
73
|
+
<Button variant={popular ? 'default' : 'outline'} size="lg" asChild className="w-full">
|
|
74
|
+
<Link to="/signup" className="inline-flex items-center gap-2">
|
|
75
|
+
{ctaLabel}
|
|
76
|
+
<ArrowRight className="size-4 shrink-0" />
|
|
77
|
+
</Link>
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Cap the column count at the number of cards so 1–2 tiers don't stretch oddly. */
|
|
84
|
+
const gridColsByCount: Record<number, string> = {
|
|
85
|
+
1: 'sm:max-w-md sm:mx-auto',
|
|
86
|
+
2: 'sm:grid-cols-2 sm:max-w-3xl sm:mx-auto',
|
|
87
|
+
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
|
88
|
+
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default function Pricing() {
|
|
92
|
+
const useCatalog = catalogTiers.length > 0;
|
|
93
|
+
const count = useCatalog ? catalogTiers.length : plans.length;
|
|
94
|
+
const gridCols = gridColsByCount[Math.min(count, 4)] ?? 'sm:grid-cols-2 lg:grid-cols-4';
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="min-h-screen bg-background">
|
|
98
|
+
<Nav />
|
|
99
|
+
<SEO
|
|
100
|
+
title="Pricing"
|
|
101
|
+
description="Simple, transparent pricing. Start free and scale as you grow."
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<div className="mx-auto max-w-6xl px-6 pt-32 pb-24">
|
|
105
|
+
{/* Header */}
|
|
106
|
+
<div className="mx-auto mb-16 max-w-2xl text-center">
|
|
107
|
+
<h1 className="mb-4 text-4xl font-black tracking-tight md:text-5xl">
|
|
108
|
+
Simple, transparent pricing
|
|
109
|
+
</h1>
|
|
110
|
+
<p className="text-lg text-muted-foreground">
|
|
111
|
+
Start free and upgrade when you're ready to ship. No hidden fees.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Plan cards */}
|
|
116
|
+
<div className={cn('grid items-stretch gap-6', gridCols)}>
|
|
117
|
+
{useCatalog
|
|
118
|
+
? catalogTiers.map((tier: CatalogTier) => (
|
|
119
|
+
<PricingCard
|
|
120
|
+
key={tier.priceId}
|
|
121
|
+
name={tier.name}
|
|
122
|
+
description={tier.description}
|
|
123
|
+
price={formatTierPrice(tier)}
|
|
124
|
+
features={tier.features.map((text) => ({ text, included: true }))}
|
|
125
|
+
ctaLabel={tier.amount === 0 ? 'Get started' : `Choose ${tier.name}`}
|
|
126
|
+
/>
|
|
127
|
+
))
|
|
128
|
+
: plans.map((plan: Plan) => (
|
|
129
|
+
<PricingCard
|
|
130
|
+
key={plan.id}
|
|
131
|
+
name={plan.name}
|
|
132
|
+
description={plan.description}
|
|
133
|
+
price={formatPlanPrice(plan)}
|
|
134
|
+
features={plan.features}
|
|
135
|
+
ctaLabel={plan.id === 'free' ? 'Get started' : `Choose ${plan.name}`}
|
|
136
|
+
popular={plan.popular}
|
|
137
|
+
/>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<footer className="border-t border-border py-8">
|
|
143
|
+
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-4 px-6 sm:flex-row">
|
|
144
|
+
<Wordmark size="sm" />
|
|
145
|
+
<div className="flex items-center gap-6">
|
|
146
|
+
<Link
|
|
147
|
+
to="/"
|
|
148
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
149
|
+
>
|
|
150
|
+
Home
|
|
151
|
+
</Link>
|
|
152
|
+
<Link
|
|
153
|
+
to="/privacy"
|
|
154
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
155
|
+
>
|
|
156
|
+
Privacy
|
|
157
|
+
</Link>
|
|
158
|
+
<Link
|
|
159
|
+
to="/terms"
|
|
160
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
161
|
+
>
|
|
162
|
+
Terms
|
|
163
|
+
</Link>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</footer>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Plan, type PlanId, plans } from '@shared/pricing';
|
|
1
|
+
import { formatPlanPrice, type Plan, type PlanId, plans } from '@shared/pricing';
|
|
2
2
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
3
3
|
import { AlertCircle, Check, CreditCard, ExternalLink, Loader2, X } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -287,7 +287,6 @@ export default function Billing() {
|
|
|
287
287
|
{displayPlans.map((plan) => {
|
|
288
288
|
const isCurrentPlan = plan.id === currentPlanId;
|
|
289
289
|
const priceId = getPriceId(plan);
|
|
290
|
-
const price = plan.price.monthly;
|
|
291
290
|
|
|
292
291
|
return (
|
|
293
292
|
<div
|
|
@@ -306,10 +305,7 @@ export default function Billing() {
|
|
|
306
305
|
<p className="text-sm text-muted-foreground mt-1">{plan.description}</p>
|
|
307
306
|
</div>
|
|
308
307
|
<div className="mb-4">
|
|
309
|
-
<span className="text-3xl font-bold">
|
|
310
|
-
{price === 0 ? t('common.free') : `$${(price / 100).toFixed(0)}`}
|
|
311
|
-
</span>
|
|
312
|
-
{price > 0 && <span className="text-muted-foreground text-sm">/mo</span>}
|
|
308
|
+
<span className="text-3xl font-bold">{formatPlanPrice(plan)}</span>
|
|
313
309
|
</div>
|
|
314
310
|
<ul className="space-y-2 mb-6 flex-1">
|
|
315
311
|
{plan.features.map((feature) => (
|
|
@@ -506,11 +506,30 @@ if (process.env.NODE_ENV === 'production') {
|
|
|
506
506
|
})
|
|
507
507
|
);
|
|
508
508
|
|
|
509
|
-
// Serve static files from dist/client
|
|
510
|
-
|
|
509
|
+
// Serve static files from dist/client. HTML (index.html for `/`) must NOT be
|
|
510
|
+
// heuristically cached, or returning visitors keep loading a stale index that
|
|
511
|
+
// references old hash-busted asset URLs after a deploy. `no-cache` still allows
|
|
512
|
+
// 304 revalidation, so it's cheap — only the tiny HTML is re-checked.
|
|
513
|
+
app.use(
|
|
514
|
+
'/*',
|
|
515
|
+
serveStatic({
|
|
516
|
+
root: './dist/client',
|
|
517
|
+
onFound: (path, c) => {
|
|
518
|
+
if (path.endsWith('.html')) c.header('Cache-Control', 'no-cache');
|
|
519
|
+
},
|
|
520
|
+
})
|
|
521
|
+
);
|
|
511
522
|
|
|
512
|
-
// SPA fallback - serve index.html for client-side routing
|
|
513
|
-
app.get(
|
|
523
|
+
// SPA fallback - serve index.html for client-side routing (always revalidate)
|
|
524
|
+
app.get(
|
|
525
|
+
'/*',
|
|
526
|
+
serveStatic({
|
|
527
|
+
path: './dist/client/index.html',
|
|
528
|
+
onFound: (_path, c) => {
|
|
529
|
+
c.header('Cache-Control', 'no-cache');
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
);
|
|
514
533
|
} else {
|
|
515
534
|
// Development: redirect root to API docs (frontend served by Vite dev server)
|
|
516
535
|
app.get('/', (c) => c.redirect('/api/docs'));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing catalog snapshot — single source of truth for the operator's activated
|
|
3
|
+
* provider products (the dynamic, price-sorted tier list).
|
|
4
|
+
*
|
|
5
|
+
* GENERATED by `vibecarbon configure` (Payments → Stripe): it fetches the live
|
|
6
|
+
* products/prices, lets the operator pick which to activate, and writes them here
|
|
7
|
+
* so the public pricing surface renders accurate pricing WITHOUT calling the
|
|
8
|
+
* provider API on every page load. Re-run `vibecarbon configure` to refresh, then
|
|
9
|
+
* redeploy.
|
|
10
|
+
*
|
|
11
|
+
* Contains NO secrets — product names, amounts, and price IDs are all public.
|
|
12
|
+
* Ships empty; `pricing.ts` falls back to the generic free/starter/pro demo plans
|
|
13
|
+
* until this is populated.
|
|
14
|
+
*
|
|
15
|
+
* Types live in `billing-catalog.types.ts` (not regenerated). Do not hand-edit
|
|
16
|
+
* the data below — re-run `vibecarbon configure` instead.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { BillingCatalog } from './billing-catalog.types';
|
|
20
|
+
|
|
21
|
+
export const billingCatalog: BillingCatalog = {
|
|
22
|
+
provider: null,
|
|
23
|
+
generatedAt: null,
|
|
24
|
+
tiers: [],
|
|
25
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the billing-catalog snapshot. Kept separate from the
|
|
3
|
+
* generated data file (`billing-catalog.ts`) so `vibecarbon configure` can
|
|
4
|
+
* regenerate the data without touching these stable types.
|
|
5
|
+
*
|
|
6
|
+
* The catalog is a price-sorted list of the products the operator activated in
|
|
7
|
+
* their provider (via `vibecarbon configure` → Payments). Each activated product
|
|
8
|
+
* becomes a tier shown on the public pricing surface. The template's generic
|
|
9
|
+
* free/starter/pro demo (in `pricing.ts`) is separate and used when the catalog
|
|
10
|
+
* is empty.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface CatalogTier {
|
|
14
|
+
/** Provider price ID (e.g. Stripe `price_...`). */
|
|
15
|
+
priceId: string;
|
|
16
|
+
/** Product name — the tier's display name. */
|
|
17
|
+
name: string;
|
|
18
|
+
description: string | null;
|
|
19
|
+
/** Feature bullets, from the product's provider "marketing features". */
|
|
20
|
+
features: string[];
|
|
21
|
+
/** Amount in the currency's smallest unit (e.g. cents). 0 = free. */
|
|
22
|
+
amount: number;
|
|
23
|
+
/** ISO 4217 currency code, lowercase (e.g. `usd`). */
|
|
24
|
+
currency: string;
|
|
25
|
+
interval: 'month' | 'year' | 'one_time';
|
|
26
|
+
type: 'recurring' | 'one_time';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BillingCatalog {
|
|
30
|
+
provider: 'stripe' | 'paddle' | 'polar' | null;
|
|
31
|
+
/** ISO 8601 timestamp of the last `vibecarbon configure` snapshot. */
|
|
32
|
+
generatedAt: string | null;
|
|
33
|
+
/** Activated tiers, sorted by `amount` ascending. */
|
|
34
|
+
tiers: CatalogTier[];
|
|
35
|
+
}
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* Server-side: import { getStripePriceId } from '@/shared/pricing' (resolves env vars)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { billingCatalog } from './billing-catalog';
|
|
15
|
+
import type { CatalogTier } from './billing-catalog.types';
|
|
16
|
+
|
|
17
|
+
export type { CatalogTier };
|
|
18
|
+
|
|
14
19
|
export type PlanId = 'free' | 'starter' | 'pro';
|
|
15
20
|
|
|
16
21
|
export interface PlanFeature {
|
|
@@ -32,6 +37,12 @@ export interface Plan {
|
|
|
32
37
|
apiRequestsPerMinute: number;
|
|
33
38
|
};
|
|
34
39
|
popular?: boolean;
|
|
40
|
+
// Dynamic fields, populated from the billing-catalog snapshot when the
|
|
41
|
+
// provider has been configured. Undefined falls back to the hardcoded price.
|
|
42
|
+
priceId?: string;
|
|
43
|
+
currency?: string; // ISO 4217, lowercase (e.g. 'usd')
|
|
44
|
+
interval?: 'month' | 'year' | 'one_time';
|
|
45
|
+
type?: 'recurring' | 'one_time';
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -57,7 +68,7 @@ function buildFeatures(included: string[]): PlanFeature[] {
|
|
|
57
68
|
}));
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
const basePlans: Plan[] = [
|
|
61
72
|
{
|
|
62
73
|
id: 'free',
|
|
63
74
|
name: 'Free',
|
|
@@ -120,10 +131,54 @@ export const plans: Plan[] = [
|
|
|
120
131
|
},
|
|
121
132
|
];
|
|
122
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The template's generic SaaS demo plans (free/starter/pro). Used as the fallback
|
|
136
|
+
* on the pricing surface when no provider catalog has been configured.
|
|
137
|
+
*/
|
|
138
|
+
export const plans: Plan[] = basePlans;
|
|
139
|
+
|
|
123
140
|
export function getPlan(planId: PlanId): Plan | undefined {
|
|
124
141
|
return plans.find((p) => p.id === planId);
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The activated provider products (price-sorted), synced by `vibecarbon configure`
|
|
146
|
+
* → Payments. When non-empty, these drive the public pricing surface instead of
|
|
147
|
+
* the generic demo `plans`. Sorted defensively by amount in case the snapshot isn't.
|
|
148
|
+
*/
|
|
149
|
+
export const catalogTiers: CatalogTier[] = [...billingCatalog.tiers].sort(
|
|
150
|
+
(a, b) => a.amount - b.amount
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
function priceSuffix(interval: CatalogTier['interval'], type: CatalogTier['type']): string {
|
|
154
|
+
if (type === 'one_time' || interval === 'one_time') return '';
|
|
155
|
+
if (interval === 'year') return '/yr';
|
|
156
|
+
return '/mo';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatAmount(amount: number, currency: string): string {
|
|
160
|
+
const value = amount / 100;
|
|
161
|
+
return new Intl.NumberFormat('en-US', {
|
|
162
|
+
style: 'currency',
|
|
163
|
+
currency: (currency || 'usd').toUpperCase(),
|
|
164
|
+
minimumFractionDigits: 0,
|
|
165
|
+
maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
166
|
+
}).format(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Currency- and interval-aware price label for an activated catalog tier. */
|
|
170
|
+
export function formatTierPrice(tier: CatalogTier): string {
|
|
171
|
+
if (tier.amount === 0) return 'Free';
|
|
172
|
+
return `${formatAmount(tier.amount, tier.currency)}${priceSuffix(tier.interval, tier.type)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Currency- and interval-aware price label for a demo plan. */
|
|
176
|
+
export function formatPlanPrice(plan: Plan): string {
|
|
177
|
+
const cents = plan.price.monthly;
|
|
178
|
+
if (cents === 0) return 'Free';
|
|
179
|
+
return `${formatAmount(cents, plan.currency ?? 'usd')}${priceSuffix(plan.interval ?? 'month', plan.type ?? 'recurring')}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
127
182
|
export function formatPrice(cents: number): string {
|
|
128
183
|
if (cents === 0) return 'Free';
|
|
129
184
|
return `$${(cents / 100).toFixed(0)}/mo`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecarbon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Create and manage production-ready Vibecarbon applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./src/cli.js",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"url": "https://github.com/hyperformant/vibecarbon.git"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@aws-sdk/client-s3": "^3.
|
|
61
|
-
"@clack/prompts": "^1.
|
|
62
|
-
"@pulumi/hcloud": "^1.
|
|
63
|
-
"@pulumi/pulumi": "^3.
|
|
60
|
+
"@aws-sdk/client-s3": "^3.1058.0",
|
|
61
|
+
"@clack/prompts": "^1.5.0",
|
|
62
|
+
"@pulumi/hcloud": "^1.38.0",
|
|
63
|
+
"@pulumi/pulumi": "^3.244.0",
|
|
64
64
|
"bcryptjs": "^3.0.3",
|
|
65
|
-
"undici": "^8.
|
|
65
|
+
"undici": "^8.3.0",
|
|
66
66
|
"which": "^7.0.0"
|
|
67
67
|
},
|
|
68
68
|
"pnpm": {
|
|
@@ -81,18 +81,18 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@biomejs/biome": "^2.4.
|
|
84
|
+
"@biomejs/biome": "^2.4.16",
|
|
85
85
|
"@semantic-release/changelog": "^6.0.3",
|
|
86
86
|
"@semantic-release/git": "^10.0.1",
|
|
87
87
|
"@types/better-sqlite3": "^7.6.13",
|
|
88
|
-
"@vitest/coverage-v8": "^4.1.
|
|
88
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
89
89
|
"autocannon": "^8.0.0",
|
|
90
|
-
"better-sqlite3": "^12.
|
|
90
|
+
"better-sqlite3": "^12.10.0",
|
|
91
91
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
92
92
|
"semantic-release": "^25.0.3",
|
|
93
|
-
"tsx": "^4.
|
|
94
|
-
"vite": "^8.0.
|
|
95
|
-
"vitest": "^4.1.
|
|
96
|
-
"zod": "^4.3
|
|
93
|
+
"tsx": "^4.22.4",
|
|
94
|
+
"vite": "^8.0.16",
|
|
95
|
+
"vitest": "^4.1.8",
|
|
96
|
+
"zod": "^4.4.3"
|
|
97
97
|
}
|
|
98
98
|
}
|
package/src/activate.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function runActivate(args) {
|
|
|
111
111
|
spinner.stop('License validation failed');
|
|
112
112
|
p.log.error(c.error(`Error: ${result.error}`));
|
|
113
113
|
p.log.info('');
|
|
114
|
-
p.log.info(`${c.dim('Purchase a license at')} ${c.info('https://vibecarbon.
|
|
114
|
+
p.log.info(`${c.dim('Purchase a license at')} ${c.info('https://vibecarbon.com/#pricing')}`);
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
117
|
|