shsu 0.0.5 → 0.0.7

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.
Files changed (3) hide show
  1. package/README.md +32 -4
  2. package/bin/shsu.mjs +215 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **S**elf-**H**osted **S**upabase **U**tilities
4
4
 
5
- Deploy and manage Supabase Edge Functions on Coolify-hosted Supabase.
5
+ Deploy and manage Supabase Edge Functions and migrations on Coolify-hosted Supabase.
6
6
 
7
7
  ## Install
8
8
 
@@ -27,17 +27,22 @@ This adds config to your `package.json`:
27
27
  "shsu": {
28
28
  "server": "root@your-coolify-server",
29
29
  "remotePath": "/data/coolify/services/YOUR_SERVICE_ID/volumes/functions",
30
- "url": "https://your-supabase.example.com"
30
+ "url": "https://your-supabase.example.com",
31
+ "edgeContainer": "edge",
32
+ "dbContainer": "postgres"
31
33
  }
32
34
  }
33
35
  ```
34
36
 
35
- Find your `remotePath` by running on your server:
37
+ ### Finding Configuration Values
36
38
 
39
+ **remotePath** - SSH to your server and run:
37
40
  ```bash
38
41
  docker inspect $(docker ps -q --filter 'name=edge') | grep -A 5 "Mounts"
39
42
  ```
40
43
 
44
+ **Container names** - SSH to your server and run `docker ps` to list containers. Coolify names containers using the pattern `<service>-<uuid>` (e.g., `abc123-supabase-edge-functions-1`). The filter does substring matching, so `edge` matches any container with "edge" in its name.
45
+
41
46
  ## Usage
42
47
 
43
48
  ```bash
@@ -56,6 +61,9 @@ shsu deploy hello-world
56
61
  # Deploy without restarting edge-runtime
57
62
  shsu deploy hello-world --no-restart
58
63
 
64
+ # Run database migrations
65
+ shsu migrate
66
+
59
67
  # Stream logs
60
68
  shsu logs
61
69
 
@@ -75,6 +83,21 @@ shsu new my-function
75
83
  shsu restart
76
84
  ```
77
85
 
86
+ ## Migrations
87
+
88
+ Place SQL files in `./supabase/migrations/` (or your configured `migrationsPath`). Files are sorted alphabetically and executed in order.
89
+
90
+ ```
91
+ supabase/migrations/
92
+ 001_create_users.sql
93
+ 002_add_email_index.sql
94
+ ```
95
+
96
+ Run with:
97
+ ```bash
98
+ shsu migrate
99
+ ```
100
+
78
101
  ## Configuration
79
102
 
80
103
  Config is read from `package.json` "shsu" key. Environment variables override package.json values.
@@ -85,6 +108,9 @@ Config is read from `package.json` "shsu" key. Environment variables override pa
85
108
  | `remotePath` / `SHSU_REMOTE_PATH` | Yes | Remote path to functions directory |
86
109
  | `url` / `SHSU_URL` | For `invoke` | Supabase URL |
87
110
  | `localPath` / `SHSU_LOCAL_PATH` | No | Local functions path (default: `./supabase/functions`) |
111
+ | `migrationsPath` / `SHSU_MIGRATIONS_PATH` | No | Local migrations path (default: `./supabase/migrations`) |
112
+ | `edgeContainer` / `SHSU_EDGE_CONTAINER` | No | Edge runtime container filter (default: `edge`) |
113
+ | `dbContainer` / `SHSU_DB_CONTAINER` | No | Database container filter (default: `postgres`) |
88
114
 
89
115
  ## MCP Server
90
116
 
@@ -136,14 +162,16 @@ Add to `.cursor/mcp.json` in your project:
136
162
  }
137
163
  ```
138
164
 
139
- ### Available Tools
165
+ ### Available MCP Tools
140
166
 
141
167
  - `deploy` - Deploy edge functions
168
+ - `migrate` - Run database migrations
142
169
  - `list` - List local and remote functions
143
170
  - `invoke` - Invoke a function
144
171
  - `restart` - Restart edge-runtime
145
172
  - `new` - Create new function from template
146
173
  - `config` - Show current configuration
174
+ - `docs` - Get setup documentation
147
175
 
148
176
  ## Releasing
149
177
 
package/bin/shsu.mjs CHANGED
@@ -27,7 +27,10 @@ function loadConfig() {
27
27
  server: process.env.SHSU_SERVER || pkgConfig.server,
28
28
  remotePath: process.env.SHSU_REMOTE_PATH || pkgConfig.remotePath,
29
29
  localPath: process.env.SHSU_LOCAL_PATH || pkgConfig.localPath || './supabase/functions',
30
+ migrationsPath: process.env.SHSU_MIGRATIONS_PATH || pkgConfig.migrationsPath || './supabase/migrations',
30
31
  url: process.env.SHSU_URL || pkgConfig.url,
32
+ edgeContainer: process.env.SHSU_EDGE_CONTAINER || pkgConfig.edgeContainer || 'edge',
33
+ dbContainer: process.env.SHSU_DB_CONTAINER || pkgConfig.dbContainer || 'postgres',
31
34
  };
32
35
  }
33
36
 
@@ -85,7 +88,7 @@ function runSync(cmd) {
85
88
  }
86
89
 
87
90
  function getEdgeContainer() {
88
- return runSync(`ssh ${config.server} "docker ps -q --filter 'name=edge'"`);
91
+ return runSync(`ssh ${config.server} "docker ps -q --filter 'name=${config.edgeContainer}'"`);
89
92
  }
90
93
 
91
94
  // ─────────────────────────────────────────────────────────────
@@ -121,7 +124,7 @@ async function cmdDeploy(funcName, noRestart = false) {
121
124
  info('Restarting edge-runtime...');
122
125
  await run('ssh', [
123
126
  config.server,
124
- `docker restart $(docker ps -q --filter 'name=edge')`,
127
+ `docker restart $(docker ps -q --filter 'name=${config.edgeContainer}')`,
125
128
  ], { stdio: ['inherit', 'pipe', 'inherit'] });
126
129
  success(`Deployed${funcName ? ` ${funcName}` : ''}`);
127
130
  } else {
@@ -136,7 +139,7 @@ async function cmdLogs(filter, lines = 100) {
136
139
 
137
140
  const sshArgs = [
138
141
  config.server,
139
- `docker logs -f $(docker ps -q --filter 'name=edge') --tail ${lines} 2>&1`,
142
+ `docker logs -f $(docker ps -q --filter 'name=${config.edgeContainer}') --tail ${lines} 2>&1`,
140
143
  ];
141
144
 
142
145
  if (filter) {
@@ -196,11 +199,62 @@ async function cmdRestart() {
196
199
  info('Restarting edge-runtime...');
197
200
  await run('ssh', [
198
201
  config.server,
199
- `docker restart $(docker ps -q --filter 'name=edge')`,
202
+ `docker restart $(docker ps -q --filter 'name=${config.edgeContainer}')`,
200
203
  ], { stdio: ['inherit', 'pipe', 'inherit'] });
201
204
  success('Restarted');
202
205
  }
203
206
 
207
+ async function cmdMigrate() {
208
+ requireServer();
209
+
210
+ if (!existsSync(config.migrationsPath)) {
211
+ error(`Migrations folder not found: ${config.migrationsPath}`);
212
+ }
213
+
214
+ // Get list of migration files
215
+ const migrations = readdirSync(config.migrationsPath)
216
+ .filter((f) => f.endsWith('.sql'))
217
+ .sort();
218
+
219
+ if (migrations.length === 0) {
220
+ info('No migration files found.');
221
+ return;
222
+ }
223
+
224
+ info(`Found ${migrations.length} migration(s): ${migrations.join(', ')}`);
225
+
226
+ // Copy migrations to server
227
+ const remoteMigrationsPath = '/tmp/shsu-migrations';
228
+ info('Syncing migrations to server...');
229
+ await run('rsync', [
230
+ '-avz', '--delete',
231
+ `${config.migrationsPath}/`,
232
+ `${config.server}:${remoteMigrationsPath}/`,
233
+ ]);
234
+
235
+ // Find the database container
236
+ const dbContainer = runSync(`ssh ${config.server} "docker ps -q --filter 'name=${config.dbContainer}'"`);
237
+ if (!dbContainer) {
238
+ error(`Database container not found (filter: ${config.dbContainer})`);
239
+ }
240
+
241
+ // Run each migration
242
+ for (const migration of migrations) {
243
+ info(`Running ${migration}...`);
244
+ try {
245
+ await run('ssh', [
246
+ config.server,
247
+ `docker exec -i ${dbContainer} psql -U postgres -d postgres -f /tmp/shsu-migrations/${migration}`,
248
+ ]);
249
+ success(`Applied ${migration}`);
250
+ } catch (e) {
251
+ error(`Failed to apply ${migration}: ${e.message}`);
252
+ }
253
+ }
254
+
255
+ success('All migrations applied');
256
+ }
257
+
204
258
  async function cmdNew(funcName) {
205
259
  if (!funcName) {
206
260
  error('Usage: shsu new <function-name>');
@@ -239,23 +293,26 @@ function cmdEnv() {
239
293
  console.log(`
240
294
  ${c.yellow('Configuration (package.json "shsu" key or environment variables):')}
241
295
 
242
- server SSH host for your Coolify server
243
- remotePath Remote path to functions directory
244
- url Supabase URL (for invoke command)
245
- localPath Local functions path (default: ./supabase/functions)
246
-
247
- ${c.yellow('Environment variables override package.json:')}
248
-
249
- SHSU_SERVER, SHSU_REMOTE_PATH, SHSU_URL, SHSU_LOCAL_PATH
296
+ server SSH host for your Coolify server
297
+ remotePath Remote path to functions directory
298
+ url Supabase URL (for invoke command)
299
+ localPath Local functions path (default: ./supabase/functions)
300
+ migrationsPath Local migrations path (default: ./supabase/migrations)
301
+ edgeContainer Edge runtime container filter (default: edge)
302
+ dbContainer Database container filter (default: postgres)
250
303
 
251
304
  ${c.yellow('Current values:')}
252
305
 
253
- server = ${config.server || c.dim('(not set)')}
254
- remotePath = ${config.remotePath || c.dim('(not set)')}
255
- url = ${config.url || c.dim('(not set)')}
256
- localPath = ${config.localPath}
306
+ server = ${config.server || c.dim('(not set)')}
307
+ remotePath = ${config.remotePath || c.dim('(not set)')}
308
+ url = ${config.url || c.dim('(not set)')}
309
+ localPath = ${config.localPath}
310
+ migrationsPath = ${config.migrationsPath}
311
+ edgeContainer = ${config.edgeContainer}
312
+ dbContainer = ${config.dbContainer}
257
313
 
258
314
  ${c.dim('Run "shsu init" to configure via prompts.')}
315
+ ${c.dim('Find container names in Coolify: Services → Your Service → look for container name prefix')}
259
316
  `);
260
317
  }
261
318
 
@@ -285,6 +342,8 @@ async function cmdInit() {
285
342
  const remotePath = await ask('Remote path to functions', config.remotePath);
286
343
  const url = await ask('Supabase URL', config.url);
287
344
  const localPath = await ask('Local functions path', config.localPath || './supabase/functions');
345
+ const edgeContainer = await ask('Edge container name filter', config.edgeContainer || 'edge');
346
+ const dbContainer = await ask('Database container name filter', config.dbContainer || 'postgres');
288
347
 
289
348
  rl.close();
290
349
 
@@ -295,6 +354,8 @@ async function cmdInit() {
295
354
  remotePath: remotePath || undefined,
296
355
  url: url || undefined,
297
356
  localPath: localPath !== './supabase/functions' ? localPath : undefined,
357
+ edgeContainer: edgeContainer !== 'edge' ? edgeContainer : undefined,
358
+ dbContainer: dbContainer !== 'postgres' ? dbContainer : undefined,
298
359
  };
299
360
 
300
361
  // Remove undefined values
@@ -362,6 +423,16 @@ async function cmdMcp() {
362
423
  description: 'Get current shsu configuration.',
363
424
  inputSchema: { type: 'object', properties: {} },
364
425
  },
426
+ {
427
+ name: 'migrate',
428
+ description: 'Run SQL migrations on the database. Syncs migration files via rsync and executes them via psql.',
429
+ inputSchema: { type: 'object', properties: {} },
430
+ },
431
+ {
432
+ name: 'docs',
433
+ description: 'Get documentation on how to set up and use shsu for deploying Supabase Edge Functions.',
434
+ inputSchema: { type: 'object', properties: {} },
435
+ },
365
436
  ];
366
437
 
367
438
  const serverInfo = {
@@ -411,7 +482,7 @@ async function cmdMcp() {
411
482
  }
412
483
 
413
484
  if (!noRestart) {
414
- output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
485
+ output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
415
486
  }
416
487
 
417
488
  return { content: [{ type: 'text', text: `Deployed${funcName ? ` ${funcName}` : ' all functions'}${noRestart ? ' (no restart)' : ''}\n\n${output}` }] };
@@ -448,7 +519,7 @@ async function cmdMcp() {
448
519
  if (!config.server) {
449
520
  return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
450
521
  }
451
- const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
522
+ const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
452
523
  return { content: [{ type: 'text', text: `Restarted edge-runtime\n\n${output}` }] };
453
524
  }
454
525
 
@@ -487,7 +558,124 @@ async function cmdMcp() {
487
558
  return {
488
559
  content: [{
489
560
  type: 'text',
490
- text: `Current configuration:\n server: ${config.server || '(not set)'}\n remotePath: ${config.remotePath || '(not set)'}\n url: ${config.url || '(not set)'}\n localPath: ${config.localPath}`,
561
+ text: `Current configuration:\n server: ${config.server || '(not set)'}\n remotePath: ${config.remotePath || '(not set)'}\n url: ${config.url || '(not set)'}\n localPath: ${config.localPath}\n migrationsPath: ${config.migrationsPath}\n edgeContainer: ${config.edgeContainer}\n dbContainer: ${config.dbContainer}`,
562
+ }],
563
+ };
564
+ }
565
+
566
+ case 'migrate': {
567
+ if (!config.server) {
568
+ return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
569
+ }
570
+ if (!existsSync(config.migrationsPath)) {
571
+ return { content: [{ type: 'text', text: `Error: Migrations folder not found: ${config.migrationsPath}` }] };
572
+ }
573
+ const migrations = readdirSync(config.migrationsPath)
574
+ .filter((f) => f.endsWith('.sql'))
575
+ .sort();
576
+ if (migrations.length === 0) {
577
+ return { content: [{ type: 'text', text: 'No migration files found.' }] };
578
+ }
579
+ let output = `Found ${migrations.length} migration(s): ${migrations.join(', ')}\n\n`;
580
+ // Sync migrations
581
+ output += captureExec(`rsync -avz --delete "${config.migrationsPath}/" "${config.server}:/tmp/shsu-migrations/"`) + '\n';
582
+ // Find db container
583
+ const dbContainer = captureExec(`ssh ${config.server} "docker ps -q --filter 'name=${config.dbContainer}'"`);
584
+ if (!dbContainer) {
585
+ return { content: [{ type: 'text', text: `Error: Database container not found (filter: ${config.dbContainer})` }] };
586
+ }
587
+ // Run migrations
588
+ for (const migration of migrations) {
589
+ output += `\nRunning ${migration}...\n`;
590
+ output += captureExec(`ssh ${config.server} "docker exec -i ${dbContainer} psql -U postgres -d postgres -f /tmp/shsu-migrations/${migration}"`) + '\n';
591
+ }
592
+ output += '\nAll migrations applied.';
593
+ return { content: [{ type: 'text', text: output }] };
594
+ }
595
+
596
+ case 'docs': {
597
+ return {
598
+ content: [{
599
+ type: 'text',
600
+ text: `# shsu - Self-Hosted Supabase Utilities
601
+
602
+ Deploy and manage Supabase Edge Functions on Coolify-hosted Supabase.
603
+
604
+ ## Project Setup
605
+
606
+ 1. **Configure shsu** by adding to package.json:
607
+ \`\`\`json
608
+ {
609
+ "shsu": {
610
+ "server": "root@your-coolify-server",
611
+ "remotePath": "/data/coolify/services/YOUR_SERVICE_ID/volumes/functions",
612
+ "url": "https://your-supabase.example.com",
613
+ "edgeContainer": "edge",
614
+ "dbContainer": "postgres"
615
+ }
616
+ }
617
+ \`\`\`
618
+
619
+ Or run \`npx shsu init\` for interactive setup.
620
+
621
+ 2. **Find configuration values** by SSH'ing to your server:
622
+ - Container names: \`docker ps\` (Coolify uses pattern \`<service>-<uuid>\`)
623
+ - Remote path: \`docker inspect $(docker ps -q --filter 'name=edge') | grep -A 5 "Mounts"\`
624
+
625
+ ## Directory Structure
626
+
627
+ \`\`\`
628
+ your-project/
629
+ ├── package.json # Contains shsu config
630
+ ├── supabase/
631
+ │ ├── functions/ # Edge functions (default localPath)
632
+ │ │ ├── hello-world/
633
+ │ │ │ └── index.ts
634
+ │ │ └── another-func/
635
+ │ │ └── index.ts
636
+ │ └── migrations/ # SQL migrations (default migrationsPath)
637
+ │ ├── 001_create_users.sql
638
+ │ └── 002_add_indexes.sql
639
+ \`\`\`
640
+
641
+ ## Configuration Options
642
+
643
+ | Key | Required | Default | Description |
644
+ |-----|----------|---------|-------------|
645
+ | server | Yes | - | SSH host (e.g., root@server.com) |
646
+ | remotePath | Yes | - | Remote path to functions directory |
647
+ | url | For invoke | - | Supabase URL |
648
+ | localPath | No | ./supabase/functions | Local functions path |
649
+ | migrationsPath | No | ./supabase/migrations | Local migrations path |
650
+ | edgeContainer | No | edge | Edge runtime container filter |
651
+ | dbContainer | No | postgres | Database container filter |
652
+
653
+ ## Edge Function Template
654
+
655
+ Use \`new\` tool to create functions. Each function needs an index.ts:
656
+
657
+ \`\`\`typescript
658
+ Deno.serve(async (req) => {
659
+ const { name } = await req.json()
660
+ return new Response(
661
+ JSON.stringify({ message: \`Hello \${name}!\` }),
662
+ { headers: { "Content-Type": "application/json" } }
663
+ )
664
+ })
665
+ \`\`\`
666
+
667
+ ## Workflow
668
+
669
+ 1. Create function: \`new\` tool with function name
670
+ 2. Edit the function code in supabase/functions/<name>/index.ts
671
+ 3. Deploy: \`deploy\` tool (syncs via rsync, restarts edge-runtime)
672
+ 4. Test: \`invoke\` tool with JSON data
673
+ 5. Debug: Check logs on the server
674
+
675
+ ## Migrations
676
+
677
+ Place .sql files in supabase/migrations/. They execute alphabetically.
678
+ Use \`migrate\` tool to run all migrations via psql in the database container.`,
491
679
  }],
492
680
  };
493
681
  }
@@ -559,6 +747,8 @@ ${c.yellow('Commands:')}
559
747
  - With name: deploy single function
560
748
  Options: --no-restart
561
749
 
750
+ migrate Run SQL migrations on database
751
+
562
752
  logs [filter] Stream edge-runtime logs
563
753
  - Optional filter string
564
754
 
@@ -578,6 +768,7 @@ ${c.yellow('Examples:')}
578
768
  shsu init
579
769
  shsu deploy
580
770
  shsu deploy hello-world --no-restart
771
+ shsu migrate
581
772
  shsu logs hello-world
582
773
  shsu invoke hello-world '{"name":"Stefan"}'
583
774
  shsu new my-function
@@ -617,6 +808,11 @@ async function main() {
617
808
  case 'restart':
618
809
  await cmdRestart();
619
810
  break;
811
+ case 'migrate':
812
+ case 'migration':
813
+ case 'migrations':
814
+ await cmdMigrate();
815
+ break;
620
816
  case 'new':
621
817
  case 'create':
622
818
  await cmdNew(args[1]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shsu",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CLI for deploying and managing Supabase Edge Functions on self-hosted Supabase (Coolify, Docker Compose). Sync functions via rsync, stream logs, and invoke endpoints.",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"