vibecarbon 0.1.3 → 0.1.5
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/carbon/package.json
CHANGED
|
@@ -50,41 +50,41 @@
|
|
|
50
50
|
"stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@base-ui/react": "^1.
|
|
53
|
+
"@base-ui/react": "^1.5.0",
|
|
54
54
|
"@fontsource-variable/noto-sans": "^5.2.10",
|
|
55
55
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
56
|
-
"@hono/node-server": "^
|
|
57
|
-
"@hono/zod-openapi": "^1.
|
|
58
|
-
"@hookform/resolvers": "^5.
|
|
56
|
+
"@hono/node-server": "^2.0.4",
|
|
57
|
+
"@hono/zod-openapi": "^1.4.0",
|
|
58
|
+
"@hookform/resolvers": "^5.4.0",
|
|
59
59
|
"@mdx-js/react": "^3.1.1",
|
|
60
60
|
"@mdx-js/rollup": "^3.1.1",
|
|
61
|
-
"@scalar/api-reference-react": "^0.9.
|
|
61
|
+
"@scalar/api-reference-react": "^0.9.41",
|
|
62
62
|
"@supabase/ssr": "^0.9.0",
|
|
63
|
-
"@supabase/supabase-js": "^2.
|
|
63
|
+
"@supabase/supabase-js": "^2.106.2",
|
|
64
64
|
"@tanstack/react-query": "^5.95.2",
|
|
65
65
|
"class-variance-authority": "^0.7.1",
|
|
66
66
|
"clsx": "^2.1.1",
|
|
67
67
|
"cmdk": "^1.1.1",
|
|
68
68
|
"date-fns": "^4.1.0",
|
|
69
69
|
"embla-carousel-react": "^8.6.0",
|
|
70
|
-
"framer-motion": "^12.
|
|
71
|
-
"hono": "^4.12.
|
|
70
|
+
"framer-motion": "^12.40.0",
|
|
71
|
+
"hono": "^4.12.23",
|
|
72
72
|
"i18next": "^25.10.9",
|
|
73
73
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
74
74
|
"input-otp": "^1.4.2",
|
|
75
75
|
"ioredis": "^5.10.1",
|
|
76
|
-
"lenis": "^1.3.
|
|
76
|
+
"lenis": "^1.3.23",
|
|
77
77
|
"lucide-react": "^1.7.0",
|
|
78
78
|
"next-themes": "^0.4.6",
|
|
79
|
-
"nodemailer": "^8.0.
|
|
79
|
+
"nodemailer": "^8.0.8",
|
|
80
80
|
"pino": "^10.3.1",
|
|
81
|
-
"react": "^19.2.
|
|
81
|
+
"react": "^19.2.6",
|
|
82
82
|
"react-day-picker": "^9.14.0",
|
|
83
|
-
"react-dom": "^19.2.
|
|
83
|
+
"react-dom": "^19.2.6",
|
|
84
84
|
"react-hook-form": "^7.72.0",
|
|
85
85
|
"react-i18next": "^16.6.6",
|
|
86
|
-
"react-resizable-panels": "^4.
|
|
87
|
-
"react-router-dom": "^7.
|
|
86
|
+
"react-resizable-panels": "^4.11.2",
|
|
87
|
+
"react-router-dom": "^7.15.1",
|
|
88
88
|
"recharts": "^3.8.1",
|
|
89
89
|
"rehype-autolink-headings": "^7.1.0",
|
|
90
90
|
"rehype-slug": "^6.0.0",
|
|
@@ -113,7 +113,8 @@
|
|
|
113
113
|
"ip-address@<10.1.1": ">=10.1.1",
|
|
114
114
|
"fast-uri@<3.1.2": ">=3.1.2",
|
|
115
115
|
"protobufjs@<7.5.6": "^7.5.6",
|
|
116
|
-
"@protobufjs/utf8@<1.1.1": "^1.1.1"
|
|
116
|
+
"@protobufjs/utf8@<1.1.1": "^1.1.1",
|
|
117
|
+
"ws@>=8.0.0 <8.20.1": ">=8.20.1"
|
|
117
118
|
},
|
|
118
119
|
"peerDependencyRules": {
|
|
119
120
|
"allowedVersions": {
|
|
@@ -122,33 +123,33 @@
|
|
|
122
123
|
}
|
|
123
124
|
},
|
|
124
125
|
"devDependencies": {
|
|
125
|
-
"@biomejs/biome": "^2.4.
|
|
126
|
+
"@biomejs/biome": "^2.4.15",
|
|
126
127
|
"@hono/swagger-ui": "^0.6.1",
|
|
127
|
-
"@scalar/hono-api-reference": "^0.10.
|
|
128
|
+
"@scalar/hono-api-reference": "^0.10.19",
|
|
128
129
|
"@tailwindcss/typography": "^0.5.19",
|
|
129
|
-
"@tailwindcss/vite": "^4.
|
|
130
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
130
131
|
"@testing-library/jest-dom": "^6.6.3",
|
|
131
132
|
"@testing-library/react": "^16.3.0",
|
|
132
133
|
"@testing-library/user-event": "^14.6.1",
|
|
133
|
-
"@types/node": "^25.
|
|
134
|
-
"@types/nodemailer": "^
|
|
134
|
+
"@types/node": "^25.9.1",
|
|
135
|
+
"@types/nodemailer": "^8.0.0",
|
|
135
136
|
"@types/pg": "^8.20.0",
|
|
136
|
-
"@types/react": "^19.2.
|
|
137
|
+
"@types/react": "^19.2.15",
|
|
137
138
|
"@types/react-dom": "^19.2.3",
|
|
138
|
-
"@vitejs/plugin-react": "^6.0.
|
|
139
|
-
"@vitest/coverage-v8": "^4.1.
|
|
139
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
140
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
140
141
|
"concurrently": "^9.2.1",
|
|
141
|
-
"esbuild": "^0.
|
|
142
|
+
"esbuild": "^0.28.0",
|
|
142
143
|
"jsdom": "^26.1.0",
|
|
143
|
-
"pg": "^8.
|
|
144
|
+
"pg": "^8.21.0",
|
|
144
145
|
"pino-pretty": "^13.1.3",
|
|
145
146
|
"remark-gfm": "^4.0.1",
|
|
146
|
-
"shadcn": "^4.1
|
|
147
|
-
"tailwindcss": "^4.
|
|
148
|
-
"tsx": "^4.
|
|
147
|
+
"shadcn": "^4.8.1",
|
|
148
|
+
"tailwindcss": "^4.3.0",
|
|
149
|
+
"tsx": "^4.22.3",
|
|
149
150
|
"tw-animate-css": "^1.4.0",
|
|
150
|
-
"typescript": "^6.0.
|
|
151
|
-
"vite": "^8.0.
|
|
152
|
-
"vitest": "^4.1.
|
|
151
|
+
"typescript": "^6.0.3",
|
|
152
|
+
"vite": "^8.0.14",
|
|
153
|
+
"vitest": "^4.1.7"
|
|
153
154
|
}
|
|
154
155
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecarbon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Create and manage production-ready Vibecarbon applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./src/cli.js",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@pulumi/pulumi": "^3.231.0",
|
|
62
62
|
"bcryptjs": "^3.0.3",
|
|
63
63
|
"undici": "^8.1.0",
|
|
64
|
-
"which": "^
|
|
64
|
+
"which": "^7.0.0"
|
|
65
65
|
},
|
|
66
66
|
"pnpm": {
|
|
67
67
|
"onlyBuiltDependencies": [
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"better-sqlite3": "^12.8.0",
|
|
88
88
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
89
89
|
"msw": "^2.12.14",
|
|
90
|
-
"semantic-release": "^
|
|
90
|
+
"semantic-release": "^25.0.3",
|
|
91
91
|
"tsx": "^4.21.0",
|
|
92
|
-
"vite": "^
|
|
92
|
+
"vite": "^8.0.14",
|
|
93
93
|
"vitest": "^4.1.1",
|
|
94
94
|
"zod": "^4.3.6"
|
|
95
95
|
}
|
package/src/lib/deploy/bundle.js
CHANGED
|
@@ -207,6 +207,12 @@ export function renderBundle(projectName, options = {}) {
|
|
|
207
207
|
copyDirIfExist('volumes/db', 'volumes/db');
|
|
208
208
|
copyDirIfExist('supabase/migrations', 'supabase/migrations');
|
|
209
209
|
copyDirIfExist('volumes/kong', 'volumes/kong');
|
|
210
|
+
// Edge functions: docker-compose.prod.yml bind-mounts ./functions and starts
|
|
211
|
+
// the edge-runtime with `--main-service /home/deno/functions/main`. If this
|
|
212
|
+
// isn't bundled, the mount auto-creates an empty dir on the server and the
|
|
213
|
+
// runtime crash-loops with "could not find an appropriate entrypoint". The
|
|
214
|
+
// template ships a stub at functions/main/index.ts. RCA: prod-1 2026-05-26.
|
|
215
|
+
copyDirIfExist('functions', 'functions');
|
|
210
216
|
|
|
211
217
|
// volumes/traefik/middlewares.yml
|
|
212
218
|
const traefikMiddlewares = join(cwd, 'volumes', 'traefik', 'middlewares.yml');
|
|
@@ -734,17 +734,40 @@ export async function pullComposeImages(ip, sshKeyPath, projectName, options = {
|
|
|
734
734
|
export async function runMigrations(ip, sshKeyPath, projectName) {
|
|
735
735
|
const remoteDir = `/opt/${projectName}`;
|
|
736
736
|
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
737
|
+
// On a fresh volume, supabase/postgres runs its own first-boot initdb scripts
|
|
738
|
+
// (creating the supabase_admin role + auth/storage/realtime schemas) before
|
|
739
|
+
// our app migrations can apply. `pg_isready` flips true as soon as PG accepts
|
|
740
|
+
// connections — which can be *during* that init, before supabase_admin
|
|
741
|
+
// exists. Running migrations then errors (missing role/extension). Previously
|
|
742
|
+
// those errors were piped to /dev/null with `|| true`, so a fully-failed
|
|
743
|
+
// migration was indistinguishable from success and an empty schema shipped to
|
|
744
|
+
// prod (RCA prod-1 2026-05-26: 0 public tables, every DB feature 500'd).
|
|
745
|
+
//
|
|
746
|
+
// Gate on supabase_admin actually being able to run a query (not just
|
|
747
|
+
// pg_isready), polling up to ~3 min.
|
|
742
748
|
await sshRunAsync(
|
|
743
749
|
ip,
|
|
744
750
|
sshKeyPath,
|
|
745
|
-
`cd ${remoteDir} &&
|
|
751
|
+
`cd ${remoteDir} && ` +
|
|
752
|
+
`for i in $(seq 1 60); do ` +
|
|
753
|
+
`docker compose exec -T db psql -U supabase_admin -d postgres -c 'SELECT 1' >/dev/null 2>&1 && exit 0; ` +
|
|
754
|
+
`echo "[migrate] waiting for supabase_admin to accept queries (attempt $i/60)"; sleep 3; ` +
|
|
755
|
+
`done; echo "[migrate] supabase_admin never became ready" >&2; exit 1`,
|
|
756
|
+
{ timeout: 240_000 },
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
// Apply each migration with ON_ERROR_STOP=1 and abort on the first failure.
|
|
760
|
+
// A failed migration MUST fail the deploy — silently shipping an empty schema
|
|
761
|
+
// is far worse than a deploy the operator can see failed and retry. Errors
|
|
762
|
+
// propagate (no 2>/dev/null, no `|| true`); sshRunAsync throws on non-zero.
|
|
763
|
+
await sshRunAsync(
|
|
764
|
+
ip,
|
|
765
|
+
sshKeyPath,
|
|
766
|
+
`cd ${remoteDir} && ` +
|
|
746
767
|
`for f in $(ls supabase/migrations/ 2>/dev/null | sort); do ` +
|
|
747
|
-
`
|
|
768
|
+
`echo "[migrate] applying $f"; ` +
|
|
769
|
+
`cat "supabase/migrations/$f" | docker compose exec -T db psql -U supabase_admin -d postgres -v ON_ERROR_STOP=1 || ` +
|
|
770
|
+
`{ echo "[migrate] FAILED applying $f" >&2; exit 1; }; ` +
|
|
748
771
|
`done`,
|
|
749
772
|
{ timeout: 300_000 },
|
|
750
773
|
);
|
|
@@ -809,20 +832,28 @@ export async function deployCompose(options) {
|
|
|
809
832
|
onProgress('Starting services...');
|
|
810
833
|
await startComposeStack(ip, sshKeyPath, projectName, serviceOpts);
|
|
811
834
|
|
|
812
|
-
// Step 5: Run migrations
|
|
835
|
+
// Step 5: Run migrations. A failure here means an empty/partial schema, which
|
|
836
|
+
// makes every DB-backed feature 500 — so let it abort the deploy rather than
|
|
837
|
+
// swallowing it (RCA prod-1 2026-05-26: the previous blanket try/catch hid a
|
|
838
|
+
// fully-failed migration and shipped a schema-less prod that reported success).
|
|
813
839
|
onProgress('Running database migrations...');
|
|
814
840
|
const remoteDir = `/opt/${projectName}`;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
841
|
+
await runMigrations(ip, sshKeyPath, projectName);
|
|
842
|
+
// Reload PostgREST schema cache so it sees newly created tables. The old
|
|
843
|
+
// `docker compose exec rest kill -s SIGUSR1 1` never worked — the
|
|
844
|
+
// postgrest/postgrest image has no `kill` binary ("exec: kill: executable
|
|
845
|
+
// file not found in $PATH"), so the reload silently no-op'd and fresh tables
|
|
846
|
+
// stayed invisible (PGRST205) until rest happened to restart. RCA prod-1
|
|
847
|
+
// 2026-05-26. Use the canonical SQL NOTIFY on the pgrst channel instead
|
|
848
|
+
// (db-channel-enabled defaults on); fall back to restarting rest if NOTIFY
|
|
849
|
+
// doesn't land. Best-effort: a missed reload self-heals on the next deploy.
|
|
850
|
+
await sshRunAsync(
|
|
851
|
+
ip,
|
|
852
|
+
sshKeyPath,
|
|
853
|
+
`cd ${remoteDir} && ` +
|
|
854
|
+
`docker compose exec -T db psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'" 2>/dev/null || ` +
|
|
855
|
+
`docker compose restart rest 2>/dev/null || true`,
|
|
856
|
+
);
|
|
826
857
|
|
|
827
858
|
return {
|
|
828
859
|
success: true,
|
|
@@ -355,16 +355,28 @@ export class HetznerS3Provider {
|
|
|
355
355
|
* @param {string} bucketName - Bucket name
|
|
356
356
|
*/
|
|
357
357
|
async deleteBucket(bucketName) {
|
|
358
|
-
|
|
358
|
+
// Hetzner's S3 returns BucketNotEmpty for some time after the last object
|
|
359
|
+
// deletion finishes propagating, even when GET/LIST already report the
|
|
360
|
+
// bucket as empty. Observed in prod-1 destroy 2026-05-26: cleanup ran,
|
|
361
|
+
// all subsequent listings returned 0 items, but DeleteBucket still raised
|
|
362
|
+
// BucketNotEmpty for >30s. Manual `aws s3api delete-bucket` minutes later
|
|
363
|
+
// succeeded immediately. Backoff schedule below totals ~90s wall time
|
|
364
|
+
// across 7 attempts — covers the consistency lag we've seen without
|
|
365
|
+
// dragging out the destroy stage on the happy path.
|
|
366
|
+
const delays = [2000, 5000, 10000, 15000, 20000, 25000, 30000];
|
|
367
|
+
let lastErr;
|
|
368
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
359
369
|
try {
|
|
360
370
|
await this._send(new DeleteBucketCommand({ Bucket: bucketName }));
|
|
361
371
|
return;
|
|
362
372
|
} catch (err) {
|
|
373
|
+
lastErr = err;
|
|
363
374
|
const isNotEmpty = err.Code === 'BucketNotEmpty' || err.message?.includes('not empty');
|
|
364
|
-
if (!isNotEmpty || attempt ===
|
|
365
|
-
await new Promise((r) => setTimeout(r,
|
|
375
|
+
if (!isNotEmpty || attempt === delays.length) throw err;
|
|
376
|
+
await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
366
377
|
}
|
|
367
378
|
}
|
|
379
|
+
throw lastErr;
|
|
368
380
|
}
|
|
369
381
|
|
|
370
382
|
/**
|
|
@@ -546,7 +558,15 @@ export class HetznerS3Provider {
|
|
|
546
558
|
// multipart API absent — covered above
|
|
547
559
|
}
|
|
548
560
|
|
|
549
|
-
|
|
561
|
+
// If all three listings succeeded and returned zero items, the bucket
|
|
562
|
+
// *appears* empty but DeleteBucket still raised BucketNotEmpty. Almost
|
|
563
|
+
// always Hetzner consistency lag — the upstream caller has already
|
|
564
|
+
// exhausted its retry budget by this point, so tell the operator how to
|
|
565
|
+
// recover instead of the generic "unable to list" message which sounds
|
|
566
|
+
// like the diagnostic itself broke.
|
|
567
|
+
return parts.length > 0
|
|
568
|
+
? parts.join(', ')
|
|
569
|
+
: 'bucket appears empty in all listings — likely Hetzner S3 eventual-consistency lag. Wait ~60s, then retry the destroy or delete the bucket manually with `aws s3api delete-bucket --bucket <name> --endpoint-url <endpoint>`.';
|
|
550
570
|
}
|
|
551
571
|
|
|
552
572
|
/**
|