shsu 0.0.1 → 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.
- package/.github/workflows/publish.yml +5 -4
- package/README.md +98 -3
- package/bin/shsu.mjs +361 -16
- package/package.json +2 -2
- package/.claude/settings.local.json +0 -7
|
@@ -8,12 +8,13 @@ on:
|
|
|
8
8
|
jobs:
|
|
9
9
|
publish:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
11
14
|
steps:
|
|
12
15
|
- uses: actions/checkout@v4
|
|
13
16
|
- uses: actions/setup-node@v4
|
|
14
17
|
with:
|
|
15
|
-
node-version: '
|
|
18
|
+
node-version: '24'
|
|
16
19
|
registry-url: 'https://registry.npmjs.org'
|
|
17
|
-
- run: npm publish
|
|
18
|
-
env:
|
|
19
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
20
|
+
- run: npm publish --provenance --access public
|
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
|
-
|
|
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,78 @@ 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`) |
|
|
114
|
+
|
|
115
|
+
## MCP Server
|
|
116
|
+
|
|
117
|
+
shsu can run as an MCP server for AI assistants.
|
|
118
|
+
|
|
119
|
+
### Claude Code
|
|
120
|
+
|
|
121
|
+
Add to `.mcp.json` in your project root:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"shsu": {
|
|
127
|
+
"command": "npx",
|
|
128
|
+
"args": ["shsu", "mcp"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Claude Desktop
|
|
135
|
+
|
|
136
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"shsu": {
|
|
142
|
+
"command": "npx",
|
|
143
|
+
"args": ["shsu", "mcp"],
|
|
144
|
+
"cwd": "/path/to/your/project"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Cursor
|
|
151
|
+
|
|
152
|
+
Add to `.cursor/mcp.json` in your project:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"shsu": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["shsu", "mcp"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Available MCP Tools
|
|
166
|
+
|
|
167
|
+
- `deploy` - Deploy edge functions
|
|
168
|
+
- `migrate` - Run database migrations
|
|
169
|
+
- `list` - List local and remote functions
|
|
170
|
+
- `invoke` - Invoke a function
|
|
171
|
+
- `restart` - Restart edge-runtime
|
|
172
|
+
- `new` - Create new function from template
|
|
173
|
+
- `config` - Show current configuration
|
|
174
|
+
|
|
175
|
+
## Releasing
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm version patch # or minor/major
|
|
179
|
+
git push --follow-tags
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
GitHub Actions will automatically publish to npm when the tag is pushed.
|
|
88
183
|
|
|
89
184
|
## License
|
|
90
185
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
243
|
-
remotePath
|
|
244
|
-
url
|
|
245
|
-
localPath
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
254
|
-
remotePath
|
|
255
|
-
url
|
|
256
|
-
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
|
|
@@ -308,6 +369,277 @@ async function cmdInit() {
|
|
|
308
369
|
success('Added shsu config to package.json');
|
|
309
370
|
}
|
|
310
371
|
|
|
372
|
+
// ─────────────────────────────────────────────────────────────
|
|
373
|
+
// MCP Server
|
|
374
|
+
// ─────────────────────────────────────────────────────────────
|
|
375
|
+
async function cmdMcp() {
|
|
376
|
+
const tools = [
|
|
377
|
+
{
|
|
378
|
+
name: 'deploy',
|
|
379
|
+
description: 'Deploy edge function(s) to the server. Syncs via rsync and restarts edge-runtime.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
name: { type: 'string', description: 'Function name to deploy. If omitted, deploys all functions.' },
|
|
384
|
+
noRestart: { type: 'boolean', description: 'Skip restarting edge-runtime after deploy.' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'list',
|
|
390
|
+
description: 'List edge functions (both local and remote).',
|
|
391
|
+
inputSchema: { type: 'object', properties: {} },
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'invoke',
|
|
395
|
+
description: 'Invoke an edge function with optional JSON data.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
name: { type: 'string', description: 'Function name to invoke.' },
|
|
400
|
+
data: { type: 'string', description: 'JSON data to send (default: {}).' },
|
|
401
|
+
},
|
|
402
|
+
required: ['name'],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'restart',
|
|
407
|
+
description: 'Restart the edge-runtime container.',
|
|
408
|
+
inputSchema: { type: 'object', properties: {} },
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: 'new',
|
|
412
|
+
description: 'Create a new edge function from template.',
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: 'object',
|
|
415
|
+
properties: {
|
|
416
|
+
name: { type: 'string', description: 'Name for the new function.' },
|
|
417
|
+
},
|
|
418
|
+
required: ['name'],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'config',
|
|
423
|
+
description: 'Get current shsu configuration.',
|
|
424
|
+
inputSchema: { type: 'object', properties: {} },
|
|
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
|
+
|
|
433
|
+
const serverInfo = {
|
|
434
|
+
name: 'shsu',
|
|
435
|
+
version: '0.0.1',
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Helper to write JSON-RPC response
|
|
439
|
+
const respond = (id, result) => {
|
|
440
|
+
const response = { jsonrpc: '2.0', id, result };
|
|
441
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const respondError = (id, code, message) => {
|
|
445
|
+
const response = { jsonrpc: '2.0', id, error: { code, message } };
|
|
446
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Capture output helper
|
|
450
|
+
const captureExec = (cmd) => {
|
|
451
|
+
try {
|
|
452
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
453
|
+
} catch (e) {
|
|
454
|
+
return e.message;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Tool handlers
|
|
459
|
+
const handleTool = async (name, args = {}) => {
|
|
460
|
+
switch (name) {
|
|
461
|
+
case 'deploy': {
|
|
462
|
+
if (!config.server || !config.remotePath) {
|
|
463
|
+
return { content: [{ type: 'text', text: 'Error: server and remotePath must be configured. Run "shsu init" first.' }] };
|
|
464
|
+
}
|
|
465
|
+
const funcName = args.name;
|
|
466
|
+
const noRestart = args.noRestart || false;
|
|
467
|
+
let output = '';
|
|
468
|
+
|
|
469
|
+
if (!funcName) {
|
|
470
|
+
output = captureExec(`rsync -avz --delete --exclude='*.test.ts' --exclude='*.spec.ts' "${config.localPath}/" "${config.server}:${config.remotePath}/"`);
|
|
471
|
+
} else {
|
|
472
|
+
const funcPath = join(config.localPath, funcName);
|
|
473
|
+
if (!existsSync(funcPath)) {
|
|
474
|
+
return { content: [{ type: 'text', text: `Error: Function not found: ${funcPath}` }] };
|
|
475
|
+
}
|
|
476
|
+
output = captureExec(`rsync -avz "${funcPath}/" "${config.server}:${config.remotePath}/${funcName}/"`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!noRestart) {
|
|
480
|
+
output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { content: [{ type: 'text', text: `Deployed${funcName ? ` ${funcName}` : ' all functions'}${noRestart ? ' (no restart)' : ''}\n\n${output}` }] };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
case 'list': {
|
|
487
|
+
if (!config.server || !config.remotePath) {
|
|
488
|
+
return { content: [{ type: 'text', text: 'Error: server and remotePath must be configured.' }] };
|
|
489
|
+
}
|
|
490
|
+
const remote = captureExec(`ssh ${config.server} "ls -1 ${config.remotePath} 2>/dev/null"`) || '(none)';
|
|
491
|
+
let local = '(none)';
|
|
492
|
+
if (existsSync(config.localPath)) {
|
|
493
|
+
const dirs = readdirSync(config.localPath, { withFileTypes: true })
|
|
494
|
+
.filter((d) => d.isDirectory())
|
|
495
|
+
.map((d) => d.name);
|
|
496
|
+
local = dirs.length ? dirs.join('\n') : '(none)';
|
|
497
|
+
}
|
|
498
|
+
return { content: [{ type: 'text', text: `Remote functions:\n${remote}\n\nLocal functions:\n${local}` }] };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
case 'invoke': {
|
|
502
|
+
if (!config.url) {
|
|
503
|
+
return { content: [{ type: 'text', text: 'Error: url must be configured for invoke.' }] };
|
|
504
|
+
}
|
|
505
|
+
if (!args.name) {
|
|
506
|
+
return { content: [{ type: 'text', text: 'Error: function name is required.' }] };
|
|
507
|
+
}
|
|
508
|
+
const data = args.data || '{}';
|
|
509
|
+
const output = captureExec(`curl -s -X POST "${config.url}/functions/v1/${args.name}" -H "Content-Type: application/json" -d '${data}'`);
|
|
510
|
+
return { content: [{ type: 'text', text: output }] };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
case 'restart': {
|
|
514
|
+
if (!config.server) {
|
|
515
|
+
return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
|
|
516
|
+
}
|
|
517
|
+
const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=${config.edgeContainer}')"`);
|
|
518
|
+
return { content: [{ type: 'text', text: `Restarted edge-runtime\n\n${output}` }] };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
case 'new': {
|
|
522
|
+
if (!args.name) {
|
|
523
|
+
return { content: [{ type: 'text', text: 'Error: function name is required.' }] };
|
|
524
|
+
}
|
|
525
|
+
const funcPath = join(config.localPath, args.name);
|
|
526
|
+
if (existsSync(funcPath)) {
|
|
527
|
+
return { content: [{ type: 'text', text: `Error: Function already exists: ${args.name}` }] };
|
|
528
|
+
}
|
|
529
|
+
mkdirSync(funcPath, { recursive: true });
|
|
530
|
+
writeFileSync(
|
|
531
|
+
join(funcPath, 'index.ts'),
|
|
532
|
+
`Deno.serve(async (req) => {
|
|
533
|
+
try {
|
|
534
|
+
const { name } = await req.json()
|
|
535
|
+
|
|
536
|
+
return new Response(
|
|
537
|
+
JSON.stringify({ message: \`Hello \${name}!\` }),
|
|
538
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
539
|
+
)
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return new Response(
|
|
542
|
+
JSON.stringify({ error: error.message }),
|
|
543
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
})
|
|
547
|
+
`
|
|
548
|
+
);
|
|
549
|
+
return { content: [{ type: 'text', text: `Created ${funcPath}/index.ts` }] };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case 'config': {
|
|
553
|
+
return {
|
|
554
|
+
content: [{
|
|
555
|
+
type: 'text',
|
|
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}`,
|
|
557
|
+
}],
|
|
558
|
+
};
|
|
559
|
+
}
|
|
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
|
+
|
|
591
|
+
default:
|
|
592
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// Process incoming messages
|
|
597
|
+
const rl = createInterface({ input: process.stdin });
|
|
598
|
+
|
|
599
|
+
for await (const line of rl) {
|
|
600
|
+
let msg;
|
|
601
|
+
try {
|
|
602
|
+
msg = JSON.parse(line);
|
|
603
|
+
} catch {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const { id, method, params } = msg;
|
|
608
|
+
|
|
609
|
+
switch (method) {
|
|
610
|
+
case 'initialize':
|
|
611
|
+
respond(id, {
|
|
612
|
+
protocolVersion: '2024-11-05',
|
|
613
|
+
capabilities: { tools: {} },
|
|
614
|
+
serverInfo,
|
|
615
|
+
});
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case 'notifications/initialized':
|
|
619
|
+
// No response needed for notifications
|
|
620
|
+
break;
|
|
621
|
+
|
|
622
|
+
case 'tools/list':
|
|
623
|
+
respond(id, { tools });
|
|
624
|
+
break;
|
|
625
|
+
|
|
626
|
+
case 'tools/call':
|
|
627
|
+
try {
|
|
628
|
+
const result = await handleTool(params.name, params.arguments || {});
|
|
629
|
+
respond(id, result);
|
|
630
|
+
} catch (e) {
|
|
631
|
+
respond(id, { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true });
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
|
|
635
|
+
default:
|
|
636
|
+
if (id !== undefined) {
|
|
637
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
311
643
|
function cmdHelp() {
|
|
312
644
|
console.log(`
|
|
313
645
|
${c.blue('shsu')} - Self-Hosted Supabase Utilities
|
|
@@ -323,6 +655,8 @@ ${c.yellow('Commands:')}
|
|
|
323
655
|
- With name: deploy single function
|
|
324
656
|
Options: --no-restart
|
|
325
657
|
|
|
658
|
+
migrate Run SQL migrations on database
|
|
659
|
+
|
|
326
660
|
logs [filter] Stream edge-runtime logs
|
|
327
661
|
- Optional filter string
|
|
328
662
|
|
|
@@ -336,10 +670,13 @@ ${c.yellow('Commands:')}
|
|
|
336
670
|
|
|
337
671
|
env Show current configuration
|
|
338
672
|
|
|
673
|
+
mcp Start MCP server (for AI assistants)
|
|
674
|
+
|
|
339
675
|
${c.yellow('Examples:')}
|
|
340
676
|
shsu init
|
|
341
677
|
shsu deploy
|
|
342
678
|
shsu deploy hello-world --no-restart
|
|
679
|
+
shsu migrate
|
|
343
680
|
shsu logs hello-world
|
|
344
681
|
shsu invoke hello-world '{"name":"Stefan"}'
|
|
345
682
|
shsu new my-function
|
|
@@ -379,6 +716,11 @@ async function main() {
|
|
|
379
716
|
case 'restart':
|
|
380
717
|
await cmdRestart();
|
|
381
718
|
break;
|
|
719
|
+
case 'migrate':
|
|
720
|
+
case 'migration':
|
|
721
|
+
case 'migrations':
|
|
722
|
+
await cmdMigrate();
|
|
723
|
+
break;
|
|
382
724
|
case 'new':
|
|
383
725
|
case 'create':
|
|
384
726
|
await cmdNew(args[1]);
|
|
@@ -386,6 +728,9 @@ async function main() {
|
|
|
386
728
|
case 'init':
|
|
387
729
|
await cmdInit();
|
|
388
730
|
break;
|
|
731
|
+
case 'mcp':
|
|
732
|
+
await cmdMcp();
|
|
733
|
+
break;
|
|
389
734
|
case 'env':
|
|
390
735
|
case 'config':
|
|
391
736
|
cmdEnv();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shsu",
|
|
3
|
-
"version": "0.0.
|
|
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"
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"url": "https://github.com/YUZU-Hub/shsu/issues"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/YUZU-Hub/shsu#readme"
|
|
34
|
-
}
|
|
34
|
+
}
|