shsu 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node bin/shsu.mjs:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,19 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: '20'
16
+ registry-url: 'https://registry.npmjs.org'
17
+ - run: npm publish
18
+ env:
19
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # shsu
2
+
3
+ **S**elf-**H**osted **S**upabase **U**tilities
4
+
5
+ Deploy and manage Supabase Edge Functions on Coolify-hosted Supabase.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g shsu
11
+ # or use directly
12
+ npx shsu
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Run the init command to configure your project:
18
+
19
+ ```bash
20
+ npx shsu init
21
+ ```
22
+
23
+ This adds config to your `package.json`:
24
+
25
+ ```json
26
+ {
27
+ "shsu": {
28
+ "server": "root@your-coolify-server",
29
+ "remotePath": "/data/coolify/services/YOUR_SERVICE_ID/volumes/functions",
30
+ "url": "https://your-supabase.example.com"
31
+ }
32
+ }
33
+ ```
34
+
35
+ Find your `remotePath` by running on your server:
36
+
37
+ ```bash
38
+ docker inspect $(docker ps -q --filter 'name=edge') | grep -A 5 "Mounts"
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ # Configure project
45
+ shsu init
46
+
47
+ # Show current configuration
48
+ shsu env
49
+
50
+ # Deploy all functions
51
+ shsu deploy
52
+
53
+ # Deploy single function
54
+ shsu deploy hello-world
55
+
56
+ # Deploy without restarting edge-runtime
57
+ shsu deploy hello-world --no-restart
58
+
59
+ # Stream logs
60
+ shsu logs
61
+
62
+ # Stream logs filtered by function name
63
+ shsu logs hello-world
64
+
65
+ # List local and remote functions
66
+ shsu list
67
+
68
+ # Invoke a function
69
+ shsu invoke hello-world '{"name":"Stefan"}'
70
+
71
+ # Create new function from template
72
+ shsu new my-function
73
+
74
+ # Restart edge-runtime
75
+ shsu restart
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ Config is read from `package.json` "shsu" key. Environment variables override package.json values.
81
+
82
+ | Key / Env Var | Required | Description |
83
+ |---------------|----------|-------------|
84
+ | `server` / `SHSU_SERVER` | Yes | SSH host (e.g., `root@your-server.com`) |
85
+ | `remotePath` / `SHSU_REMOTE_PATH` | Yes | Remote path to functions directory |
86
+ | `url` / `SHSU_URL` | For `invoke` | Supabase URL |
87
+ | `localPath` / `SHSU_LOCAL_PATH` | No | Local functions path (default: `./supabase/functions`) |
88
+
89
+ ## License
90
+
91
+ MIT
package/bin/shsu.mjs ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, execSync } from 'node:child_process';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+
8
+ // ─────────────────────────────────────────────────────────────
9
+ // Configuration (package.json + environment variables)
10
+ // ─────────────────────────────────────────────────────────────
11
+ function loadConfig() {
12
+ let pkgConfig = {};
13
+
14
+ // Try to load from package.json
15
+ const pkgPath = join(process.cwd(), 'package.json');
16
+ if (existsSync(pkgPath)) {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
19
+ pkgConfig = pkg.shsu || {};
20
+ } catch (e) {
21
+ // Ignore parse errors
22
+ }
23
+ }
24
+
25
+ // Env vars override package.json
26
+ return {
27
+ server: process.env.SHSU_SERVER || pkgConfig.server,
28
+ remotePath: process.env.SHSU_REMOTE_PATH || pkgConfig.remotePath,
29
+ localPath: process.env.SHSU_LOCAL_PATH || pkgConfig.localPath || './supabase/functions',
30
+ url: process.env.SHSU_URL || pkgConfig.url,
31
+ };
32
+ }
33
+
34
+ const config = loadConfig();
35
+
36
+ // ─────────────────────────────────────────────────────────────
37
+ // Colors
38
+ // ─────────────────────────────────────────────────────────────
39
+ const c = {
40
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
41
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
42
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
43
+ blue: (s) => `\x1b[34m${s}\x1b[0m`,
44
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
45
+ };
46
+
47
+ const info = (msg) => console.log(`${c.blue('▸')} ${msg}`);
48
+ const success = (msg) => console.log(`${c.green('✓')} ${msg}`);
49
+ const error = (msg) => {
50
+ console.error(`${c.red('✗')} ${msg}`);
51
+ process.exit(1);
52
+ };
53
+
54
+ // ─────────────────────────────────────────────────────────────
55
+ // Helpers
56
+ // ─────────────────────────────────────────────────────────────
57
+ function requireVar(name) {
58
+ if (!config[name]) {
59
+ error(`Missing required env var: SHSU_${name.toUpperCase()} (see 'shsu env')`);
60
+ }
61
+ }
62
+
63
+ function requireServer() {
64
+ requireVar('server');
65
+ requireVar('remotePath');
66
+ }
67
+
68
+ function run(cmd, args, options = {}) {
69
+ return new Promise((resolve, reject) => {
70
+ const proc = spawn(cmd, args, { stdio: 'inherit', ...options });
71
+ proc.on('close', (code) => {
72
+ if (code === 0) resolve();
73
+ else reject(new Error(`Command failed with code ${code}`));
74
+ });
75
+ proc.on('error', reject);
76
+ });
77
+ }
78
+
79
+ function runSync(cmd) {
80
+ try {
81
+ return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
82
+ } catch (e) {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function getEdgeContainer() {
88
+ return runSync(`ssh ${config.server} "docker ps -q --filter 'name=edge'"`);
89
+ }
90
+
91
+ // ─────────────────────────────────────────────────────────────
92
+ // Commands
93
+ // ─────────────────────────────────────────────────────────────
94
+
95
+ async function cmdDeploy(funcName, noRestart = false) {
96
+ requireServer();
97
+
98
+ if (!funcName) {
99
+ info('Deploying all functions...');
100
+ await run('rsync', [
101
+ '-avz', '--delete',
102
+ '--exclude=*.test.ts',
103
+ '--exclude=*.spec.ts',
104
+ `${config.localPath}/`,
105
+ `${config.server}:${config.remotePath}/`,
106
+ ]);
107
+ } else {
108
+ const funcPath = join(config.localPath, funcName);
109
+ if (!existsSync(funcPath)) {
110
+ error(`Function not found: ${funcPath}`);
111
+ }
112
+ info(`Deploying ${funcName}...`);
113
+ await run('rsync', [
114
+ '-avz',
115
+ `${funcPath}/`,
116
+ `${config.server}:${config.remotePath}/${funcName}/`,
117
+ ]);
118
+ }
119
+
120
+ if (!noRestart) {
121
+ info('Restarting edge-runtime...');
122
+ await run('ssh', [
123
+ config.server,
124
+ `docker restart $(docker ps -q --filter 'name=edge')`,
125
+ ], { stdio: ['inherit', 'pipe', 'inherit'] });
126
+ success(`Deployed${funcName ? ` ${funcName}` : ''}`);
127
+ } else {
128
+ success(`Synced${funcName ? ` ${funcName}` : ''} (no restart)`);
129
+ }
130
+ }
131
+
132
+ async function cmdLogs(filter, lines = 100) {
133
+ requireServer();
134
+
135
+ info(`Streaming logs${filter ? ` (filter: ${filter})` : ''}... (Ctrl+C to exit)`);
136
+
137
+ const sshArgs = [
138
+ config.server,
139
+ `docker logs -f $(docker ps -q --filter 'name=edge') --tail ${lines} 2>&1`,
140
+ ];
141
+
142
+ if (filter) {
143
+ const ssh = spawn('ssh', sshArgs, { stdio: ['inherit', 'pipe', 'inherit'] });
144
+ const grep = spawn('grep', ['--line-buffered', '-i', filter], {
145
+ stdio: ['pipe', 'inherit', 'inherit'],
146
+ });
147
+ ssh.stdout.pipe(grep.stdin);
148
+
149
+ await new Promise((resolve) => {
150
+ ssh.on('close', resolve);
151
+ grep.on('close', resolve);
152
+ });
153
+ } else {
154
+ await run('ssh', sshArgs);
155
+ }
156
+ }
157
+
158
+ async function cmdList() {
159
+ requireServer();
160
+
161
+ info('Remote functions:');
162
+ const remote = runSync(`ssh ${config.server} "ls -1 ${config.remotePath} 2>/dev/null"`);
163
+ if (remote) {
164
+ remote.split('\n').filter(Boolean).forEach((f) => console.log(` • ${f}`));
165
+ }
166
+
167
+ console.log('');
168
+ info('Local functions:');
169
+ if (existsSync(config.localPath)) {
170
+ readdirSync(config.localPath, { withFileTypes: true })
171
+ .filter((d) => d.isDirectory())
172
+ .forEach((d) => console.log(` • ${d.name}`));
173
+ }
174
+ }
175
+
176
+ async function cmdInvoke(funcName, data = '{}') {
177
+ requireVar('url');
178
+
179
+ if (!funcName) {
180
+ error('Usage: shsu invoke <function-name> [json-data]');
181
+ }
182
+
183
+ info(`Invoking ${funcName}...`);
184
+ await run('curl', [
185
+ '-s', '-X', 'POST',
186
+ `${config.url}/functions/v1/${funcName}`,
187
+ '-H', 'Content-Type: application/json',
188
+ '-d', data,
189
+ ]);
190
+ console.log('');
191
+ }
192
+
193
+ async function cmdRestart() {
194
+ requireServer();
195
+
196
+ info('Restarting edge-runtime...');
197
+ await run('ssh', [
198
+ config.server,
199
+ `docker restart $(docker ps -q --filter 'name=edge')`,
200
+ ], { stdio: ['inherit', 'pipe', 'inherit'] });
201
+ success('Restarted');
202
+ }
203
+
204
+ async function cmdNew(funcName) {
205
+ if (!funcName) {
206
+ error('Usage: shsu new <function-name>');
207
+ }
208
+
209
+ const funcPath = join(config.localPath, funcName);
210
+ if (existsSync(funcPath)) {
211
+ error(`Function already exists: ${funcName}`);
212
+ }
213
+
214
+ mkdirSync(funcPath, { recursive: true });
215
+ writeFileSync(
216
+ join(funcPath, 'index.ts'),
217
+ `Deno.serve(async (req) => {
218
+ try {
219
+ const { name } = await req.json()
220
+
221
+ return new Response(
222
+ JSON.stringify({ message: \`Hello \${name}!\` }),
223
+ { headers: { "Content-Type": "application/json" } }
224
+ )
225
+ } catch (error) {
226
+ return new Response(
227
+ JSON.stringify({ error: error.message }),
228
+ { status: 400, headers: { "Content-Type": "application/json" } }
229
+ )
230
+ }
231
+ })
232
+ `
233
+ );
234
+
235
+ success(`Created ${funcPath}/index.ts`);
236
+ }
237
+
238
+ function cmdEnv() {
239
+ console.log(`
240
+ ${c.yellow('Configuration (package.json "shsu" key or environment variables):')}
241
+
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
250
+
251
+ ${c.yellow('Current values:')}
252
+
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}
257
+
258
+ ${c.dim('Run "shsu init" to configure via prompts.')}
259
+ `);
260
+ }
261
+
262
+ async function cmdInit() {
263
+ const pkgPath = join(process.cwd(), 'package.json');
264
+
265
+ if (!existsSync(pkgPath)) {
266
+ error('No package.json found. Run "npm init" first.');
267
+ }
268
+
269
+ const rl = createInterface({
270
+ input: process.stdin,
271
+ output: process.stdout,
272
+ });
273
+
274
+ const ask = (question, defaultVal) =>
275
+ new Promise((resolve) => {
276
+ const prompt = defaultVal ? `${question} [${defaultVal}]: ` : `${question}: `;
277
+ rl.question(prompt, (answer) => {
278
+ resolve(answer.trim() || defaultVal || '');
279
+ });
280
+ });
281
+
282
+ console.log(`\n${c.blue('shsu init')} - Configure project\n`);
283
+
284
+ const server = await ask('Server (e.g. root@server.com)', config.server);
285
+ const remotePath = await ask('Remote path to functions', config.remotePath);
286
+ const url = await ask('Supabase URL', config.url);
287
+ const localPath = await ask('Local functions path', config.localPath || './supabase/functions');
288
+
289
+ rl.close();
290
+
291
+ // Read and update package.json
292
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
293
+ pkg.shsu = {
294
+ server: server || undefined,
295
+ remotePath: remotePath || undefined,
296
+ url: url || undefined,
297
+ localPath: localPath !== './supabase/functions' ? localPath : undefined,
298
+ };
299
+
300
+ // Remove undefined values
301
+ Object.keys(pkg.shsu).forEach((key) => {
302
+ if (pkg.shsu[key] === undefined) delete pkg.shsu[key];
303
+ });
304
+
305
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
306
+
307
+ console.log('');
308
+ success('Added shsu config to package.json');
309
+ }
310
+
311
+ function cmdHelp() {
312
+ console.log(`
313
+ ${c.blue('shsu')} - Self-Hosted Supabase Utilities
314
+
315
+ ${c.yellow('Usage:')}
316
+ shsu <command> [options]
317
+
318
+ ${c.yellow('Commands:')}
319
+ init Configure shsu for this project
320
+
321
+ deploy [name] Deploy function(s) to server
322
+ - No args: deploy all functions
323
+ - With name: deploy single function
324
+ Options: --no-restart
325
+
326
+ logs [filter] Stream edge-runtime logs
327
+ - Optional filter string
328
+
329
+ list List functions (local and remote)
330
+
331
+ invoke <n> [json] Invoke a function
332
+
333
+ restart Restart edge-runtime container
334
+
335
+ new <name> Create new function from template
336
+
337
+ env Show current configuration
338
+
339
+ ${c.yellow('Examples:')}
340
+ shsu init
341
+ shsu deploy
342
+ shsu deploy hello-world --no-restart
343
+ shsu logs hello-world
344
+ shsu invoke hello-world '{"name":"Stefan"}'
345
+ shsu new my-function
346
+
347
+ ${c.yellow('Setup:')}
348
+ Run 'shsu init' to configure, or set values in package.json "shsu" key.
349
+ `);
350
+ }
351
+
352
+ // ─────────────────────────────────────────────────────────────
353
+ // Main
354
+ // ─────────────────────────────────────────────────────────────
355
+ async function main() {
356
+ const args = process.argv.slice(2);
357
+ const cmd = args[0] || 'help';
358
+
359
+ try {
360
+ switch (cmd) {
361
+ case 'deploy': {
362
+ const noRestart = args.includes('--no-restart');
363
+ const funcName = args[1] === '--no-restart' ? args[2] : args[1];
364
+ await cmdDeploy(funcName, noRestart);
365
+ break;
366
+ }
367
+ case 'logs':
368
+ case 'log':
369
+ await cmdLogs(args[1], args[2] || 100);
370
+ break;
371
+ case 'list':
372
+ case 'ls':
373
+ await cmdList();
374
+ break;
375
+ case 'invoke':
376
+ case 'call':
377
+ await cmdInvoke(args[1], args[2]);
378
+ break;
379
+ case 'restart':
380
+ await cmdRestart();
381
+ break;
382
+ case 'new':
383
+ case 'create':
384
+ await cmdNew(args[1]);
385
+ break;
386
+ case 'init':
387
+ await cmdInit();
388
+ break;
389
+ case 'env':
390
+ case 'config':
391
+ cmdEnv();
392
+ break;
393
+ case 'help':
394
+ case '-h':
395
+ case '--help':
396
+ cmdHelp();
397
+ break;
398
+ default:
399
+ error(`Unknown command: ${cmd} (try 'shsu help')`);
400
+ }
401
+ } catch (e) {
402
+ error(e.message);
403
+ }
404
+ }
405
+
406
+ main();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "shsu",
3
+ "version": "0.0.1",
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
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "bin": {
9
+ "shsu": "./bin/shsu.mjs"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/YUZU-Hub/shsu.git"
14
+ },
15
+ "keywords": [
16
+ "supabase",
17
+ "edge-functions",
18
+ "deno",
19
+ "coolify",
20
+ "self-hosted",
21
+ "deploy",
22
+ "cli",
23
+ "serverless",
24
+ "docker",
25
+ "rsync"
26
+ ],
27
+ "author": "Stefan Lange-Hegermann <stefan@yuzuhub.com>",
28
+ "license": "MIT",
29
+ "type": "module",
30
+ "bugs": {
31
+ "url": "https://github.com/YUZU-Hub/shsu/issues"
32
+ },
33
+ "homepage": "https://github.com/YUZU-Hub/shsu#readme"
34
+ }