supabase-selfhosted-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1329 -0
- package/package.json +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/lib/config.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var CONFIG_DIR = path.join(os.homedir(), ".supabase-selfhosted-cli");
|
|
11
|
+
var DEFAULT_PROFILE = "default";
|
|
12
|
+
function profilePath(profile) {
|
|
13
|
+
return path.join(CONFIG_DIR, "profiles", `${profile}.json`);
|
|
14
|
+
}
|
|
15
|
+
function projectLinkPath(cwd) {
|
|
16
|
+
return path.join(cwd, ".supabase-selfhosted-cli.json");
|
|
17
|
+
}
|
|
18
|
+
function ensureConfigDir() {
|
|
19
|
+
fs.mkdirSync(path.join(CONFIG_DIR, "profiles"), { recursive: true, mode: 448 });
|
|
20
|
+
}
|
|
21
|
+
function saveConfig(config) {
|
|
22
|
+
ensureConfigDir();
|
|
23
|
+
const filePath = profilePath(config.profile);
|
|
24
|
+
fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
|
|
25
|
+
`, { mode: 384 });
|
|
26
|
+
}
|
|
27
|
+
function normalizeConfig(raw) {
|
|
28
|
+
return {
|
|
29
|
+
...raw,
|
|
30
|
+
target: raw.target ?? "ssh"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function loadConfig(profile = DEFAULT_PROFILE) {
|
|
34
|
+
const filePath = profilePath(profile);
|
|
35
|
+
if (!fs.existsSync(filePath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
39
|
+
return normalizeConfig(raw);
|
|
40
|
+
}
|
|
41
|
+
function deleteConfig(profile = DEFAULT_PROFILE) {
|
|
42
|
+
const filePath = profilePath(profile);
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
fs.unlinkSync(filePath);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function listProfiles() {
|
|
50
|
+
const profilesDir = path.join(CONFIG_DIR, "profiles");
|
|
51
|
+
if (!fs.existsSync(profilesDir)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return fs.readdirSync(profilesDir).filter((name) => name.endsWith(".json")).map((name) => name.replace(/\.json$/, ""));
|
|
55
|
+
}
|
|
56
|
+
function resolveProfile(cwd, explicitProfile) {
|
|
57
|
+
if (explicitProfile) {
|
|
58
|
+
return explicitProfile;
|
|
59
|
+
}
|
|
60
|
+
const linkPath = projectLinkPath(cwd);
|
|
61
|
+
if (fs.existsSync(linkPath)) {
|
|
62
|
+
const link = JSON.parse(fs.readFileSync(linkPath, "utf8"));
|
|
63
|
+
return link.profile;
|
|
64
|
+
}
|
|
65
|
+
return DEFAULT_PROFILE;
|
|
66
|
+
}
|
|
67
|
+
function saveProjectLink(cwd, profile) {
|
|
68
|
+
const linkPath = projectLinkPath(cwd);
|
|
69
|
+
const link = { profile };
|
|
70
|
+
fs.writeFileSync(linkPath, `${JSON.stringify(link, null, 2)}
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
function buildDbUrl(config, kind) {
|
|
74
|
+
const { tenantId, password: password2, host, pushPort, typesPort, database } = config.database;
|
|
75
|
+
const port = kind === "push" ? pushPort : typesPort;
|
|
76
|
+
const encodedPassword = encodeURIComponent(password2);
|
|
77
|
+
return `postgresql://postgres.${tenantId}:${encodedPassword}@${host}:${port}/${database}`;
|
|
78
|
+
}
|
|
79
|
+
function maskSecret(value) {
|
|
80
|
+
if (value.length <= 4) {
|
|
81
|
+
return "****";
|
|
82
|
+
}
|
|
83
|
+
return `${value.slice(0, 2)}${"*".repeat(Math.min(value.length - 4, 12))}${value.slice(-2)}`;
|
|
84
|
+
}
|
|
85
|
+
function formatConfigSummary(config) {
|
|
86
|
+
const targetLabel = config.target === "local" ? "Local (Docker / filesystem)" : "Remote (SSH)";
|
|
87
|
+
const lines = [
|
|
88
|
+
`Profile: ${config.profile}`,
|
|
89
|
+
`Target: ${targetLabel}`
|
|
90
|
+
];
|
|
91
|
+
if (config.target === "ssh") {
|
|
92
|
+
lines.push(
|
|
93
|
+
`SSH: ${config.ssh.user}@${config.ssh.host}`,
|
|
94
|
+
`SSH password: ${maskSecret(config.ssh.password)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
lines.push(
|
|
98
|
+
`Local functions: ${config.functions.localPath}`,
|
|
99
|
+
`Functions destination: ${config.functions.remotePath}`,
|
|
100
|
+
`DB tenant: postgres.${config.database.tenantId}`,
|
|
101
|
+
`DB password: ${maskSecret(config.database.password)}`,
|
|
102
|
+
`DB push port: ${config.database.pushPort}`,
|
|
103
|
+
`DB types port: ${config.database.typesPort}`,
|
|
104
|
+
`Restart after deploy: ${config.deploy.restartAfterDeploy ? "yes" : "no"}`,
|
|
105
|
+
`Restart command: ${config.deploy.restartCommand || "(not set)"}`
|
|
106
|
+
);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/lib/ui.ts
|
|
111
|
+
import os2 from "os";
|
|
112
|
+
import process2 from "process";
|
|
113
|
+
var VERSION = "0.1.1";
|
|
114
|
+
var BRAND_NAME = "Velocilabs";
|
|
115
|
+
var BRAND_URL = "https://velocilabs.com";
|
|
116
|
+
var SPYKE_URL = "https://spyke.social";
|
|
117
|
+
var TAGLINE = "Deploy, migrate, and sync self-hosted Supabase.";
|
|
118
|
+
var REPO_URL = "https://github.com/spykesocial/supabase-selfhosted-cli";
|
|
119
|
+
var ESC = "\x1B";
|
|
120
|
+
var colors = {
|
|
121
|
+
green: `${ESC}[0;32m`,
|
|
122
|
+
blue: `${ESC}[1;34m`,
|
|
123
|
+
cyan: `${ESC}[0;36m`,
|
|
124
|
+
yellow: `${ESC}[0;33m`,
|
|
125
|
+
purple: `${ESC}[0;35m`,
|
|
126
|
+
red: `${ESC}[0;31m`,
|
|
127
|
+
gray: `${ESC}[0;90m`,
|
|
128
|
+
nc: `${ESC}[0m`
|
|
129
|
+
};
|
|
130
|
+
var VELOCILABS_PURPLE_RGB = "219;156;253";
|
|
131
|
+
var icons = {
|
|
132
|
+
success: "\u2713",
|
|
133
|
+
error: "\u263B",
|
|
134
|
+
warning: "\u25CE",
|
|
135
|
+
arrow: "\u27A4",
|
|
136
|
+
list: "\u2022",
|
|
137
|
+
dryRun: "\u2192",
|
|
138
|
+
review: "\u261E",
|
|
139
|
+
info: "\u2139"
|
|
140
|
+
};
|
|
141
|
+
function isColorEnabled() {
|
|
142
|
+
if (process2.env.NO_COLOR) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return Boolean(process2.stdout.isTTY);
|
|
146
|
+
}
|
|
147
|
+
function hideCursor() {
|
|
148
|
+
if (process2.stderr.isTTY) {
|
|
149
|
+
process2.stderr.write("\x1B[?25l");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function showCursor() {
|
|
153
|
+
if (process2.stderr.isTTY) {
|
|
154
|
+
process2.stderr.write("\x1B[?25h");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function paint(text, color) {
|
|
158
|
+
if (!isColorEnabled()) {
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
return `${colors[color]}${text}${colors.nc}`;
|
|
162
|
+
}
|
|
163
|
+
function paintBrand(text) {
|
|
164
|
+
if (!isColorEnabled()) {
|
|
165
|
+
return text;
|
|
166
|
+
}
|
|
167
|
+
const depth = process2.stdout.getColorDepth?.() ?? 4;
|
|
168
|
+
if (depth >= 8) {
|
|
169
|
+
return `${ESC}[38;2;${VELOCILABS_PURPLE_RGB}m${text}${colors.nc}`;
|
|
170
|
+
}
|
|
171
|
+
return paint(text, "purple");
|
|
172
|
+
}
|
|
173
|
+
var LIGHTNING_MARK = [
|
|
174
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2580",
|
|
175
|
+
" \u2588\u2588\u2588\u2588\u2588",
|
|
176
|
+
" \u2588\u2588\u2588\u2588\u2580",
|
|
177
|
+
"\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580",
|
|
178
|
+
"\u2580\u2580\u2580\u2588\u2588\u2588\u2580",
|
|
179
|
+
" \u2584\u2588\u2588\u2580",
|
|
180
|
+
" \u2588\u2580",
|
|
181
|
+
" \u2580"
|
|
182
|
+
];
|
|
183
|
+
var WORDMARK = [
|
|
184
|
+
"__ __ _ _ _ _ ",
|
|
185
|
+
"\\ \\ / /__| | ___ ___(_) | __ _| |__ ___ ",
|
|
186
|
+
" \\ \\ / / _ \\ |/ _ \\ / __| | |/ _` | '_ \\/ __|",
|
|
187
|
+
" \\ V / __/ | (_) | (__| | | (_| | |_) \\__ \\",
|
|
188
|
+
" \\_/ \\___|_|\\___/ \\___|_|_|\\__,_|_.__/|___/"
|
|
189
|
+
];
|
|
190
|
+
function combineLockupLines(markLines, textLines, gap = 2, textOffset = 0) {
|
|
191
|
+
const markWidth = Math.max(...markLines.map((line) => line.length));
|
|
192
|
+
const rows = Math.max(markLines.length, textLines.length + textOffset);
|
|
193
|
+
const combined = [];
|
|
194
|
+
for (let index = 0; index < rows; index += 1) {
|
|
195
|
+
const mark = markLines[index] ?? "";
|
|
196
|
+
const textIndex = index - textOffset;
|
|
197
|
+
const text = textIndex >= 0 && textIndex < textLines.length ? textLines[textIndex] : "";
|
|
198
|
+
combined.push(`${mark.padEnd(markWidth + gap)}${text}`.trimEnd());
|
|
199
|
+
}
|
|
200
|
+
return combined;
|
|
201
|
+
}
|
|
202
|
+
function showBrandBanner() {
|
|
203
|
+
const useColor = isColorEnabled();
|
|
204
|
+
const brand = (text) => useColor ? paintBrand(text) : text;
|
|
205
|
+
const link = (text) => useColor ? paint(text, "blue") : text;
|
|
206
|
+
const meta = (text) => useColor ? paint(text, "gray") : text;
|
|
207
|
+
const textOffset = Math.floor((LIGHTNING_MARK.length - WORDMARK.length) / 2);
|
|
208
|
+
const lockup = combineLockupLines(LIGHTNING_MARK, WORDMARK, 2, textOffset);
|
|
209
|
+
return [
|
|
210
|
+
...lockup.map((line) => brand(line)),
|
|
211
|
+
link(REPO_URL),
|
|
212
|
+
`${link(BRAND_URL)} ${meta("\xB7")} ${link(SPYKE_URL)} ${meta(`\xB7 ${TAGLINE}`)}`,
|
|
213
|
+
""
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
216
|
+
function showMenuOption(number2, label, description, selected) {
|
|
217
|
+
const text = `${label.padEnd(12)} ${description}`;
|
|
218
|
+
if (selected) {
|
|
219
|
+
return `${paintBrand(`${icons.arrow} ${number2}. ${text}`)}`;
|
|
220
|
+
}
|
|
221
|
+
return ` ${number2}. ${text}`;
|
|
222
|
+
}
|
|
223
|
+
function logInfo(message) {
|
|
224
|
+
console.log(paint(message, "blue"));
|
|
225
|
+
}
|
|
226
|
+
function logSuccess(message) {
|
|
227
|
+
console.log(` ${paintBrand(icons.success)} ${message}`);
|
|
228
|
+
}
|
|
229
|
+
function logWarning(message) {
|
|
230
|
+
console.log(paint(message, "yellow"));
|
|
231
|
+
}
|
|
232
|
+
function logError(message) {
|
|
233
|
+
console.error(`${paint(icons.error, "yellow")} ${message}`);
|
|
234
|
+
}
|
|
235
|
+
function logReview(message) {
|
|
236
|
+
console.log(`${icons.review} ${message}`);
|
|
237
|
+
}
|
|
238
|
+
function printSummaryBlock(heading, ...details) {
|
|
239
|
+
const width = Math.min(process2.stdout.columns ?? 70, 70);
|
|
240
|
+
const divider = "=".repeat(width);
|
|
241
|
+
console.log("");
|
|
242
|
+
console.log(divider);
|
|
243
|
+
console.log(paint(heading, "blue"));
|
|
244
|
+
for (const detail of details) {
|
|
245
|
+
if (detail) {
|
|
246
|
+
console.log(formatStyledConfigSummary([detail]));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
console.log(divider);
|
|
250
|
+
console.log("");
|
|
251
|
+
}
|
|
252
|
+
function formatVersion() {
|
|
253
|
+
const profiles = listProfiles();
|
|
254
|
+
return [
|
|
255
|
+
"",
|
|
256
|
+
`${paintBrand(BRAND_NAME)} ${paint("supabase-selfhosted-cli", "gray")} ${VERSION}`,
|
|
257
|
+
paint(BRAND_URL, "blue"),
|
|
258
|
+
`Node: ${process2.version}`,
|
|
259
|
+
`Platform: ${process2.platform} ${os2.arch()}`,
|
|
260
|
+
`Config: ${CONFIG_DIR}`,
|
|
261
|
+
`Profiles: ${profiles.length > 0 ? profiles.join(", ") : "(none)"}`,
|
|
262
|
+
`Shell: ${process2.env.SHELL ?? "unknown"}`,
|
|
263
|
+
""
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
function showVersion() {
|
|
267
|
+
console.log(formatVersion());
|
|
268
|
+
}
|
|
269
|
+
var COMMAND_ENTRIES = [
|
|
270
|
+
{ command: "supabase-selfhosted-cli", description: "Main menu" },
|
|
271
|
+
{ command: "setup", description: "Interactive setup wizard" },
|
|
272
|
+
{ command: "settings", description: "View, update, or delete stored credentials" },
|
|
273
|
+
{ command: "functions deploy", description: "Deploy local supabase/functions" },
|
|
274
|
+
{ command: "db push", description: "Push local migrations with supabase db push" },
|
|
275
|
+
{ command: "gen types", description: "Generate TypeScript types from remote DB" }
|
|
276
|
+
];
|
|
277
|
+
var EXAMPLE_ENTRIES = [
|
|
278
|
+
{ command: "setup -p staging", description: "Create a named profile" },
|
|
279
|
+
{ command: "functions deploy --prune", description: "Deploy and remove remote-only files" },
|
|
280
|
+
{ command: "functions deploy --no-restart", description: "Deploy without restarting runtime" },
|
|
281
|
+
{ command: "db push --debug", description: "Push migrations with supabase debug logs" },
|
|
282
|
+
{ command: "gen types -o src/types/database.ts", description: "Write types to a custom path" }
|
|
283
|
+
];
|
|
284
|
+
function formatHelpSection(title, entries) {
|
|
285
|
+
const lines = [paint(title, "blue")];
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
const command = entry.command.padEnd(28);
|
|
288
|
+
lines.push(` ${paintBrand(command)} ${entry.description}`);
|
|
289
|
+
}
|
|
290
|
+
lines.push("");
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
|
293
|
+
function formatHelp() {
|
|
294
|
+
return [
|
|
295
|
+
showBrandBanner(),
|
|
296
|
+
formatHelpSection("COMMANDS", COMMAND_ENTRIES),
|
|
297
|
+
formatHelpSection("EXAMPLES", EXAMPLE_ENTRIES),
|
|
298
|
+
paint("OPTIONS", "blue"),
|
|
299
|
+
` ${paintBrand("-p, --profile <name>".padEnd(28))} Use a stored profile`,
|
|
300
|
+
` ${paintBrand("-h, --help".padEnd(28))} Show this help message`,
|
|
301
|
+
` ${paintBrand("-V, --version".padEnd(28))} Show version information`,
|
|
302
|
+
""
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
function showHelp() {
|
|
306
|
+
console.log(formatHelp());
|
|
307
|
+
}
|
|
308
|
+
var InlineSpinner = class {
|
|
309
|
+
constructor(message) {
|
|
310
|
+
this.message = message;
|
|
311
|
+
}
|
|
312
|
+
message;
|
|
313
|
+
timer = null;
|
|
314
|
+
frame = 0;
|
|
315
|
+
frames = "|/-\\";
|
|
316
|
+
start() {
|
|
317
|
+
if (!process2.stderr.isTTY) {
|
|
318
|
+
console.error(this.message);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.stop();
|
|
322
|
+
this.frame = 0;
|
|
323
|
+
this.timer = setInterval(() => {
|
|
324
|
+
const char = this.frames[this.frame % this.frames.length];
|
|
325
|
+
this.frame += 1;
|
|
326
|
+
process2.stderr.write(
|
|
327
|
+
`\r${paintBrand(char)} ${truncateMessage(this.message)}`
|
|
328
|
+
);
|
|
329
|
+
}, 80);
|
|
330
|
+
}
|
|
331
|
+
stop() {
|
|
332
|
+
if (this.timer) {
|
|
333
|
+
clearInterval(this.timer);
|
|
334
|
+
this.timer = null;
|
|
335
|
+
}
|
|
336
|
+
if (process2.stderr.isTTY) {
|
|
337
|
+
process2.stderr.write("\r\x1B[2K");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
async function withSpinner(message, task) {
|
|
342
|
+
const spinner = new InlineSpinner(message);
|
|
343
|
+
spinner.start();
|
|
344
|
+
try {
|
|
345
|
+
return await task();
|
|
346
|
+
} finally {
|
|
347
|
+
spinner.stop();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function truncateMessage(message, reserve = 8) {
|
|
351
|
+
const cols = process2.stderr.columns ?? 80;
|
|
352
|
+
const available = Math.max(cols - reserve, 20);
|
|
353
|
+
const normalized = message.replace(/[\r\n]+/g, " ");
|
|
354
|
+
if (normalized.length <= available) {
|
|
355
|
+
return normalized;
|
|
356
|
+
}
|
|
357
|
+
return `${normalized.slice(0, available - 3)}...`;
|
|
358
|
+
}
|
|
359
|
+
function formatStyledConfigSummary(lines) {
|
|
360
|
+
return lines.map((line) => {
|
|
361
|
+
const separator = line.indexOf(":");
|
|
362
|
+
if (separator === -1) {
|
|
363
|
+
return line;
|
|
364
|
+
}
|
|
365
|
+
const label = line.slice(0, separator + 1);
|
|
366
|
+
const value = line.slice(separator + 1);
|
|
367
|
+
return `${paint(label, "gray")}${value}`;
|
|
368
|
+
}).join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/lib/require-config.ts
|
|
372
|
+
async function requireConfig(profile) {
|
|
373
|
+
const cwd = process.cwd();
|
|
374
|
+
const resolvedProfile = resolveProfile(cwd, profile);
|
|
375
|
+
const config = loadConfig(resolvedProfile);
|
|
376
|
+
if (!config) {
|
|
377
|
+
logError(
|
|
378
|
+
`No profile "${resolvedProfile}" found. Run \`supabase-selfhosted-cli setup\` first.`
|
|
379
|
+
);
|
|
380
|
+
process.exitCode = 1;
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return config;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/lib/supabase-runner.ts
|
|
387
|
+
import { spawnSync } from "child_process";
|
|
388
|
+
import fs2 from "fs";
|
|
389
|
+
import path2 from "path";
|
|
390
|
+
function runSupabase(args, options) {
|
|
391
|
+
const localBin = path2.join(process.cwd(), "node_modules", ".bin", "supabase");
|
|
392
|
+
const useNpx = !fs2.existsSync(localBin);
|
|
393
|
+
const command = useNpx ? "npx" : localBin;
|
|
394
|
+
const finalArgs = useNpx ? ["supabase", ...args] : args;
|
|
395
|
+
if (options?.outputFile) {
|
|
396
|
+
const result2 = spawnSync(command, finalArgs, {
|
|
397
|
+
cwd: options.cwd ?? process.cwd(),
|
|
398
|
+
encoding: "utf8",
|
|
399
|
+
shell: false
|
|
400
|
+
});
|
|
401
|
+
if (result2.error) {
|
|
402
|
+
throw result2.error;
|
|
403
|
+
}
|
|
404
|
+
if ((result2.status ?? 1) !== 0) {
|
|
405
|
+
throw new Error(result2.stderr || result2.stdout || "supabase command failed");
|
|
406
|
+
}
|
|
407
|
+
fs2.writeFileSync(options.outputFile, result2.stdout);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const result = spawnSync(command, finalArgs, {
|
|
411
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
412
|
+
stdio: "inherit",
|
|
413
|
+
shell: false
|
|
414
|
+
});
|
|
415
|
+
if (result.error) {
|
|
416
|
+
throw result.error;
|
|
417
|
+
}
|
|
418
|
+
if ((result.status ?? 1) !== 0) {
|
|
419
|
+
throw new Error("supabase command failed");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function pushDatabaseMigrations(dbUrl, options) {
|
|
423
|
+
const args = ["db", "push", "--db-url", dbUrl, "--yes"];
|
|
424
|
+
if (options?.debug) {
|
|
425
|
+
args.push("--debug");
|
|
426
|
+
}
|
|
427
|
+
await withSpinner("Running supabase db push...", async () => {
|
|
428
|
+
runSupabase(args, { cwd: options?.cwd });
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async function generateTypeScriptTypes(dbUrl, outputFile, options) {
|
|
432
|
+
const args = [
|
|
433
|
+
"gen",
|
|
434
|
+
"types",
|
|
435
|
+
"typescript",
|
|
436
|
+
"--db-url",
|
|
437
|
+
dbUrl,
|
|
438
|
+
"--schema",
|
|
439
|
+
options?.schema ?? "public"
|
|
440
|
+
];
|
|
441
|
+
if (options?.debug) {
|
|
442
|
+
args.push("--debug");
|
|
443
|
+
}
|
|
444
|
+
await withSpinner(`Generating TypeScript types -> ${outputFile}`, async () => {
|
|
445
|
+
runSupabase(args, { cwd: options?.cwd, outputFile });
|
|
446
|
+
});
|
|
447
|
+
logSuccess(`Wrote ${outputFile}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/db-push.ts
|
|
451
|
+
async function runDbPush(options) {
|
|
452
|
+
const config = await requireConfig(options?.profile);
|
|
453
|
+
if (!config) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const dbUrl = buildDbUrl(config, "push");
|
|
457
|
+
await pushDatabaseMigrations(dbUrl, { debug: options?.debug, cwd: process.cwd() });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/commands/functions-deploy.ts
|
|
461
|
+
import { confirm } from "@inquirer/prompts";
|
|
462
|
+
|
|
463
|
+
// src/lib/deploy-options.ts
|
|
464
|
+
function resolveShouldRestart(config, options) {
|
|
465
|
+
if (options?.restart === true) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
if (options?.restart === false) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return config.deploy.restartAfterDeploy ? "prompt-default-yes" : "prompt-default-no";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/lib/local-deploy.ts
|
|
475
|
+
import fs4 from "fs";
|
|
476
|
+
import path4 from "path";
|
|
477
|
+
import { execSync } from "child_process";
|
|
478
|
+
|
|
479
|
+
// src/lib/function-sync.ts
|
|
480
|
+
import fs3 from "fs";
|
|
481
|
+
import path3 from "path";
|
|
482
|
+
function listLocalEntries(localDir) {
|
|
483
|
+
return fs3.readdirSync(localDir, { withFileTypes: true }).map((entry) => ({
|
|
484
|
+
name: entry.name,
|
|
485
|
+
isDirectory: entry.isDirectory()
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
function joinRemotePath(remoteDir, name) {
|
|
489
|
+
return `${remoteDir}/${name}`.replace(/\\/g, "/");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/lib/local-deploy.ts
|
|
493
|
+
function copyDirectory(localDir, targetDir, onFile, baseDir = localDir) {
|
|
494
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
495
|
+
for (const entry of fs4.readdirSync(localDir, { withFileTypes: true })) {
|
|
496
|
+
const localPath = path4.join(localDir, entry.name);
|
|
497
|
+
const destPath = path4.join(targetDir, entry.name);
|
|
498
|
+
if (entry.isDirectory()) {
|
|
499
|
+
copyDirectory(localPath, destPath, onFile, baseDir);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (entry.isFile()) {
|
|
503
|
+
onFile?.(path4.relative(baseDir, localPath).replace(/\\/g, "/"));
|
|
504
|
+
fs4.copyFileSync(localPath, destPath);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function pruneLocalDirectory(localDir, targetDir, targetRoot, onPrune) {
|
|
509
|
+
if (!fs4.existsSync(targetDir)) {
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
const localEntries = listLocalEntries(localDir);
|
|
513
|
+
const targetEntries = fs4.readdirSync(targetDir, { withFileTypes: true });
|
|
514
|
+
const localByName = new Map(localEntries.map((entry) => [entry.name, entry]));
|
|
515
|
+
let pruned = 0;
|
|
516
|
+
for (const targetEntry of targetEntries) {
|
|
517
|
+
const localEntry = localByName.get(targetEntry.name);
|
|
518
|
+
const localPath = path4.join(localDir, targetEntry.name);
|
|
519
|
+
const entryPath = joinRemotePath(targetDir, targetEntry.name);
|
|
520
|
+
if (!localEntry) {
|
|
521
|
+
fs4.rmSync(entryPath, { recursive: true, force: true });
|
|
522
|
+
onPrune?.(entryPath.slice(targetRoot.length + 1));
|
|
523
|
+
pruned += 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (targetEntry.isDirectory() && localEntry.isDirectory) {
|
|
527
|
+
pruned += pruneLocalDirectory(localPath, entryPath, targetRoot, onPrune);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return pruned;
|
|
531
|
+
}
|
|
532
|
+
async function deployFunctionsLocal(config, localFunctionsPath, options) {
|
|
533
|
+
const targetPath = path4.resolve(config.functions.remotePath);
|
|
534
|
+
logInfo(`Copying ${localFunctionsPath} -> ${targetPath}`);
|
|
535
|
+
if (options?.prune) {
|
|
536
|
+
const removed = pruneLocalDirectory(
|
|
537
|
+
localFunctionsPath,
|
|
538
|
+
targetPath,
|
|
539
|
+
targetPath,
|
|
540
|
+
(relativePath) => {
|
|
541
|
+
logInfo(`Pruned destination-only path: ${relativePath}`);
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
if (removed > 0) {
|
|
545
|
+
logSuccess(`Pruned ${removed} destination-only path(s).`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
let uploaded = 0;
|
|
549
|
+
await withSpinner("Copying function files...", async () => {
|
|
550
|
+
copyDirectory(localFunctionsPath, targetPath, () => {
|
|
551
|
+
uploaded += 1;
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
logSuccess(`Functions copied successfully (${uploaded} file(s)).`);
|
|
555
|
+
}
|
|
556
|
+
async function restartLocal(config) {
|
|
557
|
+
const command = config.deploy.restartCommand.trim();
|
|
558
|
+
if (!command) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
"Restart command is not configured. Run `supabase-selfhosted-cli setup` to set it."
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
logInfo(`Running restart command: ${command}`);
|
|
564
|
+
try {
|
|
565
|
+
await withSpinner("Restarting Supabase runtime...", async () => {
|
|
566
|
+
execSync(command, { stdio: "inherit", shell: "/bin/sh" });
|
|
567
|
+
});
|
|
568
|
+
} catch {
|
|
569
|
+
throw new Error("Restart command failed");
|
|
570
|
+
}
|
|
571
|
+
logSuccess("Restart completed.");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/lib/paths.ts
|
|
575
|
+
import fs5 from "fs";
|
|
576
|
+
import path5 from "path";
|
|
577
|
+
function resolveLocalFunctionsPath(cwd, configuredPath) {
|
|
578
|
+
const resolved = path5.isAbsolute(configuredPath) ? configuredPath : path5.resolve(cwd, configuredPath);
|
|
579
|
+
if (!fs5.existsSync(resolved)) {
|
|
580
|
+
throw new Error(`Local functions path does not exist: ${resolved}`);
|
|
581
|
+
}
|
|
582
|
+
return resolved;
|
|
583
|
+
}
|
|
584
|
+
function findSupabaseProjectRoot(startDir) {
|
|
585
|
+
let current = startDir;
|
|
586
|
+
while (true) {
|
|
587
|
+
const candidate = path5.join(current, "supabase", "functions");
|
|
588
|
+
if (fs5.existsSync(candidate)) {
|
|
589
|
+
return current;
|
|
590
|
+
}
|
|
591
|
+
const parent = path5.dirname(current);
|
|
592
|
+
if (parent === current) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
current = parent;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/lib/ssh.ts
|
|
600
|
+
import fs6 from "fs";
|
|
601
|
+
import path6 from "path";
|
|
602
|
+
import { Client } from "ssh2";
|
|
603
|
+
import SftpClient from "ssh2-sftp-client";
|
|
604
|
+
function withSshClient(credentials, run) {
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
const client = new Client();
|
|
607
|
+
client.on("ready", () => {
|
|
608
|
+
run(client).then(resolve).catch(reject).finally(() => client.end());
|
|
609
|
+
}).on("error", reject).connect({
|
|
610
|
+
host: credentials.host,
|
|
611
|
+
port: 22,
|
|
612
|
+
username: credentials.user,
|
|
613
|
+
password: credentials.password,
|
|
614
|
+
readyTimeout: 2e4
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async function runRemoteCommand(credentials, command) {
|
|
619
|
+
return withSshClient(
|
|
620
|
+
credentials,
|
|
621
|
+
(client) => new Promise((resolve, reject) => {
|
|
622
|
+
client.exec(command, (error, stream) => {
|
|
623
|
+
if (error) {
|
|
624
|
+
reject(error);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
let stdout = "";
|
|
628
|
+
let stderr = "";
|
|
629
|
+
stream.on("close", (code) => {
|
|
630
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
631
|
+
}).on("data", (data) => {
|
|
632
|
+
stdout += data.toString();
|
|
633
|
+
process.stdout.write(data);
|
|
634
|
+
});
|
|
635
|
+
stream.stderr.on("data", (data) => {
|
|
636
|
+
stderr += data.toString();
|
|
637
|
+
process.stderr.write(data);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
async function uploadDirectory(sftp, localDir, remoteDir, onFile, baseDir = localDir) {
|
|
644
|
+
await sftp.mkdir(remoteDir, true);
|
|
645
|
+
for (const entry of fs6.readdirSync(localDir, { withFileTypes: true })) {
|
|
646
|
+
const localPath = path6.join(localDir, entry.name);
|
|
647
|
+
const remotePath = `${remoteDir}/${entry.name}`.replace(/\\/g, "/");
|
|
648
|
+
if (entry.isDirectory()) {
|
|
649
|
+
await uploadDirectory(sftp, localPath, remotePath, onFile, baseDir);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (entry.isFile()) {
|
|
653
|
+
onFile?.(path6.relative(baseDir, localPath).replace(/\\/g, "/"));
|
|
654
|
+
await sftp.fastPut(localPath, remotePath);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function pruneDirectory(sftp, localDir, remoteDir, remoteRoot, onPrune) {
|
|
659
|
+
const localEntries = listLocalEntries(localDir);
|
|
660
|
+
let remoteEntries;
|
|
661
|
+
try {
|
|
662
|
+
remoteEntries = await sftp.list(remoteDir);
|
|
663
|
+
} catch {
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
const localByName = new Map(localEntries.map((entry) => [entry.name, entry]));
|
|
667
|
+
let pruned = 0;
|
|
668
|
+
for (const remoteEntry of remoteEntries) {
|
|
669
|
+
const localEntry = localByName.get(remoteEntry.name);
|
|
670
|
+
const localPath = path6.join(localDir, remoteEntry.name);
|
|
671
|
+
const remotePath = joinRemotePath(remoteDir, remoteEntry.name);
|
|
672
|
+
if (!localEntry) {
|
|
673
|
+
if (remoteEntry.type === "d") {
|
|
674
|
+
await sftp.rmdir(remotePath, true);
|
|
675
|
+
} else {
|
|
676
|
+
await sftp.delete(remotePath);
|
|
677
|
+
}
|
|
678
|
+
onPrune?.(remotePath.slice(remoteRoot.length + 1));
|
|
679
|
+
pruned += 1;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (remoteEntry.type === "d" && localEntry.isDirectory) {
|
|
683
|
+
pruned += await pruneDirectory(sftp, localPath, remotePath, remoteRoot, onPrune);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return pruned;
|
|
687
|
+
}
|
|
688
|
+
async function deployFunctionsDirectory(config, localFunctionsPath, options) {
|
|
689
|
+
const sftp = new SftpClient();
|
|
690
|
+
try {
|
|
691
|
+
await sftp.connect({
|
|
692
|
+
host: config.ssh.host,
|
|
693
|
+
port: 22,
|
|
694
|
+
username: config.ssh.user,
|
|
695
|
+
password: config.ssh.password,
|
|
696
|
+
readyTimeout: 2e4
|
|
697
|
+
});
|
|
698
|
+
logInfo(
|
|
699
|
+
`Uploading ${localFunctionsPath} -> ${config.ssh.user}@${config.ssh.host}:${config.functions.remotePath}`
|
|
700
|
+
);
|
|
701
|
+
if (options?.prune) {
|
|
702
|
+
const removed = await pruneDirectory(
|
|
703
|
+
sftp,
|
|
704
|
+
localFunctionsPath,
|
|
705
|
+
config.functions.remotePath,
|
|
706
|
+
config.functions.remotePath,
|
|
707
|
+
(relativePath) => {
|
|
708
|
+
logInfo(`Pruned remote-only path: ${relativePath}`);
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
if (removed > 0) {
|
|
712
|
+
logSuccess(`Pruned ${removed} remote-only path(s).`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
let uploaded = 0;
|
|
716
|
+
await withSpinner("Uploading function files...", async () => {
|
|
717
|
+
await uploadDirectory(sftp, localFunctionsPath, config.functions.remotePath, () => {
|
|
718
|
+
uploaded += 1;
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
logSuccess(`Functions uploaded successfully (${uploaded} file(s)).`);
|
|
722
|
+
} finally {
|
|
723
|
+
await sftp.end();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function restartSupabaseInstance(config) {
|
|
727
|
+
const command = config.deploy.restartCommand.trim();
|
|
728
|
+
if (!command) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
"Restart command is not configured. Run `supabase-selfhosted-cli setup` to set it."
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
logInfo(`Running restart command: ${command}`);
|
|
734
|
+
const result = await withSpinner(
|
|
735
|
+
"Restarting Supabase runtime...",
|
|
736
|
+
async () => runRemoteCommand(config.ssh, command)
|
|
737
|
+
);
|
|
738
|
+
if (result.code !== 0) {
|
|
739
|
+
throw new Error(`Restart command failed with exit code ${result.code}`);
|
|
740
|
+
}
|
|
741
|
+
logSuccess("Restart completed.");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/commands/functions-deploy.ts
|
|
745
|
+
async function runFunctionsDeploy(options) {
|
|
746
|
+
const config = await requireConfig(options?.profile);
|
|
747
|
+
if (!config) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const localPath = resolveLocalFunctionsPath(process.cwd(), config.functions.localPath);
|
|
751
|
+
if (config.target === "local") {
|
|
752
|
+
await deployFunctionsLocal(config, localPath, { prune: options?.prune });
|
|
753
|
+
} else {
|
|
754
|
+
await deployFunctionsDirectory(config, localPath, { prune: options?.prune });
|
|
755
|
+
}
|
|
756
|
+
const restartDecision = resolveShouldRestart(config, options);
|
|
757
|
+
let shouldRestart = false;
|
|
758
|
+
if (restartDecision === true) {
|
|
759
|
+
shouldRestart = true;
|
|
760
|
+
} else if (restartDecision === false) {
|
|
761
|
+
shouldRestart = false;
|
|
762
|
+
} else {
|
|
763
|
+
shouldRestart = await confirm({
|
|
764
|
+
message: "Restart Supabase instance now?",
|
|
765
|
+
default: restartDecision === "prompt-default-yes"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (shouldRestart) {
|
|
769
|
+
if (config.target === "local") {
|
|
770
|
+
await restartLocal(config);
|
|
771
|
+
} else {
|
|
772
|
+
await restartSupabaseInstance(config);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
logWarning("Skipped restart.");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/commands/gen-types.ts
|
|
780
|
+
import path7 from "path";
|
|
781
|
+
async function runGenTypes(options) {
|
|
782
|
+
const config = await requireConfig(options?.profile);
|
|
783
|
+
if (!config) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const outputFile = path7.resolve(
|
|
787
|
+
process.cwd(),
|
|
788
|
+
options?.output ?? "database.types.ts"
|
|
789
|
+
);
|
|
790
|
+
const dbUrl = buildDbUrl(config, "types");
|
|
791
|
+
await generateTypeScriptTypes(dbUrl, outputFile, {
|
|
792
|
+
debug: options?.debug,
|
|
793
|
+
cwd: process.cwd(),
|
|
794
|
+
schema: options?.schema
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/settings.ts
|
|
799
|
+
import { confirm as confirm3, select as select2 } from "@inquirer/prompts";
|
|
800
|
+
|
|
801
|
+
// src/commands/setup.ts
|
|
802
|
+
import path8 from "path";
|
|
803
|
+
import {
|
|
804
|
+
confirm as confirm2,
|
|
805
|
+
input,
|
|
806
|
+
number,
|
|
807
|
+
password,
|
|
808
|
+
select
|
|
809
|
+
} from "@inquirer/prompts";
|
|
810
|
+
async function promptSecretWithRetention(options) {
|
|
811
|
+
if (options.existing) {
|
|
812
|
+
const keepExisting = await confirm2({
|
|
813
|
+
message: `Keep existing ${options.label}?`,
|
|
814
|
+
default: true
|
|
815
|
+
});
|
|
816
|
+
if (keepExisting) {
|
|
817
|
+
return options.existing;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return password({
|
|
821
|
+
message: options.label,
|
|
822
|
+
mask: "*",
|
|
823
|
+
validate: (value) => value ? true : "Password is required"
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
var LOCAL_RESTART_DEFAULT = "docker ps --format '{{.Names}}' | grep -i edge | head -n 1 | xargs -I{} docker restart {}";
|
|
827
|
+
var REMOTE_RESTART_DEFAULT = "docker ps --format '{{.Names}}' | grep -i edge | head -n 1 | xargs -I{} docker restart {}";
|
|
828
|
+
async function runSetup(options) {
|
|
829
|
+
const cwd = process.cwd();
|
|
830
|
+
const profile = options?.profile ?? DEFAULT_PROFILE;
|
|
831
|
+
const existing = loadConfig(profile);
|
|
832
|
+
if (existing && !options?.forceUpdate) {
|
|
833
|
+
const overwrite = await confirm2({
|
|
834
|
+
message: `Profile "${profile}" already exists. Overwrite it?`,
|
|
835
|
+
default: false
|
|
836
|
+
});
|
|
837
|
+
if (!overwrite) {
|
|
838
|
+
logWarning("Setup cancelled.");
|
|
839
|
+
return existing;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
console.log(showBrandBanner());
|
|
843
|
+
logInfo("These settings are stored locally in ~/.supabase-selfhosted-cli/");
|
|
844
|
+
console.log("");
|
|
845
|
+
const target = await select({
|
|
846
|
+
message: "Where is your Supabase instance running?",
|
|
847
|
+
choices: [
|
|
848
|
+
{
|
|
849
|
+
name: "Local machine (Docker / Docker Compose on this computer)",
|
|
850
|
+
value: "local"
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "Remote server (VPS, cloud VM, etc. over SSH)",
|
|
854
|
+
value: "ssh"
|
|
855
|
+
}
|
|
856
|
+
],
|
|
857
|
+
default: existing?.target ?? "ssh"
|
|
858
|
+
});
|
|
859
|
+
let sshUser = existing?.ssh.user ?? "root";
|
|
860
|
+
let sshHost = existing?.ssh.host ?? "";
|
|
861
|
+
let sshPassword = existing?.ssh.password ?? "";
|
|
862
|
+
if (target === "ssh") {
|
|
863
|
+
sshUser = await input({
|
|
864
|
+
message: "SSH user",
|
|
865
|
+
default: sshUser
|
|
866
|
+
});
|
|
867
|
+
sshHost = await input({
|
|
868
|
+
message: "Server IP address or hostname",
|
|
869
|
+
default: sshHost,
|
|
870
|
+
validate: (value) => value.trim() ? true : "Host is required"
|
|
871
|
+
});
|
|
872
|
+
sshPassword = await promptSecretWithRetention({
|
|
873
|
+
label: "SSH password (stored locally on this machine)",
|
|
874
|
+
existing: existing?.ssh.password
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const functionsPathPrompt = target === "local" ? "Edge functions volume path on this machine (absolute path to Docker volume mount)" : "Remote edge functions path on the server";
|
|
878
|
+
const defaultFunctionsPath = existing?.functions.remotePath ?? (target === "local" ? path8.resolve(cwd, "volumes/functions") : "/etc/supabase/volumes/functions");
|
|
879
|
+
const functionsDestinationPath = await input({
|
|
880
|
+
message: functionsPathPrompt,
|
|
881
|
+
default: defaultFunctionsPath,
|
|
882
|
+
validate: (value) => value.trim() ? true : "Functions path is required"
|
|
883
|
+
});
|
|
884
|
+
const projectRoot = findSupabaseProjectRoot(cwd);
|
|
885
|
+
const defaultLocalPath = projectRoot ? "supabase/functions" : existing?.functions.localPath ?? "supabase/functions";
|
|
886
|
+
const localFunctionsPath = await input({
|
|
887
|
+
message: "Local functions folder (relative to project root or absolute)",
|
|
888
|
+
default: defaultLocalPath,
|
|
889
|
+
validate: (value) => value.trim() ? true : "Local path is required"
|
|
890
|
+
});
|
|
891
|
+
logInfo("Database connection (for migrations and type generation)");
|
|
892
|
+
console.log("");
|
|
893
|
+
const tenantId = await input({
|
|
894
|
+
message: "Postgres tenant id (the part after postgres.)",
|
|
895
|
+
default: existing?.database.tenantId ?? "your-tenant-id",
|
|
896
|
+
validate: (value) => value.trim() ? true : "Tenant id is required"
|
|
897
|
+
});
|
|
898
|
+
const dbPassword = await promptSecretWithRetention({
|
|
899
|
+
label: "Database password",
|
|
900
|
+
existing: existing?.database.password
|
|
901
|
+
});
|
|
902
|
+
const defaultDbHost = target === "local" ? "127.0.0.1" : existing?.database.host ?? sshHost.trim();
|
|
903
|
+
const dbHost = await input({
|
|
904
|
+
message: "Database host",
|
|
905
|
+
default: defaultDbHost,
|
|
906
|
+
validate: (value) => value.trim() ? true : "Database host is required"
|
|
907
|
+
});
|
|
908
|
+
const defaultPushPort = target === "local" ? 5432 : existing?.database.pushPort ?? 5453;
|
|
909
|
+
const defaultTypesPort = target === "local" ? 5432 : existing?.database.typesPort ?? 6438;
|
|
910
|
+
const pushPort = await number({
|
|
911
|
+
message: "Database port for migrations (supabase db push)",
|
|
912
|
+
default: defaultPushPort,
|
|
913
|
+
min: 1,
|
|
914
|
+
max: 65535
|
|
915
|
+
});
|
|
916
|
+
const typesPort = await number({
|
|
917
|
+
message: "Database port for type generation (supabase gen types)",
|
|
918
|
+
default: defaultTypesPort,
|
|
919
|
+
min: 1,
|
|
920
|
+
max: 65535
|
|
921
|
+
});
|
|
922
|
+
logInfo("Deploy options");
|
|
923
|
+
console.log("");
|
|
924
|
+
const restartAfterDeploy = await confirm2({
|
|
925
|
+
message: "Restart Supabase after deploying functions by default?",
|
|
926
|
+
default: existing?.deploy.restartAfterDeploy ?? true
|
|
927
|
+
});
|
|
928
|
+
let restartCommand = existing?.deploy.restartCommand ?? "";
|
|
929
|
+
if (restartAfterDeploy) {
|
|
930
|
+
const restartPrompt = target === "local" ? "Restart command to run locally after deploy" : "Restart command to run over SSH after deploy";
|
|
931
|
+
restartCommand = await input({
|
|
932
|
+
message: restartPrompt,
|
|
933
|
+
default: restartCommand || (target === "local" ? LOCAL_RESTART_DEFAULT : REMOTE_RESTART_DEFAULT),
|
|
934
|
+
validate: (value) => value.trim() ? true : "Restart command is required"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const linkProject = options?.linkProject ?? await confirm2({
|
|
938
|
+
message: "Link this project directory to this profile (.supabase-selfhosted-cli.json)?",
|
|
939
|
+
default: true
|
|
940
|
+
});
|
|
941
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
942
|
+
const config = {
|
|
943
|
+
profile,
|
|
944
|
+
target,
|
|
945
|
+
ssh: {
|
|
946
|
+
user: sshUser.trim(),
|
|
947
|
+
host: sshHost.trim(),
|
|
948
|
+
password: sshPassword
|
|
949
|
+
},
|
|
950
|
+
functions: {
|
|
951
|
+
localPath: localFunctionsPath.trim(),
|
|
952
|
+
remotePath: functionsDestinationPath.trim()
|
|
953
|
+
},
|
|
954
|
+
database: {
|
|
955
|
+
tenantId: tenantId.trim(),
|
|
956
|
+
password: dbPassword,
|
|
957
|
+
host: dbHost.trim(),
|
|
958
|
+
pushPort: pushPort ?? defaultPushPort,
|
|
959
|
+
typesPort: typesPort ?? defaultTypesPort,
|
|
960
|
+
database: "postgres"
|
|
961
|
+
},
|
|
962
|
+
deploy: {
|
|
963
|
+
restartAfterDeploy,
|
|
964
|
+
restartCommand: restartCommand.trim()
|
|
965
|
+
},
|
|
966
|
+
createdAt: existing?.createdAt ?? now,
|
|
967
|
+
updatedAt: now
|
|
968
|
+
};
|
|
969
|
+
saveConfig(config);
|
|
970
|
+
if (linkProject) {
|
|
971
|
+
saveProjectLink(cwd, profile);
|
|
972
|
+
logSuccess(`Linked ${cwd} to profile "${profile}".`);
|
|
973
|
+
}
|
|
974
|
+
printSummaryBlock("Setup complete", ...formatConfigSummary(config).split("\n"));
|
|
975
|
+
return config;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/commands/settings.ts
|
|
979
|
+
async function runSettings(options) {
|
|
980
|
+
const cwd = process.cwd();
|
|
981
|
+
const profile = resolveProfile(cwd, options?.profile);
|
|
982
|
+
const config = loadConfig(profile);
|
|
983
|
+
if (!config) {
|
|
984
|
+
logError(
|
|
985
|
+
`No profile "${profile}" found. Run \`supabase-selfhosted-cli setup\` first.`
|
|
986
|
+
);
|
|
987
|
+
process.exitCode = 1;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const action = await select2({
|
|
991
|
+
message: "Settings",
|
|
992
|
+
choices: [
|
|
993
|
+
{ name: "Show current configuration", value: "show" },
|
|
994
|
+
{ name: "Re-run setup wizard (update credentials)", value: "setup" },
|
|
995
|
+
{ name: "Delete stored credentials for this profile", value: "delete" },
|
|
996
|
+
{ name: "Cancel", value: "cancel" }
|
|
997
|
+
]
|
|
998
|
+
});
|
|
999
|
+
if (action === "cancel") {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (action === "show") {
|
|
1003
|
+
printSummaryBlock("Current configuration", ...formatConfigSummary(config).split("\n"));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (action === "setup") {
|
|
1007
|
+
await runSetup({ profile, linkProject: true, forceUpdate: true });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (action === "delete") {
|
|
1011
|
+
const profiles = listProfiles();
|
|
1012
|
+
const confirmed = await confirm3({
|
|
1013
|
+
message: `Delete profile "${profile}" and remove stored passwords?`,
|
|
1014
|
+
default: false
|
|
1015
|
+
});
|
|
1016
|
+
if (!confirmed) {
|
|
1017
|
+
logWarning("Cancelled.");
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
deleteConfig(profile);
|
|
1021
|
+
logSuccess(`Deleted profile "${profile}".`);
|
|
1022
|
+
if (profiles.length === 1) {
|
|
1023
|
+
logReview("No profiles remain. Run `supabase-selfhosted-cli setup` to configure again.");
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/lib/menu.ts
|
|
1029
|
+
import readline from "readline";
|
|
1030
|
+
import process3 from "process";
|
|
1031
|
+
var MENU_ITEMS = [
|
|
1032
|
+
{
|
|
1033
|
+
label: "Setup",
|
|
1034
|
+
description: "Configure SSH/local credentials",
|
|
1035
|
+
run: async () => {
|
|
1036
|
+
await runSetup({ profile: "default", linkProject: true });
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
label: "Deploy",
|
|
1041
|
+
description: "Push edge functions to your instance",
|
|
1042
|
+
run: async () => {
|
|
1043
|
+
await runFunctionsDeploy();
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
label: "DB Push",
|
|
1048
|
+
description: "Run supabase db push",
|
|
1049
|
+
run: async () => {
|
|
1050
|
+
await runDbPush();
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
label: "Gen Types",
|
|
1055
|
+
description: "Generate TypeScript types",
|
|
1056
|
+
run: async () => {
|
|
1057
|
+
await runGenTypes();
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
label: "Settings",
|
|
1062
|
+
description: "View or update stored profile",
|
|
1063
|
+
run: async () => {
|
|
1064
|
+
await runSettings();
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
];
|
|
1068
|
+
function clearScreen() {
|
|
1069
|
+
process3.stdout.write("\x1B[2J\x1B[H");
|
|
1070
|
+
}
|
|
1071
|
+
function clearLine() {
|
|
1072
|
+
return "\r\x1B[2K";
|
|
1073
|
+
}
|
|
1074
|
+
function renderMenu(selected) {
|
|
1075
|
+
clearScreen();
|
|
1076
|
+
for (const line of showBrandBanner().split("\n")) {
|
|
1077
|
+
process3.stdout.write(`${clearLine()}${line}
|
|
1078
|
+
`);
|
|
1079
|
+
}
|
|
1080
|
+
process3.stdout.write(`${clearLine()}
|
|
1081
|
+
`);
|
|
1082
|
+
MENU_ITEMS.forEach((item, index) => {
|
|
1083
|
+
const line = showMenuOption(
|
|
1084
|
+
index + 1,
|
|
1085
|
+
item.label,
|
|
1086
|
+
item.description,
|
|
1087
|
+
selected === index + 1
|
|
1088
|
+
);
|
|
1089
|
+
process3.stdout.write(`${clearLine()}${line}
|
|
1090
|
+
`);
|
|
1091
|
+
});
|
|
1092
|
+
if (process3.stdin.isTTY) {
|
|
1093
|
+
process3.stdout.write(`${clearLine()}
|
|
1094
|
+
`);
|
|
1095
|
+
const controls = `${colors.gray}\u2191\u2193 | Enter | M More | V Version | Q Quit${colors.nc}`;
|
|
1096
|
+
process3.stdout.write(`${clearLine()}${controls}
|
|
1097
|
+
`);
|
|
1098
|
+
process3.stdout.write(`${clearLine()}
|
|
1099
|
+
`);
|
|
1100
|
+
}
|
|
1101
|
+
process3.stdout.write("\x1B[J");
|
|
1102
|
+
}
|
|
1103
|
+
async function readKey() {
|
|
1104
|
+
if (!process3.stdin.isTTY) {
|
|
1105
|
+
return { action: "QUIT" };
|
|
1106
|
+
}
|
|
1107
|
+
readline.emitKeypressEvents(process3.stdin);
|
|
1108
|
+
process3.stdin.setRawMode(true);
|
|
1109
|
+
process3.stdin.resume();
|
|
1110
|
+
return new Promise((resolve) => {
|
|
1111
|
+
const cleanup = () => {
|
|
1112
|
+
process3.stdin.removeListener("keypress", onKeypress);
|
|
1113
|
+
process3.stdin.setRawMode(false);
|
|
1114
|
+
};
|
|
1115
|
+
const onKeypress = (_str, key) => {
|
|
1116
|
+
cleanup();
|
|
1117
|
+
if (key.ctrl && key.name === "c") {
|
|
1118
|
+
resolve({ action: "QUIT" });
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
switch (key.name) {
|
|
1122
|
+
case "up":
|
|
1123
|
+
case "k":
|
|
1124
|
+
resolve({ action: "UP" });
|
|
1125
|
+
return;
|
|
1126
|
+
case "down":
|
|
1127
|
+
case "j":
|
|
1128
|
+
resolve({ action: "DOWN" });
|
|
1129
|
+
return;
|
|
1130
|
+
case "return":
|
|
1131
|
+
case "enter":
|
|
1132
|
+
resolve({ action: "ENTER" });
|
|
1133
|
+
return;
|
|
1134
|
+
case "q":
|
|
1135
|
+
resolve({ action: "QUIT" });
|
|
1136
|
+
return;
|
|
1137
|
+
case "m":
|
|
1138
|
+
resolve({ action: "MORE" });
|
|
1139
|
+
return;
|
|
1140
|
+
case "v":
|
|
1141
|
+
resolve({ action: "VERSION" });
|
|
1142
|
+
return;
|
|
1143
|
+
default:
|
|
1144
|
+
if (/^[1-9]$/.test(key.sequence ?? "")) {
|
|
1145
|
+
resolve({ action: "NUMBER", number: Number(key.sequence) });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
resolve({ action: "QUIT" });
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
process3.stdin.on("keypress", onKeypress);
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function renderOverlay(content) {
|
|
1155
|
+
clearScreen();
|
|
1156
|
+
for (const line of content.split("\n")) {
|
|
1157
|
+
process3.stdout.write(`${clearLine()}${line}
|
|
1158
|
+
`);
|
|
1159
|
+
}
|
|
1160
|
+
process3.stdout.write(`${clearLine()}
|
|
1161
|
+
`);
|
|
1162
|
+
const controls = `${colors.gray}Esc | B Back | Enter${colors.nc}`;
|
|
1163
|
+
process3.stdout.write(`${clearLine()}${controls}
|
|
1164
|
+
`);
|
|
1165
|
+
process3.stdout.write("\x1B[J");
|
|
1166
|
+
}
|
|
1167
|
+
async function waitForBack() {
|
|
1168
|
+
if (!process3.stdin.isTTY) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
readline.emitKeypressEvents(process3.stdin);
|
|
1172
|
+
process3.stdin.setRawMode(true);
|
|
1173
|
+
process3.stdin.resume();
|
|
1174
|
+
return new Promise((resolve) => {
|
|
1175
|
+
const cleanup = () => {
|
|
1176
|
+
process3.stdin.removeListener("keypress", onKeypress);
|
|
1177
|
+
process3.stdin.setRawMode(false);
|
|
1178
|
+
};
|
|
1179
|
+
const onKeypress = (_str, key) => {
|
|
1180
|
+
if (key.ctrl && key.name === "c") {
|
|
1181
|
+
cleanup();
|
|
1182
|
+
resolve();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
switch (key.name) {
|
|
1186
|
+
case "escape":
|
|
1187
|
+
case "backspace":
|
|
1188
|
+
case "b":
|
|
1189
|
+
case "return":
|
|
1190
|
+
case "enter":
|
|
1191
|
+
cleanup();
|
|
1192
|
+
resolve();
|
|
1193
|
+
return;
|
|
1194
|
+
default:
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
process3.stdin.on("keypress", onKeypress);
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
async function runMenuItem(index, leaveScreen, enterScreen) {
|
|
1202
|
+
showCursor();
|
|
1203
|
+
leaveScreen();
|
|
1204
|
+
clearScreen();
|
|
1205
|
+
await MENU_ITEMS[index].run();
|
|
1206
|
+
process3.stdout.write("\n");
|
|
1207
|
+
const controls = `${colors.gray}Press Enter to return to menu${colors.nc}
|
|
1208
|
+
`;
|
|
1209
|
+
process3.stdout.write(controls);
|
|
1210
|
+
await waitForBack();
|
|
1211
|
+
enterScreen();
|
|
1212
|
+
hideCursor();
|
|
1213
|
+
}
|
|
1214
|
+
async function runInteractiveMenu() {
|
|
1215
|
+
if (!process3.stdin.isTTY || !process3.stdout.isTTY) {
|
|
1216
|
+
showHelp();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
let usingAlternateScreen = false;
|
|
1220
|
+
const enterScreen = () => {
|
|
1221
|
+
if (!process3.stdout.isTTY) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
process3.stdout.write("\x1B[?1049h");
|
|
1225
|
+
usingAlternateScreen = true;
|
|
1226
|
+
clearScreen();
|
|
1227
|
+
};
|
|
1228
|
+
const leaveScreen = () => {
|
|
1229
|
+
if (!usingAlternateScreen || !process3.stdout.isTTY) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
process3.stdout.write("\x1B[?1049l");
|
|
1233
|
+
usingAlternateScreen = false;
|
|
1234
|
+
};
|
|
1235
|
+
let selected = 1;
|
|
1236
|
+
enterScreen();
|
|
1237
|
+
hideCursor();
|
|
1238
|
+
const cleanup = () => {
|
|
1239
|
+
showCursor();
|
|
1240
|
+
leaveScreen();
|
|
1241
|
+
};
|
|
1242
|
+
process3.on("SIGINT", cleanup);
|
|
1243
|
+
try {
|
|
1244
|
+
while (true) {
|
|
1245
|
+
renderMenu(selected);
|
|
1246
|
+
const key = await readKey();
|
|
1247
|
+
switch (key.action) {
|
|
1248
|
+
case "UP":
|
|
1249
|
+
if (selected > 1) {
|
|
1250
|
+
selected -= 1;
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
case "DOWN":
|
|
1254
|
+
if (selected < MENU_ITEMS.length) {
|
|
1255
|
+
selected += 1;
|
|
1256
|
+
}
|
|
1257
|
+
break;
|
|
1258
|
+
case "ENTER":
|
|
1259
|
+
await runMenuItem(selected - 1, leaveScreen, enterScreen);
|
|
1260
|
+
break;
|
|
1261
|
+
case "NUMBER":
|
|
1262
|
+
if (key.number && key.number >= 1 && key.number <= MENU_ITEMS.length) {
|
|
1263
|
+
await runMenuItem(key.number - 1, leaveScreen, enterScreen);
|
|
1264
|
+
}
|
|
1265
|
+
break;
|
|
1266
|
+
case "MORE":
|
|
1267
|
+
renderOverlay(formatHelp());
|
|
1268
|
+
await waitForBack();
|
|
1269
|
+
break;
|
|
1270
|
+
case "VERSION":
|
|
1271
|
+
renderOverlay(formatVersion());
|
|
1272
|
+
await waitForBack();
|
|
1273
|
+
break;
|
|
1274
|
+
case "QUIT":
|
|
1275
|
+
cleanup();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
} finally {
|
|
1280
|
+
cleanup();
|
|
1281
|
+
process3.removeListener("SIGINT", cleanup);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/cli.ts
|
|
1286
|
+
var program = new Command();
|
|
1287
|
+
program.name("supabase-selfhosted-cli").description(
|
|
1288
|
+
"CLI for self-hosted Supabase \u2014 deploy functions, push migrations, sync types (VPS, Docker, local)"
|
|
1289
|
+
).version(VERSION, "-V, --version", "Show version information").helpOption(false).option("-h, --help", "Show help");
|
|
1290
|
+
program.command("setup").description("Interactive setup \u2014 store SSH/local, database, and deploy settings").option("-p, --profile <name>", "Profile name", "default").action(async (options) => {
|
|
1291
|
+
await runSetup({ profile: options.profile, linkProject: true });
|
|
1292
|
+
});
|
|
1293
|
+
program.command("settings").description("View, update, or delete stored credentials").option("-p, --profile <name>", "Profile name").action(async (options) => {
|
|
1294
|
+
await runSettings(options);
|
|
1295
|
+
});
|
|
1296
|
+
var functionsCommand = program.command("functions").description("Edge function operations");
|
|
1297
|
+
functionsCommand.command("deploy").description("Deploy local supabase/functions to your self-hosted instance").option("-p, --profile <name>", "Profile name").option("--restart", "Restart Supabase after deploy").option("--no-restart", "Skip restart after deploy").option("--prune", "Remove destination files and folders not present locally").action(async (options) => {
|
|
1298
|
+
await runFunctionsDeploy(options);
|
|
1299
|
+
});
|
|
1300
|
+
var dbCommand = program.command("db").description("Database operations");
|
|
1301
|
+
dbCommand.command("push").description("Push local migrations using supabase db push").option("-p, --profile <name>", "Profile name").option("--debug", "Pass --debug to supabase CLI").action(async (options) => {
|
|
1302
|
+
await runDbPush(options);
|
|
1303
|
+
});
|
|
1304
|
+
var genCommand = program.command("gen").description("Code generation");
|
|
1305
|
+
genCommand.command("types").description("Generate TypeScript types from the remote database").option("-p, --profile <name>", "Profile name").option("-o, --output <file>", "Output file", "database.types.ts").option("--schema <name>", "Postgres schema", "public").option("--debug", "Pass --debug to supabase CLI").action(
|
|
1306
|
+
async (options) => {
|
|
1307
|
+
await runGenTypes(options);
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
async function main() {
|
|
1311
|
+
const args = process.argv.slice(2);
|
|
1312
|
+
if (args.length === 0) {
|
|
1313
|
+
await runInteractiveMenu();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1317
|
+
showHelp();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
1321
|
+
showVersion();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
await program.parseAsync(process.argv);
|
|
1325
|
+
}
|
|
1326
|
+
main().catch((error) => {
|
|
1327
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
});
|