spine-framework 0.3.72 → 0.3.74

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.
@@ -23,6 +23,7 @@ import { resolve, dirname, join, basename } from 'path'
23
23
  import { fileURLToPath } from 'url'
24
24
  import { execSync } from 'child_process'
25
25
  import { createClient } from '@supabase/supabase-js'
26
+ import { runMigrations } from './migrate.js'
26
27
 
27
28
  const __filename = fileURLToPath(import.meta.url)
28
29
  const __dirname = dirname(__filename)
@@ -85,12 +86,30 @@ export function registerInstallAppCommands(program: Command) {
85
86
  }
86
87
  console.log(` ✓ App files copied to custom/apps/${appSlug}/`)
87
88
 
88
- // 5. Seed types, roles, link-types into database
89
+ // 5. Run pending migrations before seeding so schema is up to date
90
+ console.log(' Running migrations...')
91
+ try {
92
+ const migrateResult = await runMigrations({ frameworkOnly: false })
93
+ if (migrateResult === null) {
94
+ console.log(' ○ Skipping migrations (SUPABASE_DB_PASSWORD not set)')
95
+ } else if (migrateResult.applied > 0) {
96
+ console.log(` ✓ Migrations: ${migrateResult.applied} applied, ${migrateResult.skipped} skipped`)
97
+ } else {
98
+ console.log(` ✓ Migrations: up to date (${migrateResult.skipped} already applied)`)
99
+ }
100
+ if (migrateResult && migrateResult.failed > 0) {
101
+ console.warn(` ⚠ ${migrateResult.failed} migration(s) failed — seed may fail`)
102
+ }
103
+ } catch (migrateErr: any) {
104
+ console.warn(` ⚠ Migration error: ${migrateErr.message} — continuing with seed`)
105
+ }
106
+
107
+ // 6. Seed types, roles, link-types into database
89
108
  if (!opts.seed === false) {
90
109
  await seedApp(appDest, appSlug)
91
110
  }
92
111
 
93
- // 6. Aggregate registration config from manifest into spine.config.json
112
+ // 7. Aggregate registration config from manifest into spine.config.json
94
113
  const manifestPath = join(appDest, 'manifest.json')
95
114
  let manifest: Record<string, any> = {}
96
115
  try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {}
@@ -35,62 +35,32 @@ interface MigrateOptions {
35
35
  frameworkOnly: boolean
36
36
  }
37
37
 
38
- async function migrateCommand(options: MigrateOptions): Promise<void> {
39
- console.log('\nRunning Spine Framework migrations...\n')
40
-
41
- // Load .env from project root
42
- loadEnv()
43
-
38
+ /**
39
+ * Run all pending migrations programmatically.
40
+ * Returns { applied, skipped, failed } — throws on connection failure.
41
+ * Silent on missing SUPABASE_DB_PASSWORD (returns null to indicate skipped).
42
+ */
43
+ export async function runMigrations(opts: { frameworkOnly?: boolean } = {}): Promise<{ applied: number; skipped: number; failed: number } | null> {
44
44
  const supabaseUrl = process.env.SUPABASE_URL
45
45
  if (!supabaseUrl || supabaseUrl.includes('placeholder') || supabaseUrl.includes('your-project')) {
46
- console.error('Error: SUPABASE_URL not set. Edit your .env file first.')
47
- console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/api')
48
- process.exit(1)
46
+ return null
49
47
  }
50
48
 
51
- const dbPassword = options.dbPassword || process.env.SUPABASE_DB_PASSWORD
49
+ const dbPassword = process.env.SUPABASE_DB_PASSWORD
52
50
  if (!dbPassword) {
53
- console.error('Error: SUPABASE_DB_PASSWORD not set.')
54
- console.error(' Add it to .env or pass --db-password <password>')
55
- console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/database')
56
- process.exit(1)
51
+ return null
57
52
  }
58
53
 
59
54
  const projectRef = supabaseUrl.replace('https://', '').split('.')[0]
60
55
  const connectionString = `postgresql://postgres:${dbPassword}@db.${projectRef}.supabase.co:5432/postgres`
61
56
 
62
- // Collect migration files
63
- const migrationFiles = collectMigrations(options.frameworkOnly)
64
-
65
- if (migrationFiles.length === 0) {
66
- console.log('No migration files found.')
67
- return
68
- }
69
-
70
- console.log(` Found ${migrationFiles.length} migration file(s)\n`)
71
-
72
- if (options.dryRun) {
73
- for (const file of migrationFiles) {
74
- console.log(` [dry-run] Would apply: ${file.name}`)
75
- }
76
- console.log('\nDry run complete — no changes made.\n')
77
- return
78
- }
57
+ const migrationFiles = collectMigrations(opts.frameworkOnly ?? false)
58
+ if (migrationFiles.length === 0) return { applied: 0, skipped: 0, failed: 0 }
79
59
 
80
- // Dynamically import pg to avoid loading it at module init time
81
60
  const { default: pg } = await import('pg')
82
61
  const client = new pg.Client({ connectionString })
62
+ await client.connect()
83
63
 
84
- try {
85
- await client.connect()
86
- console.log(' Connected to database\n')
87
- } catch (err: any) {
88
- console.error(`Error: Could not connect to database: ${err.message}`)
89
- console.error(' Check your SUPABASE_DB_PASSWORD and that the project is active.')
90
- process.exit(1)
91
- }
92
-
93
- // Create migration tracking table if it doesn't exist
94
64
  try {
95
65
  await client.query(`
96
66
  CREATE TABLE IF NOT EXISTS _spine_migrations (
@@ -99,40 +69,28 @@ async function migrateCommand(options: MigrateOptions): Promise<void> {
99
69
  applied_at TIMESTAMPTZ DEFAULT NOW()
100
70
  )
101
71
  `)
102
- } catch {
103
- // Non-fatal: proceed without tracking
104
- }
72
+ } catch { /* non-fatal */ }
105
73
 
106
- // Load already-applied migrations
107
74
  let appliedMigrations = new Set<string>()
108
75
  try {
109
76
  const { rows } = await client.query('SELECT filename FROM _spine_migrations')
110
77
  appliedMigrations = new Set(rows.map((r: { filename: string }) => r.filename))
111
- } catch {
112
- // Non-fatal: proceed without tracking
113
- }
78
+ } catch { /* non-fatal */ }
114
79
 
115
- let applied = 0
116
- let skipped = 0
117
- let failed = 0
80
+ let applied = 0, skipped = 0, failed = 0
118
81
 
119
82
  for (const { name, path } of migrationFiles) {
120
83
  if (appliedMigrations.has(name)) {
121
- console.log(` Skipping ${name} (already applied)`)
122
84
  skipped++
123
85
  continue
124
86
  }
125
87
 
126
88
  const sql = readFileSync(path, 'utf8')
127
89
  process.stdout.write(` Applying ${name}... `)
128
-
129
90
  const statements = splitSqlStatements(sql)
130
91
 
131
92
  try {
132
- for (const stmt of statements) {
133
- await client.query(stmt)
134
- }
135
- // Record as applied
93
+ for (const stmt of statements) await client.query(stmt)
136
94
  await client.query(
137
95
  `INSERT INTO _spine_migrations (filename) VALUES ($1) ON CONFLICT (filename) DO NOTHING`,
138
96
  [name]
@@ -147,10 +105,60 @@ async function migrateCommand(options: MigrateOptions): Promise<void> {
147
105
  }
148
106
 
149
107
  await client.end()
108
+ return { applied, skipped, failed }
109
+ }
110
+
111
+ async function migrateCommand(options: MigrateOptions): Promise<void> {
112
+ console.log('\nRunning Spine Framework migrations...\n')
113
+
114
+ // Load .env from project root
115
+ loadEnv()
116
+
117
+ const supabaseUrl = process.env.SUPABASE_URL
118
+ if (!supabaseUrl || supabaseUrl.includes('placeholder') || supabaseUrl.includes('your-project')) {
119
+ console.error('Error: SUPABASE_URL not set. Edit your .env file first.')
120
+ console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/api')
121
+ process.exit(1)
122
+ }
123
+
124
+ const dbPassword = options.dbPassword || process.env.SUPABASE_DB_PASSWORD
125
+ if (!dbPassword) {
126
+ console.error('Error: SUPABASE_DB_PASSWORD not set.')
127
+ console.error(' Add it to .env or pass --db-password <password>')
128
+ console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/database')
129
+ process.exit(1)
130
+ }
131
+
132
+ // Override env with CLI flag if provided
133
+ if (options.dbPassword) process.env.SUPABASE_DB_PASSWORD = options.dbPassword
134
+
135
+ const migrationFiles = collectMigrations(options.frameworkOnly)
136
+
137
+ if (migrationFiles.length === 0) {
138
+ console.log('No migration files found.')
139
+ return
140
+ }
141
+
142
+ console.log(` Found ${migrationFiles.length} migration file(s)\n`)
143
+
144
+ if (options.dryRun) {
145
+ for (const file of migrationFiles) {
146
+ console.log(` [dry-run] Would apply: ${file.name}`)
147
+ }
148
+ console.log('\nDry run complete — no changes made.\n')
149
+ return
150
+ }
151
+
152
+ const result = await runMigrations({ frameworkOnly: options.frameworkOnly })
153
+
154
+ if (!result) {
155
+ console.error('Could not connect: SUPABASE_URL or SUPABASE_DB_PASSWORD missing.')
156
+ process.exit(1)
157
+ }
150
158
 
151
- console.log(`\n ${applied} applied, ${skipped} skipped, ${failed} failed`)
159
+ console.log(`\n ${result.applied} applied, ${result.skipped} skipped, ${result.failed} failed`)
152
160
 
153
- if (failed > 0) {
161
+ if (result.failed > 0) {
154
162
  console.error('\nSome migrations failed. Check errors above.')
155
163
  process.exit(1)
156
164
  }
@@ -209,6 +209,14 @@ export function RegisterPage() {
209
209
  const handleSubmit = async (e: React.FormEvent) => {
210
210
  e.preventDefault()
211
211
  setError('')
212
+
213
+ // Guard: if registration config hasn't loaded yet, block submission
214
+ // This prevents silently falling through to new_account when config is needed
215
+ if (!isFirstUser && !inviteToken && !regConfig) {
216
+ setError('Registration configuration not loaded. Please refresh and try again.')
217
+ return
218
+ }
219
+
212
220
  setIsLoading(true)
213
221
 
214
222
  try {
@@ -314,6 +322,9 @@ export function RegisterPage() {
314
322
  target_account: targetAccount,
315
323
  role_slug: effectiveRole,
316
324
  }, 'POST')
325
+ } else if (effectiveAccountStrategy === 'existing' && !targetAccount) {
326
+ // Misconfigured: existing strategy but no target account in spine.config.json
327
+ throw new Error('Registration misconfigured: account_strategy is "existing" but no target_account is set.')
317
328
  } else {
318
329
  // Create new account (personal or company)
319
330
  console.log('Using new_account path')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.72",
3
+ "version": "0.3.74",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {