spine-framework 0.1.13 → 0.1.15

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.
@@ -5,106 +5,34 @@
5
5
  * @layer cli
6
6
  * @stability stable
7
7
  *
8
- * `spine-framework init` — Bootstrap a fresh Spine installation.
8
+ * `spine-framework init` — Write .env and scaffold the custom/ workspace.
9
9
  *
10
- * Runs the foundation schema (000_foundation.sql) and seed data (001_seed.sql)
11
- * against the configured Supabase project, then scaffolds the custom/ workspace
12
- * if it doesn't already exist.
10
+ * Intentionally does NOT touch the database run `spine-framework migrate`
11
+ * after init to apply SQL migrations via a direct Postgres connection.
13
12
  *
14
13
  * **Usage:**
15
14
  * ```bash
16
- * # Fresh install (agent provides credentials from Supabase dashboard)
17
15
  * spine-framework init --url https://xyz.supabase.co --anon-key eyJ... --service-role-key eyJ...
18
- *
19
- * # Already have .env configured
20
- * spine-framework init
21
- *
22
- * # DB only, no filesystem changes
23
- * spine-framework init --skip-scaffold
16
+ * spine-framework migrate --db-password <password>
24
17
  * ```
25
- *
26
- * When --url/--anon-key/--service-role-key are provided, init writes .env before
27
- * running migrations — so it works on a completely fresh checkout with no prior config.
28
18
  */
29
19
 
30
20
  import type { Command } from 'commander'
31
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
21
+ import { existsSync, mkdirSync, writeFileSync } from 'fs'
32
22
  import { resolve, dirname } from 'path'
33
23
  import { fileURLToPath } from 'url'
34
24
 
35
25
  const __filename = fileURLToPath(import.meta.url)
36
26
  const __dirname = dirname(__filename)
37
27
  const PROJECT_ROOT = resolve(__dirname, '../../../')
38
- const MIGRATIONS_DIR = resolve(__dirname, '../../migrations')
39
28
 
40
29
  interface InitOptions {
41
- skipScaffold: boolean
42
30
  dryRun: boolean
43
31
  url?: string
44
32
  anonKey?: string
45
33
  serviceRoleKey?: string
46
34
  }
47
35
 
48
- async function runMigration(filename: string, dryRun: boolean): Promise<boolean> {
49
- const filePath = resolve(MIGRATIONS_DIR, filename)
50
- if (!existsSync(filePath)) {
51
- console.error(` ❌ Migration file not found: ${filename}`)
52
- return false
53
- }
54
-
55
- const sql = readFileSync(filePath, 'utf8')
56
-
57
- if (dryRun) {
58
- console.log(` [dry-run] Would execute: ${filename} (${sql.length} chars)`)
59
- return true
60
- }
61
-
62
- // Use the Supabase SQL endpoint to execute raw DDL.
63
- // This works on blank projects (no exec_sql RPC needed).
64
- const supabaseUrl = process.env.SUPABASE_URL
65
- const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
66
-
67
- if (!supabaseUrl || !serviceKey) {
68
- console.error(` ❌ Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY`)
69
- return false
70
- }
71
-
72
- // Use the Supabase /pg/query endpoint for raw SQL execution
73
- const pgResponse = await fetch(`${supabaseUrl}/pg/query`, {
74
- method: 'POST',
75
- headers: {
76
- 'Content-Type': 'application/json',
77
- 'apikey': serviceKey,
78
- 'Authorization': `Bearer ${serviceKey}`,
79
- },
80
- body: JSON.stringify({ query: sql })
81
- })
82
-
83
- if (pgResponse.ok) return true
84
-
85
- // Second fallback: use the Supabase Management API SQL endpoint
86
- // Extract project ref from URL (e.g. https://abcdef.supabase.co → abcdef)
87
- const projectRef = supabaseUrl.replace('https://', '').split('.')[0]
88
-
89
- const mgmtResponse = await fetch(
90
- `https://api.supabase.com/v1/projects/${projectRef}/database/query`,
91
- {
92
- method: 'POST',
93
- headers: {
94
- 'Content-Type': 'application/json',
95
- 'Authorization': `Bearer ${serviceKey}`,
96
- },
97
- body: JSON.stringify({ query: sql })
98
- }
99
- )
100
-
101
- if (mgmtResponse.ok) return true
102
-
103
- const text = await mgmtResponse.text()
104
- console.error(` ❌ Migration ${filename} failed: ${text}`)
105
- return false
106
- }
107
-
108
36
  function scaffoldCustomWorkspace(dryRun: boolean): void {
109
37
  const dirs = [
110
38
  'custom/apps',
@@ -128,22 +56,6 @@ function scaffoldCustomWorkspace(dryRun: boolean): void {
128
56
  }
129
57
  }
130
58
 
131
- async function checkAlreadyInitialized(): Promise<boolean> {
132
- try {
133
- const supabaseUrl = process.env.SUPABASE_URL
134
- const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
135
- if (!supabaseUrl || !serviceKey) return false
136
- const res = await fetch(
137
- `${supabaseUrl}/rest/v1/apps?slug=eq.spine-core&select=slug&limit=1`,
138
- { headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}` } }
139
- )
140
- const rows = await res.json() as any[]
141
- return Array.isArray(rows) && rows.length > 0
142
- } catch {
143
- return false
144
- }
145
- }
146
-
147
59
  function writeEnvFile(url: string, anonKey: string, serviceRoleKey: string, dryRun: boolean): void {
148
60
  const envPath = resolve(PROJECT_ROOT, '.env')
149
61
  const envContent = [
@@ -176,93 +88,36 @@ function writeEnvFile(url: string, anonKey: string, serviceRoleKey: string, dryR
176
88
  async function initCommand(options: InitOptions): Promise<void> {
177
89
  console.log('\n🚀 Spine Framework — Init\n')
178
90
 
179
- // Step 0: Write .env if credentials were provided via flags
91
+ // Step 1: Write .env
180
92
  if (options.url && options.anonKey && options.serviceRoleKey) {
181
- console.log('🔑 Step 0: Writing environment configuration...')
93
+ console.log('🔑 Step 1: Writing environment configuration...')
182
94
  writeEnvFile(options.url, options.anonKey, options.serviceRoleKey, options.dryRun)
183
- } else if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
95
+ } else if (!process.env.SUPABASE_URL) {
184
96
  console.error('❌ No Supabase credentials found.')
185
- console.error(' Provide them via flags:')
186
97
  console.error(' spine-framework init --url <url> --anon-key <key> --service-role-key <key>')
187
- console.error(' Or set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env')
188
98
  process.exit(1)
99
+ } else {
100
+ console.log(' ✓ Using existing .env credentials')
189
101
  }
190
102
 
191
- // Check if already initialized
192
- const alreadyInit = await checkAlreadyInitialized()
193
- if (alreadyInit) {
194
- console.log(' ⚠️ Database already initialized (spine-core app exists)')
195
- console.log(' Skipping schema migration. Use "spine-framework migrations" to manage updates.\n')
196
-
197
- if (!options.skipScaffold) {
198
- console.log('📁 Checking workspace scaffold...')
199
- scaffoldCustomWorkspace(options.dryRun)
200
- }
201
-
202
- console.log('\n✅ Init complete (already initialized)')
203
- return
204
- }
205
-
206
- // Step 1: Foundation schema
207
- console.log('📦 Step 1: Applying foundation schema (000_foundation.sql)...')
208
- const foundationOk = await runMigration('000_foundation.sql', options.dryRun)
209
- if (!foundationOk && !options.dryRun) {
210
- console.error('\n❌ Init failed at foundation schema. Check your Supabase connection.')
211
- process.exit(1)
212
- }
213
- console.log(' ✓ Foundation schema applied')
214
-
215
- // Step 2: Seed data
216
- console.log('\n🌱 Step 2: Applying seed data (001_seed.sql)...')
217
- const seedOk = await runMigration('001_seed.sql', options.dryRun)
218
- if (!seedOk && !options.dryRun) {
219
- console.error('\n❌ Init failed at seed data.')
220
- process.exit(1)
221
- }
222
- console.log(' ✓ Seed data applied')
223
-
224
- // Step 3: Record migration versions
225
- if (!options.dryRun) {
226
- console.log('\n📝 Step 3: Recording migration versions...')
227
- const supabaseUrl = process.env.SUPABASE_URL!
228
- const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
229
- await fetch(`${supabaseUrl}/rest/v1/schema_migrations`, {
230
- method: 'POST',
231
- headers: {
232
- 'Content-Type': 'application/json',
233
- 'apikey': serviceKey,
234
- 'Authorization': `Bearer ${serviceKey}`,
235
- 'Prefer': 'resolution=merge-duplicates',
236
- },
237
- body: JSON.stringify([
238
- { version: '000_foundation', applied_at: new Date().toISOString() },
239
- { version: '001_seed', applied_at: new Date().toISOString() },
240
- ])
241
- })
242
- console.log(' ✓ Migration versions recorded')
243
- }
244
-
245
- // Step 4: Scaffold custom workspace
246
- if (!options.skipScaffold) {
247
- console.log('\n📁 Step 4: Scaffolding custom workspace...')
248
- scaffoldCustomWorkspace(options.dryRun)
249
- }
103
+ // Step 2: Scaffold custom workspace
104
+ console.log('\n📁 Step 2: Scaffolding custom workspace...')
105
+ scaffoldCustomWorkspace(options.dryRun)
250
106
 
251
- console.log('\n✅ Spine initialized successfully!')
252
- console.log('\n Next steps:')
253
- console.log(' 1. spine-framework install-app <app-slug> # Install an app')
254
- console.log(' 2. spine-framework create-app my-app # Or scaffold a new one')
255
- console.log(' 3. npm run assemble && netlify dev # Start developing')
107
+ console.log('\n✅ Project initialized!')
108
+ console.log('\n Next: apply database migrations:')
109
+ console.log(' spine-framework migrate --db-password <your-db-password>')
110
+ console.log('\n Find your DB password at:')
111
+ console.log(' https://supabase.com/dashboard/project/_/settings/database')
256
112
  }
257
113
 
258
114
  export function registerInitCommands(program: Command) {
259
115
  program
260
116
  .command('init')
261
- .description('Initialize a fresh Spine installation (schema + seed + scaffold)')
262
- .option('--url <url>', 'Supabase project URL (writes .env if provided)')
117
+ .description('Write .env and scaffold the custom/ workspace (no DB changes)')
118
+ .option('--url <url>', 'Supabase project URL')
263
119
  .option('--anon-key <key>', 'Supabase anon key')
264
120
  .option('--service-role-key <key>', 'Supabase service role key')
265
- .option('--skip-scaffold', 'Skip creating custom/ workspace directories', false)
266
121
  .option('--dry-run', 'Show what would happen without making changes', false)
267
122
  .action(async (opts) => {
268
123
  try {
@@ -0,0 +1,134 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * @module cli/commands/migrate
4
+ * @audience installer
5
+ * @layer cli
6
+ * @stability stable
7
+ *
8
+ * `spine-framework migrate` — Apply SQL migrations to a Supabase project
9
+ * via a direct Postgres connection.
10
+ *
11
+ * Requires --db-password (the Supabase database password).
12
+ * Get it from: https://supabase.com/dashboard/project/_/settings/database
13
+ *
14
+ * **Usage:**
15
+ * ```bash
16
+ * spine-framework migrate --db-password <password>
17
+ * spine-framework migrate --db-password <password> --dry-run
18
+ * ```
19
+ */
20
+
21
+ import type { Command } from 'commander'
22
+ import { existsSync, readdirSync, readFileSync } from 'fs'
23
+ import { resolve, dirname } from 'path'
24
+ import { fileURLToPath } from 'url'
25
+
26
+ const __filename = fileURLToPath(import.meta.url)
27
+ const __dirname = dirname(__filename)
28
+ const MIGRATIONS_DIR = resolve(__dirname, '../../migrations')
29
+
30
+ interface MigrateOptions {
31
+ dbPassword: string
32
+ dryRun: boolean
33
+ }
34
+
35
+ async function migrateCommand(options: MigrateOptions): Promise<void> {
36
+ console.log('\n🗄️ Spine Framework — Migrate\n')
37
+
38
+ const supabaseUrl = process.env.SUPABASE_URL
39
+ if (!supabaseUrl) {
40
+ console.error('❌ SUPABASE_URL not set. Run `spine-framework init` first.')
41
+ process.exit(1)
42
+ }
43
+
44
+ // Derive connection string from Supabase URL
45
+ // Format: https://<ref>.supabase.co → postgres://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
46
+ // Use direct connection (port 5432) via the db.<ref>.supabase.co host
47
+ const projectRef = supabaseUrl.replace('https://', '').split('.')[0]
48
+ const connectionString = `postgresql://postgres:${options.dbPassword}@db.${projectRef}.supabase.co:5432/postgres`
49
+
50
+ if (!existsSync(MIGRATIONS_DIR)) {
51
+ console.error(`❌ Migrations directory not found: ${MIGRATIONS_DIR}`)
52
+ process.exit(1)
53
+ }
54
+
55
+ const files = readdirSync(MIGRATIONS_DIR)
56
+ .filter(f => f.endsWith('.sql'))
57
+ .sort()
58
+
59
+ if (files.length === 0) {
60
+ console.log(' No migration files found.')
61
+ return
62
+ }
63
+
64
+ console.log(` Found ${files.length} migration file(s)\n`)
65
+
66
+ if (options.dryRun) {
67
+ for (const file of files) {
68
+ console.log(` [dry-run] Would apply: ${file}`)
69
+ }
70
+ return
71
+ }
72
+
73
+ // Dynamically import pg to avoid loading it at module init time
74
+ const { default: pg } = await import('pg')
75
+ const client = new pg.Client({ connectionString })
76
+
77
+ try {
78
+ await client.connect()
79
+ console.log(' ✓ Connected to database\n')
80
+ } catch (err: any) {
81
+ console.error(`❌ Could not connect to database: ${err.message}`)
82
+ console.error(' Check your --db-password and that the project is active.')
83
+ process.exit(1)
84
+ }
85
+
86
+ let applied = 0
87
+ let failed = 0
88
+
89
+ for (const file of files) {
90
+ const sql = readFileSync(resolve(MIGRATIONS_DIR, file), 'utf8')
91
+ process.stdout.write(` Applying ${file}... `)
92
+ try {
93
+ await client.query(sql)
94
+ console.log('✓')
95
+ applied++
96
+ } catch (err: any) {
97
+ console.log('❌')
98
+ console.error(` ${err.message}`)
99
+ failed++
100
+ // Continue applying remaining migrations
101
+ }
102
+ }
103
+
104
+ await client.end()
105
+
106
+ console.log(`\n ${applied} applied, ${failed} failed`)
107
+
108
+ if (failed > 0) {
109
+ console.error('\n❌ Some migrations failed. Check errors above.')
110
+ process.exit(1)
111
+ }
112
+
113
+ console.log('\n✅ All migrations applied successfully!')
114
+ console.log('\n Next steps:')
115
+ console.log(' 1. spine-framework install-app <app-slug>')
116
+ console.log(' 2. npm run assemble && netlify dev')
117
+ }
118
+
119
+ export function registerMigrateCommands(program: Command) {
120
+ program
121
+ .command('migrate')
122
+ .description('Apply SQL migrations via direct Postgres connection')
123
+ .requiredOption('--db-password <password>', 'Supabase database password (from dashboard → Settings → Database)')
124
+ .option('--dry-run', 'Show what would happen without making changes', false)
125
+ .action(async (opts) => {
126
+ try {
127
+ await migrateCommand(opts)
128
+ } catch (err: any) {
129
+ console.error('Error:', err.message)
130
+ if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
131
+ process.exit(1)
132
+ }
133
+ })
134
+ }
@@ -66,6 +66,7 @@ const [
66
66
  { registerInstallAppCommands },
67
67
  { registerStatusCommands },
68
68
  { registerUninstallAppCommands },
69
+ { registerMigrateCommands },
69
70
  ] = await Promise.all([
70
71
  import('./commands/auth.ts'),
71
72
  import('./commands/pipelines.ts'),
@@ -82,6 +83,7 @@ const [
82
83
  import('./commands/install-app.ts'),
83
84
  import('./commands/status.ts'),
84
85
  import('./commands/uninstall-app.ts'),
86
+ import('./commands/migrate.ts'),
85
87
  ])
86
88
 
87
89
  registerAuthCommands(program)
@@ -99,6 +101,7 @@ registerInitCommands(program)
99
101
  registerInstallAppCommands(program)
100
102
  registerStatusCommands(program)
101
103
  registerUninstallAppCommands(program)
104
+ registerMigrateCommands(program)
102
105
 
103
106
  program.parseAsync(process.argv).catch((err) => {
104
107
  console.error('Error:', err.message)
@@ -4,26 +4,16 @@
4
4
  * @layer cli
5
5
  * @stability stable
6
6
  *
7
- * `spine-framework init` — Bootstrap a fresh Spine installation.
7
+ * `spine-framework init` — Write .env and scaffold the custom/ workspace.
8
8
  *
9
- * Runs the foundation schema (000_foundation.sql) and seed data (001_seed.sql)
10
- * against the configured Supabase project, then scaffolds the custom/ workspace
11
- * if it doesn't already exist.
9
+ * Intentionally does NOT touch the database run `spine-framework migrate`
10
+ * after init to apply SQL migrations via a direct Postgres connection.
12
11
  *
13
12
  * **Usage:**
14
13
  * ```bash
15
- * # Fresh install (agent provides credentials from Supabase dashboard)
16
14
  * spine-framework init --url https://xyz.supabase.co --anon-key eyJ... --service-role-key eyJ...
17
- *
18
- * # Already have .env configured
19
- * spine-framework init
20
- *
21
- * # DB only, no filesystem changes
22
- * spine-framework init --skip-scaffold
15
+ * spine-framework migrate --db-password <password>
23
16
  * ```
24
- *
25
- * When --url/--anon-key/--service-role-key are provided, init writes .env before
26
- * running migrations — so it works on a completely fresh checkout with no prior config.
27
17
  */
28
18
  import type { Command } from 'commander';
29
19
  export declare function registerInitCommands(program: Command): void;
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/init.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAoOxC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAkBpD"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/init.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA8FxC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAiBpD"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module cli/commands/migrate
3
+ * @audience installer
4
+ * @layer cli
5
+ * @stability stable
6
+ *
7
+ * `spine-framework migrate` — Apply SQL migrations to a Supabase project
8
+ * via a direct Postgres connection.
9
+ *
10
+ * Requires --db-password (the Supabase database password).
11
+ * Get it from: https://supabase.com/dashboard/project/_/settings/database
12
+ *
13
+ * **Usage:**
14
+ * ```bash
15
+ * spine-framework migrate --db-password <password>
16
+ * spine-framework migrate --db-password <password> --dry-run
17
+ * ```
18
+ */
19
+ import type { Command } from 'commander';
20
+ export declare function registerMigrateCommands(program: Command): void;
21
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/migrate.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAkGxC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAevD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "private": false,
5
5
  "description": "Spine — enterprise application framework built on Supabase + Netlify + React",
6
6
  "type": "module",
@@ -90,7 +90,8 @@
90
90
  "jsonwebtoken": "^9.0.3",
91
91
  "jwt-decode": "^4.0.0",
92
92
  "tsx": "^4.21.0",
93
- "ws": "^8.21.0"
93
+ "ws": "^8.21.0",
94
+ "pg": "^8.13.0"
94
95
  },
95
96
  "peerDependencies": {
96
97
  "@netlify/functions": "^2.0.0",