spine-framework 0.1.12 → 0.1.14

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 {
@@ -24,21 +24,7 @@ for (let i = 0; i < args.length; i++) {
24
24
  if (args[i] === '--service-role-key' && args[i + 1]) childEnv.SUPABASE_SERVICE_ROLE_KEY = args[++i]
25
25
  }
26
26
 
27
- // On Node < 22, supabase-js Realtime crashes if WebSocket is not globally available.
28
- // Polyfill it via a small inline require so every createClient() call across all
29
- // _shared modules finds a valid WebSocket constructor at module load time.
30
- const wsPolyfillPath = resolve(pkgRoot, 'node_modules/ws')
31
- const wsPolyfillExists = fs.existsSync(wsPolyfillPath)
32
- if (wsPolyfillExists && !childEnv.SUPABASE_WS_PATCHED) {
33
- const major = parseInt(process.version.replace('v', '').split('.')[0], 10)
34
- if (major < 22) {
35
- const wsShimPath = resolve(pkgRoot, 'bin/ws-shim.cjs')
36
- childEnv.NODE_OPTIONS = `--require ${wsShimPath}${childEnv.NODE_OPTIONS ? ' ' + childEnv.NODE_OPTIONS : ''}`
37
- childEnv.SUPABASE_WS_PATCHED = '1'
38
- }
39
- }
40
-
41
- const entry = resolve(pkgRoot, '.framework/cli/index.ts')
27
+ const entry = resolve(pkgRoot, 'bin/ws-shim.ts')
42
28
 
43
29
  // Find tsx: prefer consuming project's copy, fall back to our own
44
30
  const tsxPaths = [
package/bin/ws-shim.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Entry point for the CLI on Node < 22.
2
+ // Sets globalThis.WebSocket before any supabase-js module is imported,
3
+ // preventing the "Node.js 20 detected without native WebSocket" crash.
4
+ import { WebSocket } from 'ws'
5
+ if (typeof globalThis.WebSocket === 'undefined') {
6
+ ;(globalThis as any).WebSocket = WebSocket
7
+ }
8
+
9
+ // Now import the real CLI — all supabase createClient() calls happen after this point
10
+ await import('../.framework/cli/index.ts')
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "description": "Spine — enterprise application framework built on Supabase + Netlify + React",
6
6
  "type": "module",