@mybase-dev/cli 1.0.2 → 1.0.4
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/dist/mybase.js +215 -5
- package/package.json +1 -1
package/dist/mybase.js
CHANGED
|
@@ -139,7 +139,7 @@ function parseDotenv(text) {
|
|
|
139
139
|
// package.json
|
|
140
140
|
var package_default = {
|
|
141
141
|
name: "@mybase-dev/cli",
|
|
142
|
-
version: "1.0.
|
|
142
|
+
version: "1.0.4",
|
|
143
143
|
description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
|
|
144
144
|
type: "module",
|
|
145
145
|
repository: {
|
|
@@ -3724,8 +3724,21 @@ var compose_default = `# Runs mybase: the control plane (API + dashboard), the s
|
|
|
3724
3724
|
# Optionally POSTGRES_BIND=127.0.0.1, GARAGE_S3_BIND=127.0.0.1, and VALKEY_BIND=127.0.0.1 to stop exposing those ports on the LAN.
|
|
3725
3725
|
# 5. docker compose up -d
|
|
3726
3726
|
# Apps on serverless hosts reach Postgres at wss://mybase.<domain>/db with @neondatabase/serverless,
|
|
3727
|
+
# or, with a router port forward, at a normal postgres:// URL (public Postgres, below).
|
|
3727
3728
|
# and the cache at https://mybase.<domain>/cache/<project> (@mybase-dev/sdk/cache or @upstash/redis).
|
|
3728
3729
|
# Cloudflare's free plan caps request bodies at 100 MB: bigger dashboard uploads only work on the LAN.
|
|
3730
|
+
#
|
|
3731
|
+
# Public Postgres (any driver, from anywhere): \`mybase server database-public\` sets this up. By hand:
|
|
3732
|
+
# 1. On your router, forward TCP 5432 to this machine (it reveals your home IP; won't work behind CGNAT).
|
|
3733
|
+
# 2. Create a Cloudflare API token with Zone \u203A DNS \u203A Edit + Zone \u203A Zone \u203A Read for your zone.
|
|
3734
|
+
# 3. In .env:
|
|
3735
|
+
# MYBASE_DB_PUBLIC=on
|
|
3736
|
+
# MYBASE_DB_PUBLIC_HOST=db.<domain>
|
|
3737
|
+
# MYBASE_DNS_TOKEN=<token>
|
|
3738
|
+
# 4. docker compose up -d
|
|
3739
|
+
# mybase then keeps an unproxied A record for db.<domain> on your public IP, gets a Let's Encrypt
|
|
3740
|
+
# certificate for it (DNS challenge, no port 80), and connection strings end in ?sslmode=require.
|
|
3741
|
+
# From the internet only project roles can sign in, to their own database, over TLS.
|
|
3729
3742
|
name: mybase
|
|
3730
3743
|
|
|
3731
3744
|
services:
|
|
@@ -3759,6 +3772,12 @@ services:
|
|
|
3759
3772
|
MYBASE_PUBLIC_URL: \${MYBASE_PUBLIC_URL:-http://\${MYBASE_PUBLIC_HOST:-localhost}:\${MYBASE_PORT:-4000}}
|
|
3760
3773
|
# Postgres over WebSocket, for apps that can't reach the Postgres port. Defaults to <MYBASE_PUBLIC_URL>/db.
|
|
3761
3774
|
MYBASE_DB_WS_URL: \${MYBASE_DB_WS_URL:-}
|
|
3775
|
+
# Public Postgres (see the top of this file): on + MYBASE_DB_PUBLIC_HOST + MYBASE_DNS_TOKEN.
|
|
3776
|
+
MYBASE_DB_PUBLIC: \${MYBASE_DB_PUBLIC:-}
|
|
3777
|
+
MYBASE_DNS_TOKEN: \${MYBASE_DNS_TOKEN:-}
|
|
3778
|
+
# Optional: contact for Let's Encrypt, and 1 to use its staging CA (untrusted certs, for testing).
|
|
3779
|
+
MYBASE_ACME_EMAIL: \${MYBASE_ACME_EMAIL:-}
|
|
3780
|
+
MYBASE_ACME_STAGING: \${MYBASE_ACME_STAGING:-}
|
|
3762
3781
|
MYBASE_MAX_UPLOAD: \${MYBASE_MAX_UPLOAD:-1073741824}
|
|
3763
3782
|
VALKEY_HOST: valkey
|
|
3764
3783
|
VALKEY_PORT: 6379
|
|
@@ -3935,6 +3954,10 @@ var SECTIONS = [
|
|
|
3935
3954
|
keys: ["MYBASE_PORT", "MYBASE_PUBLIC_HOST", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY", "POSTGRES_PORT", "GARAGE_S3_PORT", "VALKEY_PORT", "POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"]
|
|
3936
3955
|
},
|
|
3937
3956
|
{ title: "Cloudflare Tunnel (public HTTPS without opening router ports)", keys: ["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN"] },
|
|
3957
|
+
{
|
|
3958
|
+
title: "Public Postgres (router port forward; `mybase server database-public`)",
|
|
3959
|
+
keys: ["MYBASE_DB_PUBLIC", "MYBASE_DB_PUBLIC_HOST", "MYBASE_DNS_TOKEN", "MYBASE_ACME_EMAIL", "MYBASE_ACME_STAGING"]
|
|
3960
|
+
},
|
|
3938
3961
|
{ title: "Startup: `no` keeps mybase from starting again after a reboot or crash", keys: ["MYBASE_RESTART"] },
|
|
3939
3962
|
{ title: "Tuning", keys: ["CACHE_MAXMEMORY", "GARAGE_CAPACITY", "MYBASE_MAX_UPLOAD", "LOG_LEVEL"] }
|
|
3940
3963
|
];
|
|
@@ -4200,14 +4223,15 @@ import { homedir as homedir2, hostname as hostname3 } from "os";
|
|
|
4200
4223
|
import { join as join3, resolve as resolve4 } from "path";
|
|
4201
4224
|
import { styleText as styleText3 } from "util";
|
|
4202
4225
|
|
|
4203
|
-
// src/cloudflare.ts
|
|
4226
|
+
// ../../packages/shared/src/cloudflare.ts
|
|
4204
4227
|
var API = "https://api.cloudflare.com/client/v4";
|
|
4205
4228
|
var TOKEN_PERMISSIONS = ["Account \u203A Cloudflare Tunnel \u203A Edit", "Zone \u203A DNS \u203A Edit", "Zone \u203A Zone \u203A Read"];
|
|
4229
|
+
var DNS_TOKEN_PERMISSIONS = ["Zone \u203A DNS \u203A Edit", "Zone \u203A Zone \u203A Read"];
|
|
4206
4230
|
var TOKEN_URL = "https://dash.cloudflare.com/profile/api-tokens";
|
|
4207
4231
|
|
|
4208
4232
|
class CloudflareError extends Error {
|
|
4209
4233
|
}
|
|
4210
|
-
function cloudflare(token, fetcher = fetch) {
|
|
4234
|
+
function cloudflare(token, fetcher = fetch, permissions = TOKEN_PERMISSIONS) {
|
|
4211
4235
|
async function call(method, path, body) {
|
|
4212
4236
|
const res = await fetcher(`${API}${path}`, {
|
|
4213
4237
|
method,
|
|
@@ -4220,7 +4244,7 @@ function cloudflare(token, fetcher = fetch) {
|
|
|
4220
4244
|
if (!res.ok || !data?.success) {
|
|
4221
4245
|
const message = data?.errors?.map((e) => e.message).join("; ") || res.statusText;
|
|
4222
4246
|
if (res.status === 401 || res.status === 403)
|
|
4223
|
-
throw new CloudflareError(`Cloudflare refused the token (${message}). It needs: ${
|
|
4247
|
+
throw new CloudflareError(`Cloudflare refused the token (${message}). It needs: ${permissions.join(", ")}.`);
|
|
4224
4248
|
throw new CloudflareError(`Cloudflare: ${message}`);
|
|
4225
4249
|
}
|
|
4226
4250
|
return data.result;
|
|
@@ -4228,6 +4252,19 @@ function cloudflare(token, fetcher = fetch) {
|
|
|
4228
4252
|
return {
|
|
4229
4253
|
listAccounts: () => call("GET", "/accounts?per_page=50"),
|
|
4230
4254
|
listZones: () => call("GET", "/zones?per_page=50&status=active"),
|
|
4255
|
+
async zoneFor(host) {
|
|
4256
|
+
const zones = await call("GET", "/zones?per_page=50&status=active");
|
|
4257
|
+
return zoneOf(zones, host);
|
|
4258
|
+
},
|
|
4259
|
+
async upsertRecord(zoneId, record) {
|
|
4260
|
+
const path = `/zones/${zoneId}/dns_records`;
|
|
4261
|
+
const existing = await call("GET", `${path}?type=${record.type}&name=${encodeURIComponent(record.name)}`);
|
|
4262
|
+
if (existing[0])
|
|
4263
|
+
return call("PUT", `${path}/${existing[0].id}`, record);
|
|
4264
|
+
return call("POST", path, record);
|
|
4265
|
+
},
|
|
4266
|
+
createRecord: (zoneId, record) => call("POST", `/zones/${zoneId}/dns_records`, record),
|
|
4267
|
+
deleteRecord: (zoneId, id) => call("DELETE", `/zones/${zoneId}/dns_records/${id}`),
|
|
4231
4268
|
async ensureTunnel(accountId, name) {
|
|
4232
4269
|
const existing = await call("GET", `/accounts/${accountId}/cfd_tunnel?name=${encodeURIComponent(name)}&is_deleted=false`);
|
|
4233
4270
|
if (existing[0])
|
|
@@ -4254,7 +4291,16 @@ function cloudflare(token, fetcher = fetch) {
|
|
|
4254
4291
|
};
|
|
4255
4292
|
}
|
|
4256
4293
|
var pointsAtTunnel = (record, tunnelId) => record.content === `${tunnelId}.cfargotunnel.com`;
|
|
4257
|
-
|
|
4294
|
+
function zoneOf(zones, host) {
|
|
4295
|
+
const name = host.toLowerCase().replace(/\.$/, "");
|
|
4296
|
+
let best = null;
|
|
4297
|
+
for (const zone of zones) {
|
|
4298
|
+
const z = zone.name.toLowerCase();
|
|
4299
|
+
if ((name === z || name.endsWith(`.${z}`)) && z.length > (best?.name.length ?? -1))
|
|
4300
|
+
best = zone;
|
|
4301
|
+
}
|
|
4302
|
+
return best;
|
|
4303
|
+
}
|
|
4258
4304
|
// src/server-setup.ts
|
|
4259
4305
|
var DEFAULT_DIR = join3(homedir2(), "mybase");
|
|
4260
4306
|
var USERNAME_PATTERN = /^[a-zA-Z0-9_.@-]{1,64}$/;
|
|
@@ -4483,6 +4529,70 @@ async function bindStep(lan, existing) {
|
|
|
4483
4529
|
}));
|
|
4484
4530
|
return Object.fromEntries(BIND_KEYS.map((k) => [k, lock ? "127.0.0.1" : null]));
|
|
4485
4531
|
}
|
|
4532
|
+
var DB_PUBLIC_KEYS = ["MYBASE_DB_PUBLIC", "MYBASE_DB_PUBLIC_HOST", "MYBASE_DNS_TOKEN", "MYBASE_ACME_EMAIL", "MYBASE_ACME_STAGING"];
|
|
4533
|
+
async function dbPublicStep(existing) {
|
|
4534
|
+
ui.note([
|
|
4535
|
+
`Apps anywhere get a normal postgres:// URL, with any driver. You need:`,
|
|
4536
|
+
`\u2022 Your router to forward TCP port ${existing.POSTGRES_PORT || "5432"} to this machine. ${dim("Shows your home IP; not possible behind CGNAT.")}`,
|
|
4537
|
+
`\u2022 A Cloudflare token that mybase keeps, to point db.<domain> at your IP and get a TLS certificate.`,
|
|
4538
|
+
` Create one at ${cyan(TOKEN_URL)} ${dim("(Custom token)")} with only:`,
|
|
4539
|
+
...DNS_TOKEN_PERMISSIONS.map((perm) => ` \u2022 ${perm}`)
|
|
4540
|
+
].join(`
|
|
4541
|
+
`), "Public Postgres");
|
|
4542
|
+
const tunnelHost = URL.parse(existing.MYBASE_PUBLIC_URL ?? "")?.hostname ?? "";
|
|
4543
|
+
for (;; ) {
|
|
4544
|
+
const token = (await ask(password({ message: "Cloudflare API token (DNS edit)", validate: (v) => v?.trim() ? undefined : "required" }))).trim();
|
|
4545
|
+
const cf = cloudflare(token, fetch, DNS_TOKEN_PERMISSIONS);
|
|
4546
|
+
const s = ui.spinner();
|
|
4547
|
+
s.start("Looking up your domains");
|
|
4548
|
+
let zones;
|
|
4549
|
+
try {
|
|
4550
|
+
zones = await cf.listZones();
|
|
4551
|
+
} catch (err) {
|
|
4552
|
+
s.error(err.message);
|
|
4553
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4554
|
+
continue;
|
|
4555
|
+
throw new CliError("public Postgres not set up: no working token");
|
|
4556
|
+
}
|
|
4557
|
+
if (zones.length === 0) {
|
|
4558
|
+
s.error("No active domains on this token");
|
|
4559
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4560
|
+
continue;
|
|
4561
|
+
throw new CliError("public Postgres not set up: the token can't see any domain");
|
|
4562
|
+
}
|
|
4563
|
+
s.stop(`Found ${zones.length} domain${zones.length === 1 ? "" : "s"}`);
|
|
4564
|
+
const preferred = zoneOf(zones, existing.MYBASE_DB_PUBLIC_HOST || tunnelHost);
|
|
4565
|
+
const zoneId = zones.length === 1 ? zones[0].id : await ask(select({ message: "Which domain?", options: zones.map((z) => ({ value: z.id, label: z.name })), initialValue: preferred?.id }));
|
|
4566
|
+
const chosen = zones.find((z) => z.id === zoneId);
|
|
4567
|
+
const suggested = existing.MYBASE_DB_PUBLIC_HOST && zoneOf([chosen], existing.MYBASE_DB_PUBLIC_HOST) ? existing.MYBASE_DB_PUBLIC_HOST : `db.${chosen.name}`;
|
|
4568
|
+
const host = (await ask(text({
|
|
4569
|
+
message: "Hostname for the database",
|
|
4570
|
+
placeholder: suggested,
|
|
4571
|
+
defaultValue: suggested,
|
|
4572
|
+
validate: (v) => {
|
|
4573
|
+
const h = (v || suggested).trim().toLowerCase();
|
|
4574
|
+
if (validHost(h))
|
|
4575
|
+
return validHost(h);
|
|
4576
|
+
return zoneOf([chosen], h) ? undefined : `must end in ${chosen.name}`;
|
|
4577
|
+
}
|
|
4578
|
+
}))).trim().toLowerCase() || suggested;
|
|
4579
|
+
const record = await cf.findRecord(chosen.id, host).catch(() => null);
|
|
4580
|
+
if (record && record.type !== "A") {
|
|
4581
|
+
const replace = await ask(confirm2({ message: `${host} already has a ${record.type} record (${record.content}). Replace it?`, initialValue: false }));
|
|
4582
|
+
if (!replace)
|
|
4583
|
+
continue;
|
|
4584
|
+
await cf.deleteRecord(chosen.id, record.id);
|
|
4585
|
+
}
|
|
4586
|
+
const email = (await ask(text({ message: "Email for Let's Encrypt expiry notices (optional)", placeholder: "skip" }))).trim();
|
|
4587
|
+
return {
|
|
4588
|
+
MYBASE_DB_PUBLIC: "on",
|
|
4589
|
+
MYBASE_DB_PUBLIC_HOST: host,
|
|
4590
|
+
MYBASE_DNS_TOKEN: token,
|
|
4591
|
+
MYBASE_ACME_EMAIL: email || null,
|
|
4592
|
+
POSTGRES_BIND: null
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4486
4596
|
async function adminStep() {
|
|
4487
4597
|
const when = await ask(select({
|
|
4488
4598
|
message: "Create the admin account now?",
|
|
@@ -4898,6 +5008,9 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
|
|
|
4898
5008
|
server status [--dir <dir>] Containers, health, and whether mybase starts on boot
|
|
4899
5009
|
server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
|
|
4900
5010
|
server tunnel [--dir <dir>] Add or replace the Cloudflare Tunnel [--tunnel-token <t> --domain <d>] | --off
|
|
5011
|
+
server database-public [--dir <dir>] A normal postgres:// URL from anywhere: router port forward +
|
|
5012
|
+
Let's Encrypt TLS + DNS kept on your IP (Cloudflare)
|
|
5013
|
+
[--host <db.domain> --dns-token <t> [--email <e>] [--staging]] | --off
|
|
4901
5014
|
server autostart on|off Start mybase when this machine boots (and restart it after crashes)
|
|
4902
5015
|
Default dir: the last one setup used, else ~/mybase`;
|
|
4903
5016
|
var dim2 = (s) => styleText4("dim", s);
|
|
@@ -5004,6 +5117,101 @@ async function tunnel(args) {
|
|
|
5004
5117
|
return print({ dir, version, publicUrl: host ? `https://${host}` : null }, () => {});
|
|
5005
5118
|
outro(host ? `Public at https://${host}. Turn on two-factor sign-in in Settings if you haven't.` : "Tunnel off. mybase is reachable on this machine and your network only.");
|
|
5006
5119
|
}
|
|
5120
|
+
async function databasePublic(args) {
|
|
5121
|
+
const dir = await serverDir(args);
|
|
5122
|
+
const { text, values } = await loadEnv(dir);
|
|
5123
|
+
const interactive = !jsonMode && process.stdin.isTTY === true;
|
|
5124
|
+
const off = args.includes("--off");
|
|
5125
|
+
if (!jsonMode)
|
|
5126
|
+
intro(`${styleText4("bold", " mybase ")} ${dim2("server database-public")}`);
|
|
5127
|
+
let updates;
|
|
5128
|
+
if (off) {
|
|
5129
|
+
updates = Object.fromEntries(DB_PUBLIC_KEYS.map((k) => [k, null]));
|
|
5130
|
+
} else if (flag(args, "--dns-token")) {
|
|
5131
|
+
const host = flag(args, "--host");
|
|
5132
|
+
if (!host)
|
|
5133
|
+
throw usageError("--dns-token needs --host (like db.example.com)");
|
|
5134
|
+
updates = {
|
|
5135
|
+
MYBASE_DB_PUBLIC: "on",
|
|
5136
|
+
MYBASE_DB_PUBLIC_HOST: host.toLowerCase(),
|
|
5137
|
+
MYBASE_DNS_TOKEN: flag(args, "--dns-token"),
|
|
5138
|
+
MYBASE_ACME_EMAIL: flag(args, "--email") ?? null,
|
|
5139
|
+
MYBASE_ACME_STAGING: args.includes("--staging") ? "1" : null,
|
|
5140
|
+
POSTGRES_BIND: null
|
|
5141
|
+
};
|
|
5142
|
+
} else if (interactive) {
|
|
5143
|
+
updates = await dbPublicStep(values);
|
|
5144
|
+
} else
|
|
5145
|
+
throw usageError("without a terminal, pass --host <db.domain> --dns-token <t>, or --off");
|
|
5146
|
+
if (!off && values.POSTGRES_BIND === "127.0.0.1")
|
|
5147
|
+
ui.info("Postgres now listens on your network again (POSTGRES_BIND), so the router can reach it.");
|
|
5148
|
+
await saveEnv(dir, mergeEnv(text, updates));
|
|
5149
|
+
if (await writeCompose(dir, interactive, true))
|
|
5150
|
+
ui.success(`compose.yml updated to this CLI's (${CLI_VERSION})`);
|
|
5151
|
+
const version = await startStack(dir, values);
|
|
5152
|
+
const status = off ? null : await waitForDbPublic(localUrl(values));
|
|
5153
|
+
const port = values.POSTGRES_PORT || "5432";
|
|
5154
|
+
const lan = addresses().find((a) => a.label.startsWith("LAN IP"))?.value ?? "<this machine's LAN IP>";
|
|
5155
|
+
const host = off ? null : updates.MYBASE_DB_PUBLIC_HOST;
|
|
5156
|
+
if (jsonMode)
|
|
5157
|
+
return print({ dir, version, host, port: Number(port), forward: host ? `${lan}:${port}` : null, status }, () => {});
|
|
5158
|
+
if (!host)
|
|
5159
|
+
return outro("Public Postgres off. Connection strings point at your network address again. Remove the port forward on your router.");
|
|
5160
|
+
ui.note([
|
|
5161
|
+
`On your router (usually "Port forwarding" or "Virtual server"):`,
|
|
5162
|
+
` TCP ${port} (external) \u2192 ${lan}:${port}`,
|
|
5163
|
+
``,
|
|
5164
|
+
dim2(status?.tls.active ? "DNS and the certificate are ready." : "Check progress in the dashboard (Database \u2192 Settings) or with `mybase doctor`.")
|
|
5165
|
+
].join(`
|
|
5166
|
+
`), "Last step");
|
|
5167
|
+
outro(`New connection strings: postgres://<project>:<password>@${host}:${port}/<project>?sslmode=require`);
|
|
5168
|
+
}
|
|
5169
|
+
async function waitForDbPublic(url) {
|
|
5170
|
+
const s = ui.spinner();
|
|
5171
|
+
s.start("Pointing DNS at your public IP and getting a certificate (about a minute)");
|
|
5172
|
+
const deadline = Date.now() + 150000;
|
|
5173
|
+
let last = null;
|
|
5174
|
+
for (;; ) {
|
|
5175
|
+
try {
|
|
5176
|
+
last = await api("/database/public", { url });
|
|
5177
|
+
} catch (err) {
|
|
5178
|
+
const status = err.status;
|
|
5179
|
+
if (status === 401 || status === 403) {
|
|
5180
|
+
s.stop("Can't check progress: this CLI isn't signed in to the server as admin");
|
|
5181
|
+
return null;
|
|
5182
|
+
}
|
|
5183
|
+
if (status !== 404)
|
|
5184
|
+
throw err;
|
|
5185
|
+
last = null;
|
|
5186
|
+
}
|
|
5187
|
+
if (typeof last?.enabled !== "boolean") {
|
|
5188
|
+
s.error("The server doesn't support public Postgres");
|
|
5189
|
+
throw new CliError("the running mybase image is older than this CLI. Run `mybase server update` (or set MYBASE_IMAGE to a newer build), then this command again.");
|
|
5190
|
+
}
|
|
5191
|
+
if (!last.enabled) {
|
|
5192
|
+
s.error("The server didn't get the settings");
|
|
5193
|
+
throw new CliError("MYBASE_DB_PUBLIC isn't reaching the API. Check that compose.yml passes it (`mybase server update` refreshes compose.yml).");
|
|
5194
|
+
}
|
|
5195
|
+
if (last.problem) {
|
|
5196
|
+
s.error(last.problem);
|
|
5197
|
+
throw new CliError(last.problem);
|
|
5198
|
+
}
|
|
5199
|
+
if (last.dns.error || last.tls.error) {
|
|
5200
|
+
s.error([last.dns.error && `DNS: ${last.dns.error}`, last.tls.error && `Certificate: ${last.tls.error}`].filter(Boolean).join("; "));
|
|
5201
|
+
ui.info("mybase keeps retrying (certificate: in 1 h, 6 h, then daily). Fix the cause, then run `mybase server update` to retry now.");
|
|
5202
|
+
return last;
|
|
5203
|
+
}
|
|
5204
|
+
if (last.tls.active) {
|
|
5205
|
+
s.stop(`${last.host} \u2192 ${last.dns.ip}, certificate valid until ${last.tls.notAfter?.slice(0, 10)}`);
|
|
5206
|
+
return last;
|
|
5207
|
+
}
|
|
5208
|
+
if (Date.now() > deadline) {
|
|
5209
|
+
s.stop(`Still working (${last.dns.ip ? `DNS \u2192 ${last.dns.ip}` : "DNS pending"}, certificate pending)`);
|
|
5210
|
+
return last;
|
|
5211
|
+
}
|
|
5212
|
+
await Bun.sleep(3000);
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5007
5215
|
async function autostart(args) {
|
|
5008
5216
|
const mode = args[0];
|
|
5009
5217
|
if (mode !== "on" && mode !== "off")
|
|
@@ -5034,6 +5242,8 @@ async function server(command, args) {
|
|
|
5034
5242
|
await update(args);
|
|
5035
5243
|
else if (command === "tunnel")
|
|
5036
5244
|
await tunnel(args);
|
|
5245
|
+
else if (command === "database-public")
|
|
5246
|
+
await databasePublic(args);
|
|
5037
5247
|
else if (command === "autostart")
|
|
5038
5248
|
await autostart(args);
|
|
5039
5249
|
else
|
package/package.json
CHANGED