@selvajs/cli 4.6.20 → 4.6.21-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/cli",
3
- "version": "4.6.20",
3
+ "version": "4.6.21-beta.1",
4
4
  "description": "Scaffold and operate a Selva white-label deployment. `npx @selvajs/cli <dir>` to bootstrap, `selva <cmd>` to manage.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -1,7 +1,7 @@
1
1
  // Validate deployment without starting it: files, secrets, layout, providers,
2
2
  // boot persistence. Read-only; yellow warnings don't fail, red failures exit 1.
3
3
 
4
- import { existsSync, readFileSync, accessSync, constants, statSync } from 'node:fs';
4
+ import { existsSync, readFileSync, readdirSync, accessSync, constants, statSync } from 'node:fs';
5
5
  import { join, resolve, dirname, delimiter } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
@@ -57,6 +57,7 @@ export async function runDoctor() {
57
57
 
58
58
  if (used.has('supabase')) {
59
59
  checks.push(checkSupabase(env));
60
+ checks.push(checkSupabaseMigrations(dir, env));
60
61
  }
61
62
 
62
63
  if (providers.auth === 'header') {
@@ -170,6 +171,83 @@ async function checkSupabase(env) {
170
171
  }
171
172
  }
172
173
 
174
+ // App↔DB schema handshake (audit O3). Expected head = newest migration shipped
175
+ // by the installed @selvajs/supabase-provider; actual = what the database
176
+ // reports via the selva.migration_head() RPC. Red on skew — the app degrades
177
+ // /api/health to 503 at boot in the same state, so catching it here saves a
178
+ // confusing deploy.
179
+ async function checkSupabaseMigrations(dir, env) {
180
+ if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) {
181
+ return yellow('Migration-head check skipped — Supabase env incomplete');
182
+ }
183
+
184
+ const migrationsDir = join(
185
+ dir,
186
+ 'node_modules',
187
+ '@selvajs',
188
+ 'supabase-provider',
189
+ 'supabase',
190
+ 'migrations'
191
+ );
192
+ let expected = null;
193
+ try {
194
+ expected =
195
+ readdirSync(migrationsDir)
196
+ .filter((f) => /^\d{14}_.+\.sql$/.test(f))
197
+ .map((f) => f.slice(0, 14))
198
+ .sort()
199
+ .pop() ?? null;
200
+ } catch {
201
+ // Provider package not installed in this deployment.
202
+ }
203
+ if (!expected) {
204
+ return yellow(
205
+ '@selvajs/supabase-provider migrations not found — skipping migration-head check'
206
+ );
207
+ }
208
+
209
+ try {
210
+ const controller = new AbortController();
211
+ const timer = setTimeout(() => controller.abort(), 4000);
212
+ const res = await fetch(env.SUPABASE_URL + '/rest/v1/rpc/migration_head', {
213
+ method: 'POST',
214
+ headers: {
215
+ apikey: env.SUPABASE_SERVICE_ROLE_KEY,
216
+ Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
217
+ 'Content-Type': 'application/json',
218
+ // The function lives in the selva schema the app is pinned to.
219
+ 'Content-Profile': 'selva'
220
+ },
221
+ body: '{}',
222
+ signal: controller.signal
223
+ });
224
+ clearTimeout(timer);
225
+ if (res.status === 404) {
226
+ return red(
227
+ `Database is missing selva.migration_head() — migrations are pending (expected head ${expected}). Sync + run: npx supabase db push`
228
+ );
229
+ }
230
+ if (!res.ok) {
231
+ return yellow(`migration_head RPC responded ${res.status} — cannot verify schema head`);
232
+ }
233
+ const actual = String((await res.json()) ?? '');
234
+ if (!actual) {
235
+ return red(
236
+ `Database reports no applied migrations (expected head ${expected}). Sync + run: npx supabase db push`
237
+ );
238
+ }
239
+ if (actual < expected) {
240
+ return red(
241
+ `Database migration head ${actual} is behind the installed provider (${expected}). Sync + run: npx supabase db push`
242
+ );
243
+ }
244
+ return green(`Database migration head ${actual} matches the installed provider`);
245
+ } catch (err) {
246
+ // Soft-fail on network errors, same as checkSupabase.
247
+ return yellow(`Migration-head check skipped (${err.message ?? err})`);
248
+ }
249
+ }
250
+
173
251
  function checkPackage(dir, name) {
174
252
  const path = join(dir, 'node_modules', ...name.split('/'), 'package.json');
175
253
  if (!existsSync(path)) return red(`${name} not installed (run npm install)`);