shsu 0.0.5 → 0.0.6

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 +31 -4
  2. package/bin/shsu.mjs +123 -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,9 +162,10 @@ 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
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,11 @@ 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
+ },
365
431
  ];
366
432
 
367
433
  const serverInfo = {
@@ -411,7 +477,7 @@ async function cmdMcp() {
411
477
  }
412
478
 
413
479
  if (!noRestart) {
414
- output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
480
+ output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
415
481
  }
416
482
 
417
483
  return { content: [{ type: 'text', text: `Deployed${funcName ? ` ${funcName}` : ' all functions'}${noRestart ? ' (no restart)' : ''}\n\n${output}` }] };
@@ -448,7 +514,7 @@ async function cmdMcp() {
448
514
  if (!config.server) {
449
515
  return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
450
516
  }
451
- const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
517
+ const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
452
518
  return { content: [{ type: 'text', text: `Restarted edge-runtime\n\n${output}` }] };
453
519
  }
454
520
 
@@ -487,11 +553,41 @@ async function cmdMcp() {
487
553
  return {
488
554
  content: [{
489
555
  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}`,
556
+ 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}`,
491
557
  }],
492
558
  };
493
559
  }
494
560
 
561
+ case 'migrate': {
562
+ if (!config.server) {
563
+ return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
564
+ }
565
+ if (!existsSync(config.migrationsPath)) {
566
+ return { content: [{ type: 'text', text: `Error: Migrations folder not found: ${config.migrationsPath}` }] };
567
+ }
568
+ const migrations = readdirSync(config.migrationsPath)
569
+ .filter((f) => f.endsWith('.sql'))
570
+ .sort();
571
+ if (migrations.length === 0) {
572
+ return { content: [{ type: 'text', text: 'No migration files found.' }] };
573
+ }
574
+ let output = `Found ${migrations.length} migration(s): ${migrations.join(', ')}\n\n`;
575
+ // Sync migrations
576
+ output += captureExec(`rsync -avz --delete "${config.migrationsPath}/" "${config.server}:/tmp/shsu-migrations/"`) + '\n';
577
+ // Find db container
578
+ const dbContainer = captureExec(`ssh ${config.server} "docker ps -q --filter 'name=${config.dbContainer}'"`);
579
+ if (!dbContainer) {
580
+ return { content: [{ type: 'text', text: `Error: Database container not found (filter: ${config.dbContainer})` }] };
581
+ }
582
+ // Run migrations
583
+ for (const migration of migrations) {
584
+ output += `\nRunning ${migration}...\n`;
585
+ output += captureExec(`ssh ${config.server} "docker exec -i ${dbContainer} psql -U postgres -d postgres -f /tmp/shsu-migrations/${migration}"`) + '\n';
586
+ }
587
+ output += '\nAll migrations applied.';
588
+ return { content: [{ type: 'text', text: output }] };
589
+ }
590
+
495
591
  default:
496
592
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
497
593
  }
@@ -559,6 +655,8 @@ ${c.yellow('Commands:')}
559
655
  - With name: deploy single function
560
656
  Options: --no-restart
561
657
 
658
+ migrate Run SQL migrations on database
659
+
562
660
  logs [filter] Stream edge-runtime logs
563
661
  - Optional filter string
564
662
 
@@ -578,6 +676,7 @@ ${c.yellow('Examples:')}
578
676
  shsu init
579
677
  shsu deploy
580
678
  shsu deploy hello-world --no-restart
679
+ shsu migrate
581
680
  shsu logs hello-world
582
681
  shsu invoke hello-world '{"name":"Stefan"}'
583
682
  shsu new my-function
@@ -617,6 +716,11 @@ async function main() {
617
716
  case 'restart':
618
717
  await cmdRestart();
619
718
  break;
719
+ case 'migrate':
720
+ case 'migration':
721
+ case 'migrations':
722
+ await cmdMigrate();
723
+ break;
620
724
  case 'new':
621
725
  case 'create':
622
726
  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.6",
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"