@mybase-dev/cli 1.0.11 → 1.0.13
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 +998 -136
- package/package.json +1 -1
package/dist/mybase.js
CHANGED
|
@@ -100,6 +100,7 @@ var init_src = __esm(() => {
|
|
|
100
100
|
{ id: "captcha", name: "Captcha", description: "Captcha settings (read), and verifying tokens (write)" },
|
|
101
101
|
{ id: "realtime", name: "Realtime", description: "Channels, members, and settings (read), and publishing messages (write)" },
|
|
102
102
|
{ id: "sandbox", name: "Agent sandboxes", description: "Sandboxes, executions, files, artifacts, and browser connections" },
|
|
103
|
+
{ id: "hosting", name: "Hosting", description: "Sites, deploys, and logs (read); pushing images, deploying, and changing sites (write)" },
|
|
103
104
|
{ id: "audit", name: "Audit log", description: "Who did what: sign-ins, key and project changes, secret reads (read only)" }
|
|
104
105
|
];
|
|
105
106
|
});
|
|
@@ -109,7 +110,7 @@ var package_default;
|
|
|
109
110
|
var init_package = __esm(() => {
|
|
110
111
|
package_default = {
|
|
111
112
|
name: "@mybase-dev/cli",
|
|
112
|
-
version: "1.0.
|
|
113
|
+
version: "1.0.13",
|
|
113
114
|
description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
|
|
114
115
|
type: "module",
|
|
115
116
|
repository: {
|
|
@@ -1347,7 +1348,7 @@ ${h}
|
|
|
1347
1348
|
}
|
|
1348
1349
|
}
|
|
1349
1350
|
}).prompt();
|
|
1350
|
-
},
|
|
1351
|
+
}, log2, cancel = (o = "", t) => {
|
|
1351
1352
|
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR_END)} ` : "";
|
|
1352
1353
|
i.write(`${e}${styleText2("red", o)}
|
|
1353
1354
|
|
|
@@ -1557,7 +1558,7 @@ ${f}
|
|
|
1557
1558
|
}
|
|
1558
1559
|
}
|
|
1559
1560
|
}).prompt();
|
|
1560
|
-
}, i,
|
|
1561
|
+
}, i, text2 = (t) => new n({
|
|
1561
1562
|
validate: t.validate,
|
|
1562
1563
|
placeholder: t.placeholder,
|
|
1563
1564
|
defaultValue: t.defaultValue,
|
|
@@ -1638,7 +1639,7 @@ var init_dist4 = __esm(() => {
|
|
|
1638
1639
|
`${styleText2("dim", "Space:")} select`,
|
|
1639
1640
|
`${styleText2("dim", "Enter:")} confirm`
|
|
1640
1641
|
];
|
|
1641
|
-
|
|
1642
|
+
log2 = {
|
|
1642
1643
|
message: (s = [], {
|
|
1643
1644
|
symbol: e = styleText2("gray", S_BAR),
|
|
1644
1645
|
secondarySymbol: r = styleText2("gray", S_BAR),
|
|
@@ -1662,22 +1663,22 @@ var init_dist4 = __esm(() => {
|
|
|
1662
1663
|
`);
|
|
1663
1664
|
},
|
|
1664
1665
|
info: (s, e) => {
|
|
1665
|
-
|
|
1666
|
+
log2.message(s, { ...e, symbol: styleText2("blue", S_INFO) });
|
|
1666
1667
|
},
|
|
1667
1668
|
success: (s, e) => {
|
|
1668
|
-
|
|
1669
|
+
log2.message(s, { ...e, symbol: styleText2("green", S_SUCCESS) });
|
|
1669
1670
|
},
|
|
1670
1671
|
step: (s, e) => {
|
|
1671
|
-
|
|
1672
|
+
log2.message(s, { ...e, symbol: styleText2("green", S_STEP_SUBMIT) });
|
|
1672
1673
|
},
|
|
1673
1674
|
warn: (s, e) => {
|
|
1674
|
-
|
|
1675
|
+
log2.message(s, { ...e, symbol: styleText2("yellow", S_WARN) });
|
|
1675
1676
|
},
|
|
1676
1677
|
warning: (s, e) => {
|
|
1677
|
-
|
|
1678
|
+
log2.warn(s, e);
|
|
1678
1679
|
},
|
|
1679
1680
|
error: (s, e) => {
|
|
1680
|
-
|
|
1681
|
+
log2.message(s, { ...e, symbol: styleText2("red", S_ERROR) });
|
|
1681
1682
|
}
|
|
1682
1683
|
};
|
|
1683
1684
|
u2 = {
|
|
@@ -1714,6 +1715,42 @@ function publicSandboxIp(ip) {
|
|
|
1714
1715
|
}
|
|
1715
1716
|
var init_public_ip = () => {};
|
|
1716
1717
|
|
|
1718
|
+
// ../../packages/shared/src/sandbox.ts
|
|
1719
|
+
function sandboxConfig(raw) {
|
|
1720
|
+
const config = { resources: { ...SANDBOX_RESOURCES }, limits: { ...SANDBOX_LIMITS } };
|
|
1721
|
+
if (!raw)
|
|
1722
|
+
return config;
|
|
1723
|
+
const input = JSON.parse(raw);
|
|
1724
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1725
|
+
throw new Error("MYBASE_SANDBOX_CONFIG must be a JSON object");
|
|
1726
|
+
for (const group of Object.keys(input)) {
|
|
1727
|
+
if (group !== "resources" && group !== "limits")
|
|
1728
|
+
throw new Error(`unknown sandbox config group: ${group}`);
|
|
1729
|
+
const values = input[group];
|
|
1730
|
+
if (!values || typeof values !== "object" || Array.isArray(values))
|
|
1731
|
+
throw new Error(`sandbox ${group} must be an object`);
|
|
1732
|
+
const target = config[group];
|
|
1733
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1734
|
+
if (!Object.hasOwn(target, key))
|
|
1735
|
+
throw new Error(`unknown sandbox ${group} setting: ${key}`);
|
|
1736
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || key !== "cpus" && !Number.isSafeInteger(value))
|
|
1737
|
+
throw new Error(`sandbox ${key} must be a positive ${key === "cpus" ? "number" : "safe integer"}`);
|
|
1738
|
+
target[key] = value;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return config;
|
|
1742
|
+
}
|
|
1743
|
+
var SANDBOX_RESOURCES, SANDBOX_LIMITS;
|
|
1744
|
+
var init_sandbox = __esm(() => {
|
|
1745
|
+
SANDBOX_RESOURCES = { cpus: 1, memoryMb: 1024, workspaceMb: 128, pids: 256, tmpMb: 256, shmMb: 64 };
|
|
1746
|
+
SANDBOX_LIMITS = {
|
|
1747
|
+
fileBytes: 8 * 1024 * 1024,
|
|
1748
|
+
outputBytes: 64 * 1024,
|
|
1749
|
+
networkStreams: 32,
|
|
1750
|
+
networkIdleMs: 15000
|
|
1751
|
+
};
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1717
1754
|
// ../../docker/compose.yml
|
|
1718
1755
|
var compose_default = `# Runs mybase: the control plane (API + dashboard), the shared Postgres server, Garage
|
|
1719
1756
|
# (S3-compatible storage), Valkey (Redis-compatible cache), and SearXNG (web search). All images are multi-arch (arm64 + amd64).
|
|
@@ -1831,8 +1868,18 @@ services:
|
|
|
1831
1868
|
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
1832
1869
|
MYBASE_SANDBOX_WORKER_URL: \${MYBASE_SANDBOX_WORKER_URL:-}
|
|
1833
1870
|
MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
|
|
1871
|
+
MYBASE_SANDBOX_CONFIG: \${MYBASE_SANDBOX_CONFIG:-}
|
|
1872
|
+
# Hosting (see the hosting services below). Sites live at <site>.MYBASE_HOSTING_DOMAIN.
|
|
1873
|
+
MYBASE_HOSTING_WORKER_URL: \${MYBASE_HOSTING_WORKER_URL:-}
|
|
1874
|
+
MYBASE_HOSTING_WORKER_KEY: \${MYBASE_HOSTING_WORKER_KEY:-}
|
|
1875
|
+
MYBASE_HOSTING_DOMAIN: \${MYBASE_HOSTING_DOMAIN:-}
|
|
1876
|
+
MYBASE_HOSTING_MEMORY: \${MYBASE_HOSTING_MEMORY:-512m}
|
|
1877
|
+
MYBASE_HOSTING_PORT: \${MYBASE_HOSTING_PORT:-80}
|
|
1834
1878
|
ports:
|
|
1835
1879
|
- "\${MYBASE_BIND:-0.0.0.0}:\${MYBASE_PORT:-4000}:4000"
|
|
1880
|
+
volumes:
|
|
1881
|
+
# Caddy's admin socket (hosting). Only the API shares this volume.
|
|
1882
|
+
- caddy-admin:/run/caddy
|
|
1836
1883
|
|
|
1837
1884
|
# Official Postgres plus extensions projects can enable (see postgres.Dockerfile).
|
|
1838
1885
|
# Upgrading from postgres:17-alpine: the data volume is compatible, but text sort order
|
|
@@ -1918,6 +1965,7 @@ services:
|
|
|
1918
1965
|
environment:
|
|
1919
1966
|
MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
|
|
1920
1967
|
MYBASE_SANDBOX_DENY_HOSTS: \${MYBASE_SANDBOX_DENY_HOSTS:-}
|
|
1968
|
+
MYBASE_SANDBOX_CONFIG: \${MYBASE_SANDBOX_CONFIG:-}
|
|
1921
1969
|
MYBASE_SANDBOX_IMAGE: \${MYBASE_SANDBOX_IMAGE:-ghcr.io/landonharter/mybase-sandbox:latest}
|
|
1922
1970
|
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
1923
1971
|
volumes:
|
|
@@ -1929,6 +1977,60 @@ services:
|
|
|
1929
1977
|
start_period: 30s
|
|
1930
1978
|
# Private Compose network only. No published worker or browser-debugging ports.
|
|
1931
1979
|
|
|
1980
|
+
# Hosting (opt-in, COMPOSE_PROFILES=hosting; \`mybase server hosting\` sets it up). Sites run as
|
|
1981
|
+
# containers on this Docker host, one network per project; Caddy routes <site>.<domain> to them.
|
|
1982
|
+
# Image registry: internal. Pushes go through the API (\`docker login <mybase host>\` with an API
|
|
1983
|
+
# key); the Docker daemon pulls from 127.0.0.1:5000, which only this machine can reach.
|
|
1984
|
+
registry:
|
|
1985
|
+
image: registry:3.0.0
|
|
1986
|
+
profiles: [hosting]
|
|
1987
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
1988
|
+
environment:
|
|
1989
|
+
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
|
1990
|
+
# Upload URLs stay relative, so they work through the API's /v2 proxy.
|
|
1991
|
+
REGISTRY_HTTP_RELATIVEURLS: "true"
|
|
1992
|
+
OTEL_TRACES_EXPORTER: none
|
|
1993
|
+
ports:
|
|
1994
|
+
- "127.0.0.1:\${MYBASE_REGISTRY_PORT:-5000}:5000"
|
|
1995
|
+
volumes:
|
|
1996
|
+
- registry-data:/var/lib/registry
|
|
1997
|
+
|
|
1998
|
+
# Routes <site>.<domain> to each site's live container. Its config comes from the API, over an
|
|
1999
|
+
# admin socket on a volume only the API shares (Caddy is on every project network). TLS ends at
|
|
2000
|
+
# Cloudflare; on a LAN, sites answer on port 80 (MYBASE_HOSTING_PORT).
|
|
2001
|
+
caddy:
|
|
2002
|
+
image: caddy:2.10.2-alpine
|
|
2003
|
+
profiles: [hosting]
|
|
2004
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
2005
|
+
command: caddy run --resume --config /etc/caddy/initial.json
|
|
2006
|
+
configs:
|
|
2007
|
+
- source: caddy
|
|
2008
|
+
target: /etc/caddy/initial.json
|
|
2009
|
+
ports:
|
|
2010
|
+
- "\${MYBASE_HOSTING_BIND:-0.0.0.0}:\${MYBASE_HOSTING_PORT:-80}:80"
|
|
2011
|
+
volumes:
|
|
2012
|
+
- caddy-admin:/run/caddy
|
|
2013
|
+
- caddy-config:/config
|
|
2014
|
+
- caddy-data:/data
|
|
2015
|
+
|
|
2016
|
+
# Trusted worker that runs site containers. No site ever gets this socket or key, and the
|
|
2017
|
+
# worker isn't on project networks.
|
|
2018
|
+
hosting-worker:
|
|
2019
|
+
image: \${MYBASE_HOSTING_WORKER_IMAGE:-ghcr.io/landonharter/mybase-hosting-worker:latest}
|
|
2020
|
+
profiles: [hosting]
|
|
2021
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
2022
|
+
environment:
|
|
2023
|
+
MYBASE_HOSTING_WORKER_KEY: \${MYBASE_HOSTING_WORKER_KEY:-}
|
|
2024
|
+
MYBASE_COMPOSE_PROJECT: mybase
|
|
2025
|
+
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
2026
|
+
volumes:
|
|
2027
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
2028
|
+
healthcheck:
|
|
2029
|
+
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4020/health',{headers:{Authorization:'Bearer '+process.env.MYBASE_HOSTING_WORKER_KEY}}).then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
|
2030
|
+
interval: 30s
|
|
2031
|
+
timeout: 5s
|
|
2032
|
+
start_period: 20s
|
|
2033
|
+
|
|
1932
2034
|
# Cloudflare Tunnel: public HTTPS without opening router ports. Off unless COMPOSE_PROFILES=tunnel.
|
|
1933
2035
|
# Hostnames are set up in the Cloudflare dashboard (see the top of this file).
|
|
1934
2036
|
cloudflared:
|
|
@@ -1947,6 +2049,11 @@ services:
|
|
|
1947
2049
|
- mybase
|
|
1948
2050
|
|
|
1949
2051
|
configs:
|
|
2052
|
+
# Caddy's first config, until the API loads the real one (and after that, its autosave).
|
|
2053
|
+
caddy:
|
|
2054
|
+
content: |
|
|
2055
|
+
{"admin":{"listen":"unix//run/caddy/admin.sock|0666"},"apps":{"http":{"servers":{"sites":{"listen":[":80"],"automatic_https":{"disable":true}}}}}}
|
|
2056
|
+
|
|
1950
2057
|
garage:
|
|
1951
2058
|
content: |
|
|
1952
2059
|
metadata_dir = "/var/lib/garage/meta"
|
|
@@ -1991,6 +2098,10 @@ volumes:
|
|
|
1991
2098
|
garage-meta:
|
|
1992
2099
|
garage-data:
|
|
1993
2100
|
valkey-data:
|
|
2101
|
+
registry-data:
|
|
2102
|
+
caddy-admin:
|
|
2103
|
+
caddy-config:
|
|
2104
|
+
caddy-data:
|
|
1994
2105
|
`;
|
|
1995
2106
|
var init_compose = () => {};
|
|
1996
2107
|
|
|
@@ -2100,7 +2211,7 @@ function versionAtLeast(version, min) {
|
|
|
2100
2211
|
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
2101
2212
|
return true;
|
|
2102
2213
|
}
|
|
2103
|
-
var SECRET_KEYS, SECTIONS, LINE, isSecretKey = (key) => (key in SECRET_KEYS) || key === "CLOUDFLARE_TUNNEL_TOKEN" || key === "MYBASE_SANDBOX_WORKER_KEY";
|
|
2214
|
+
var SECRET_KEYS, SECTIONS, LINE, isSecretKey = (key) => (key in SECRET_KEYS) || key === "CLOUDFLARE_TUNNEL_TOKEN" || key === "MYBASE_SANDBOX_WORKER_KEY" || key === "MYBASE_HOSTING_WORKER_KEY";
|
|
2104
2215
|
var init_server_env = __esm(() => {
|
|
2105
2216
|
SECRET_KEYS = {
|
|
2106
2217
|
POSTGRES_PASSWORD: { bytes: 24, encoding: "hex" },
|
|
@@ -2135,7 +2246,7 @@ var init_server_env = __esm(() => {
|
|
|
2135
2246
|
});
|
|
2136
2247
|
|
|
2137
2248
|
// src/server-docker.ts
|
|
2138
|
-
import { existsSync, readFileSync } from "fs";
|
|
2249
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
2139
2250
|
import { statfs } from "fs/promises";
|
|
2140
2251
|
import { freemem, hostname as hostname2, networkInterfaces, totalmem } from "os";
|
|
2141
2252
|
function run2(cmd, cwd) {
|
|
@@ -2181,7 +2292,7 @@ async function compose(dir, args) {
|
|
|
2181
2292
|
return { ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
2182
2293
|
}
|
|
2183
2294
|
async function stackExists(dir) {
|
|
2184
|
-
if (!
|
|
2295
|
+
if (!existsSync2(`${dir}/compose.yml`))
|
|
2185
2296
|
return false;
|
|
2186
2297
|
const ps = await compose(dir, ["ps", "-a", "-q"]);
|
|
2187
2298
|
return ps.ok && ps.stdout.length > 0;
|
|
@@ -2233,7 +2344,7 @@ function machine() {
|
|
|
2233
2344
|
}
|
|
2234
2345
|
async function freeDiskGb(dir) {
|
|
2235
2346
|
for (let d = dir;; d = d.replace(/\/[^/]*$/, "") || "/") {
|
|
2236
|
-
if (
|
|
2347
|
+
if (existsSync2(d)) {
|
|
2237
2348
|
try {
|
|
2238
2349
|
const s = await statfs(d);
|
|
2239
2350
|
return s.bavail * s.bsize / 2 ** 30;
|
|
@@ -2330,7 +2441,7 @@ var MIN_COMPOSE = "2.23.1", DOCKER_INSTALL_URL = "https://docs.docker.com/engine
|
|
|
2330
2441
|
var init_server_docker = __esm(() => {
|
|
2331
2442
|
init_compose();
|
|
2332
2443
|
init_server_env();
|
|
2333
|
-
LOG_SERVICES = ["mybase", "postgres", "garage", "valkey", "searxng", "cloudflared", "sandbox-worker"];
|
|
2444
|
+
LOG_SERVICES = ["mybase", "postgres", "garage", "valkey", "searxng", "cloudflared", "sandbox-worker", "hosting-worker", "caddy", "registry"];
|
|
2334
2445
|
LOG_ALIASES = {
|
|
2335
2446
|
api: "mybase",
|
|
2336
2447
|
dashboard: "mybase",
|
|
@@ -2342,16 +2453,18 @@ var init_server_docker = __esm(() => {
|
|
|
2342
2453
|
search: "searxng",
|
|
2343
2454
|
tunnel: "cloudflared",
|
|
2344
2455
|
sandbox: "sandbox-worker",
|
|
2345
|
-
worker: "sandbox-worker"
|
|
2456
|
+
worker: "sandbox-worker",
|
|
2457
|
+
hosting: "hosting-worker",
|
|
2458
|
+
proxy: "caddy"
|
|
2346
2459
|
};
|
|
2347
2460
|
});
|
|
2348
2461
|
|
|
2349
2462
|
// src/server-gvisor.ts
|
|
2350
|
-
import { createHash } from "crypto";
|
|
2351
|
-
import { access, mkdtemp, rm } from "fs/promises";
|
|
2463
|
+
import { createHash as createHash2 } from "crypto";
|
|
2464
|
+
import { access, mkdtemp, rm as rm3 } from "fs/promises";
|
|
2352
2465
|
import { constants as constants2 } from "fs";
|
|
2353
|
-
import { tmpdir } from "os";
|
|
2354
|
-
import { join as
|
|
2466
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2467
|
+
import { join as join5 } from "path";
|
|
2355
2468
|
async function tail(stream) {
|
|
2356
2469
|
const decoder = new TextDecoder;
|
|
2357
2470
|
let text = "";
|
|
@@ -2405,7 +2518,7 @@ async function installGvisor(info, run = hostRun) {
|
|
|
2405
2518
|
break;
|
|
2406
2519
|
} catch {}
|
|
2407
2520
|
}
|
|
2408
|
-
const root = await mkdtemp(
|
|
2521
|
+
const root = await mkdtemp(join5(tmpdir3(), "mybase-gvisor-"));
|
|
2409
2522
|
try {
|
|
2410
2523
|
let archive = null;
|
|
2411
2524
|
if (!binary) {
|
|
@@ -2418,9 +2531,9 @@ async function installGvisor(info, run = hostRun) {
|
|
|
2418
2531
|
const response = await fetch(`${base}/gvisor.tar.bz2`, { signal: AbortSignal.timeout(180000) });
|
|
2419
2532
|
if (!response.ok || !response.body)
|
|
2420
2533
|
throw new CliError("couldn't download the gVisor release bundle");
|
|
2421
|
-
archive =
|
|
2534
|
+
archive = join5(root, "gvisor.tar.bz2");
|
|
2422
2535
|
const sink = Bun.file(archive).writer();
|
|
2423
|
-
const hash =
|
|
2536
|
+
const hash = createHash2("sha512");
|
|
2424
2537
|
let size = 0;
|
|
2425
2538
|
try {
|
|
2426
2539
|
for await (const chunk of response.body) {
|
|
@@ -2463,7 +2576,7 @@ async function installGvisor(info, run = hostRun) {
|
|
|
2463
2576
|
throw new CliError("gVisor was installed, but Docker's systemd service couldn't reload. Reload the daemon's runtime configuration on this host, then rerun this command; existing containers were not restarted");
|
|
2464
2577
|
return true;
|
|
2465
2578
|
} finally {
|
|
2466
|
-
await
|
|
2579
|
+
await rm3(root, { recursive: true, force: true });
|
|
2467
2580
|
}
|
|
2468
2581
|
}
|
|
2469
2582
|
var hostRun = async (args, options = {}) => {
|
|
@@ -2600,6 +2713,10 @@ function cloudflare(token, fetcher = fetch, permissions = TOKEN_PERMISSIONS) {
|
|
|
2600
2713
|
putIngress: (accountId, tunnelId, rules) => call("PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {
|
|
2601
2714
|
config: { ingress: [...rules, { service: "http_status:404" }] }
|
|
2602
2715
|
}),
|
|
2716
|
+
async getIngress(accountId, tunnelId) {
|
|
2717
|
+
const result = await call("GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`);
|
|
2718
|
+
return (result.config?.ingress ?? []).flatMap((r) => r.hostname ? [{ hostname: r.hostname, service: r.service }] : []);
|
|
2719
|
+
},
|
|
2603
2720
|
async findRecord(zoneId, name) {
|
|
2604
2721
|
const records = await call("GET", `/zones/${zoneId}/dns_records?name=${encodeURIComponent(name)}`);
|
|
2605
2722
|
return records[0] ?? null;
|
|
@@ -2615,6 +2732,11 @@ function cloudflare(token, fetcher = fetch, permissions = TOKEN_PERMISSIONS) {
|
|
|
2615
2732
|
tunnelToken: (accountId, tunnelId) => call("GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`)
|
|
2616
2733
|
};
|
|
2617
2734
|
}
|
|
2735
|
+
function mergeIngress(existing, updates, remove = []) {
|
|
2736
|
+
const drop = new Set([...updates.map((r) => r.hostname), ...remove]);
|
|
2737
|
+
const all = [...updates, ...existing.filter((r) => !drop.has(r.hostname))];
|
|
2738
|
+
return [...all.filter((r) => !r.hostname.startsWith("*.")), ...all.filter((r) => r.hostname.startsWith("*."))];
|
|
2739
|
+
}
|
|
2618
2740
|
function zoneOf(zones, host) {
|
|
2619
2741
|
const name = host.toLowerCase().replace(/\.$/, "");
|
|
2620
2742
|
let best = null;
|
|
@@ -2641,7 +2763,7 @@ var init_cloudflare2 = __esm(() => {
|
|
|
2641
2763
|
// src/server-setup.ts
|
|
2642
2764
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
2643
2765
|
import { homedir as homedir2, hostname as hostname3 } from "os";
|
|
2644
|
-
import { join as
|
|
2766
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
2645
2767
|
import { styleText as styleText3 } from "util";
|
|
2646
2768
|
function flag(args, name) {
|
|
2647
2769
|
const i = args.indexOf(name);
|
|
@@ -2655,7 +2777,7 @@ function flag(args, name) {
|
|
|
2655
2777
|
async function serverDir(args) {
|
|
2656
2778
|
const given = flag(args, "--dir");
|
|
2657
2779
|
if (given)
|
|
2658
|
-
return
|
|
2780
|
+
return resolve5(given.replace(/^~(?=$|\/)/, homedir2()));
|
|
2659
2781
|
return (await loadConfig()).server ?? DEFAULT_DIR;
|
|
2660
2782
|
}
|
|
2661
2783
|
async function ask(answer) {
|
|
@@ -2694,13 +2816,13 @@ async function preflight(dir) {
|
|
|
2694
2816
|
async function chooseDir(args, interactive) {
|
|
2695
2817
|
let dir = await serverDir(args);
|
|
2696
2818
|
if (interactive && !args.includes("--dir")) {
|
|
2697
|
-
const answer = await ask(
|
|
2819
|
+
const answer = await ask(text2({
|
|
2698
2820
|
message: "Where should mybase keep its files?",
|
|
2699
2821
|
placeholder: tilde(dir),
|
|
2700
2822
|
defaultValue: dir,
|
|
2701
2823
|
validate: (v) => v && !v.trim().startsWith("/") && !v.trim().startsWith("~") ? "use an absolute path (or ~/\u2026)" : undefined
|
|
2702
2824
|
}));
|
|
2703
|
-
dir =
|
|
2825
|
+
dir = resolve5(answer.trim().replace(/^~(?=$|\/)/, homedir2()));
|
|
2704
2826
|
}
|
|
2705
2827
|
const other = otherInstall(dir);
|
|
2706
2828
|
if (other) {
|
|
@@ -2717,7 +2839,7 @@ async function chooseHost(current) {
|
|
|
2717
2839
|
const picked = await ask(select({ message: "Which address should apps on your network use?", options, initialValue: current ?? options[0]?.value }));
|
|
2718
2840
|
if (picked)
|
|
2719
2841
|
return picked;
|
|
2720
|
-
return (await ask(
|
|
2842
|
+
return (await ask(text2({ message: "Address (hostname or IP)", validate: (v) => v?.trim() ? undefined : "required" }))).trim();
|
|
2721
2843
|
}
|
|
2722
2844
|
async function tunnelStep() {
|
|
2723
2845
|
const mode = await ask(select({
|
|
@@ -2778,8 +2900,8 @@ async function autoTunnel() {
|
|
|
2778
2900
|
let apiHost = "";
|
|
2779
2901
|
let s3Host = "";
|
|
2780
2902
|
for (;; ) {
|
|
2781
|
-
const apiSub = await ask(
|
|
2782
|
-
const s3Sub = await ask(
|
|
2903
|
+
const apiSub = await ask(text2({ message: "Subdomain for the dashboard + API", placeholder: "mybase", defaultValue: "mybase" }));
|
|
2904
|
+
const s3Sub = await ask(text2({ message: "Subdomain for S3 storage", placeholder: "s3", defaultValue: "s3" }));
|
|
2783
2905
|
apiHost = `${apiSub.trim()}.${zone.name}`;
|
|
2784
2906
|
s3Host = `${s3Sub.trim()}.${zone.name}`;
|
|
2785
2907
|
if (validHost(apiHost) || validHost(s3Host) || apiHost === s3Host) {
|
|
@@ -2800,10 +2922,11 @@ async function autoTunnel() {
|
|
|
2800
2922
|
}
|
|
2801
2923
|
s.start("Routing hostnames to the tunnel");
|
|
2802
2924
|
try {
|
|
2803
|
-
await cf.
|
|
2925
|
+
const current = tunnel.created ? [] : await cf.getIngress(zone.account.id, tunnel.id).catch(() => []);
|
|
2926
|
+
await cf.putIngress(zone.account.id, tunnel.id, mergeIngress(current, [
|
|
2804
2927
|
{ hostname: apiHost, service: "http://mybase:4000" },
|
|
2805
2928
|
{ hostname: s3Host, service: "http://garage:3900" }
|
|
2806
|
-
]);
|
|
2929
|
+
]));
|
|
2807
2930
|
await cf.upsertCname(zone.id, apiHost, tunnel.id);
|
|
2808
2931
|
await cf.upsertCname(zone.id, s3Host, tunnel.id);
|
|
2809
2932
|
const tunnelToken = await cf.tunnelToken(zone.account.id, tunnel.id);
|
|
@@ -2830,9 +2953,9 @@ async function manualTunnel() {
|
|
|
2830
2953
|
].join(`
|
|
2831
2954
|
`), "Cloudflare Tunnel, by hand");
|
|
2832
2955
|
const token = await ask(password({ message: "Tunnel token", validate: (v) => tunnelIdFromToken(v ?? "") ? undefined : "that isn't a tunnel token (copy the part after --token)" }));
|
|
2833
|
-
const apiHost = (await ask(
|
|
2956
|
+
const apiHost = (await ask(text2({ message: "Hostname for the dashboard + API", placeholder: "mybase.example.com", validate: validHost }))).trim();
|
|
2834
2957
|
const defaultS3 = apiHost.replace(/^[^.]+/, "s3");
|
|
2835
|
-
const s3Host = (await ask(
|
|
2958
|
+
const s3Host = (await ask(text2({ message: "Hostname for S3", placeholder: defaultS3, defaultValue: defaultS3, validate: (v) => validHost(v || defaultS3) }))).trim();
|
|
2836
2959
|
return { env: tunnelEnv(token.trim(), apiHost, s3Host), apiHost, s3Host };
|
|
2837
2960
|
}
|
|
2838
2961
|
async function bindStep(lan, existing) {
|
|
@@ -2878,7 +3001,7 @@ async function dbPublicStep(existing) {
|
|
|
2878
3001
|
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 }));
|
|
2879
3002
|
const chosen = zones.find((z) => z.id === zoneId);
|
|
2880
3003
|
const suggested = existing.MYBASE_DB_PUBLIC_HOST && zoneOf([chosen], existing.MYBASE_DB_PUBLIC_HOST) ? existing.MYBASE_DB_PUBLIC_HOST : `db.${chosen.name}`;
|
|
2881
|
-
const host = (await ask(
|
|
3004
|
+
const host = (await ask(text2({
|
|
2882
3005
|
message: "Hostname for the database",
|
|
2883
3006
|
placeholder: suggested,
|
|
2884
3007
|
defaultValue: suggested,
|
|
@@ -2896,7 +3019,7 @@ async function dbPublicStep(existing) {
|
|
|
2896
3019
|
continue;
|
|
2897
3020
|
await cf.deleteRecord(chosen.id, record.id);
|
|
2898
3021
|
}
|
|
2899
|
-
const email = (await ask(
|
|
3022
|
+
const email = (await ask(text2({ message: "Email for Let's Encrypt expiry notices (optional)", placeholder: "skip" }))).trim();
|
|
2900
3023
|
return {
|
|
2901
3024
|
MYBASE_DB_PUBLIC: "on",
|
|
2902
3025
|
MYBASE_DB_PUBLIC_HOST: host,
|
|
@@ -2916,7 +3039,7 @@ async function adminStep() {
|
|
|
2916
3039
|
}));
|
|
2917
3040
|
if (when === "later")
|
|
2918
3041
|
return null;
|
|
2919
|
-
const username = (await ask(
|
|
3042
|
+
const username = (await ask(text2({ message: "Admin username", placeholder: "admin", defaultValue: "admin", validate: (v) => !v || USERNAME_PATTERN.test(v) ? undefined : "letters, digits, or . _ @ -" }))).trim();
|
|
2920
3043
|
for (;; ) {
|
|
2921
3044
|
const password2 = await ask(password({ message: `Password (at least ${ADMIN_PASSWORD_MIN} characters)`, validate: (v) => (v?.length ?? 0) >= ADMIN_PASSWORD_MIN ? undefined : `at least ${ADMIN_PASSWORD_MIN} characters` }));
|
|
2922
3045
|
const again = await ask(password({ message: "Same password again" }));
|
|
@@ -2969,7 +3092,7 @@ async function advancedStep(env) {
|
|
|
2969
3092
|
else if (env[key] !== undefined)
|
|
2970
3093
|
out[key] = null;
|
|
2971
3094
|
};
|
|
2972
|
-
const port = (key, label, fallback) => ask(
|
|
3095
|
+
const port = (key, label, fallback) => ask(text2({ message: `${label} port`, placeholder: env[key] ?? fallback, defaultValue: env[key] ?? fallback, validate: (v) => v ? validPort(v) : undefined })).then((v) => set(key, v.trim(), fallback));
|
|
2973
3096
|
await port("MYBASE_PORT", "Dashboard + API", "4000");
|
|
2974
3097
|
await port("POSTGRES_PORT", "Postgres", "5432");
|
|
2975
3098
|
await port("GARAGE_S3_PORT", "S3", "3900");
|
|
@@ -2980,7 +3103,7 @@ async function advancedStep(env) {
|
|
|
2980
3103
|
options: ["64mb", "128mb", "256mb", "512mb", "1gb"].map((v) => ({ value: v, label: v, hint: v === "64mb" ? "default, fine for a Pi" : undefined }))
|
|
2981
3104
|
}));
|
|
2982
3105
|
set("CACHE_MAXMEMORY", memory, "64mb");
|
|
2983
|
-
const capacity = await ask(
|
|
3106
|
+
const capacity = await ask(text2({ message: "Storage capacity Garage advertises", placeholder: env.GARAGE_CAPACITY ?? "100G", defaultValue: env.GARAGE_CAPACITY ?? "100G", validate: (v) => !v || /^\d+[KMGT]?$/i.test(v) ? undefined : "like 100G or 2T" }));
|
|
2984
3107
|
set("GARAGE_CAPACITY", capacity.trim(), "100G");
|
|
2985
3108
|
const level = await ask(select({ message: "Log level", initialValue: env.LOG_LEVEL ?? "info", options: ["debug", "info", "warn", "error"].map((v) => ({ value: v, label: v })) }));
|
|
2986
3109
|
set("LOG_LEVEL", level, "info");
|
|
@@ -3003,28 +3126,28 @@ async function portStep(env, interactive) {
|
|
|
3003
3126
|
let next = Number(current) + 1;
|
|
3004
3127
|
while (!portFree(next) && next < 65535)
|
|
3005
3128
|
next++;
|
|
3006
|
-
const answer = await ask(
|
|
3129
|
+
const answer = await ask(text2({ message: `Port ${current} (${label}) is in use by something else. Use which port?`, placeholder: String(next), defaultValue: String(next), validate: (v) => v ? validPort(v) : undefined }));
|
|
3007
3130
|
out[key] = answer.trim();
|
|
3008
3131
|
}
|
|
3009
3132
|
return out;
|
|
3010
3133
|
}
|
|
3011
3134
|
async function writeFiles(dir, envText, interactive, replaceCompose = interactive) {
|
|
3012
3135
|
await mkdir3(dir, { recursive: true });
|
|
3013
|
-
const envPath =
|
|
3136
|
+
const envPath = join6(dir, ".env");
|
|
3014
3137
|
await Bun.write(envPath, envText);
|
|
3015
3138
|
await chmod2(envPath, 384);
|
|
3016
3139
|
await writeCompose(dir, interactive, replaceCompose);
|
|
3017
3140
|
}
|
|
3018
3141
|
async function writeCompose(dir, interactive, replace) {
|
|
3019
|
-
const file = Bun.file(
|
|
3142
|
+
const file = Bun.file(join6(dir, "compose.yml"));
|
|
3020
3143
|
if (await file.exists()) {
|
|
3021
3144
|
const current = await file.text();
|
|
3022
3145
|
if (current === compose_default)
|
|
3023
3146
|
return false;
|
|
3024
|
-
const ok = interactive ? await ask(confirm2({ message: `${tilde(
|
|
3147
|
+
const ok = interactive ? await ask(confirm2({ message: `${tilde(join6(dir, "compose.yml"))} differs from this CLI's (${CLI_VERSION}). Replace it? (the old one is kept as compose.yml.bak)` })) : replace;
|
|
3025
3148
|
if (!ok)
|
|
3026
3149
|
return false;
|
|
3027
|
-
await Bun.write(
|
|
3150
|
+
await Bun.write(join6(dir, "compose.yml.bak"), current);
|
|
3028
3151
|
}
|
|
3029
3152
|
await Bun.write(file, compose_default);
|
|
3030
3153
|
return true;
|
|
@@ -3079,7 +3202,7 @@ ${tail2(pulled.stderr || pulled.stdout)}${pullHint(pulled.stderr || pulled.stdou
|
|
|
3079
3202
|
} else
|
|
3080
3203
|
s.stop("Images downloaded");
|
|
3081
3204
|
}
|
|
3082
|
-
const current = parseEnv(await Bun.file(
|
|
3205
|
+
const current = parseEnv(await Bun.file(join6(dir, ".env")).text());
|
|
3083
3206
|
if (sandboxProfileOn(current)) {
|
|
3084
3207
|
s.start("Updating sandbox runtime images");
|
|
3085
3208
|
await init_server_sandbox();
|
|
@@ -3152,7 +3275,7 @@ async function setup(args) {
|
|
|
3152
3275
|
${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any time.")}`);
|
|
3153
3276
|
const dir = await chooseDir(args, interactive);
|
|
3154
3277
|
await preflight(dir);
|
|
3155
|
-
const envFile = Bun.file(
|
|
3278
|
+
const envFile = Bun.file(join6(dir, ".env"));
|
|
3156
3279
|
const envText = await envFile.exists() ? await envFile.text() : null;
|
|
3157
3280
|
const existing = envText ? parseEnv(envText) : {};
|
|
3158
3281
|
const running = await stackExists(dir);
|
|
@@ -3269,7 +3392,7 @@ ${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any tim
|
|
|
3269
3392
|
ui.note([
|
|
3270
3393
|
...Object.entries(values).map(([k, v]) => `${k.padEnd(24)} ${isSecretKey(k) ? dim(maskSecret(v)) : v}`),
|
|
3271
3394
|
"",
|
|
3272
|
-
dim(`Writes ${tilde(
|
|
3395
|
+
dim(`Writes ${tilde(join6(dir, ".env"))} (only you can read it) and ${tilde(join6(dir, "compose.yml"))}`)
|
|
3273
3396
|
].join(`
|
|
3274
3397
|
`), "Settings");
|
|
3275
3398
|
if (interactive && !await ask(confirm2({ message: running ? "Save and restart mybase?" : "Save and start mybase?" }))) {
|
|
@@ -3320,10 +3443,10 @@ ${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any tim
|
|
|
3320
3443
|
].join(`
|
|
3321
3444
|
`), "mybase is running");
|
|
3322
3445
|
if (secrets.MYBASE_SECRETS_KEY) {
|
|
3323
|
-
ui.warn(`${bold("Back up MYBASE_SECRETS_KEY")} (in ${tilde(
|
|
3446
|
+
ui.warn(`${bold("Back up MYBASE_SECRETS_KEY")} (in ${tilde(join6(dir, ".env"))}) somewhere other than this machine,
|
|
3324
3447
|
like a password manager. If it's lost, every stored secret is lost with it.`);
|
|
3325
3448
|
if (interactive && await ask(confirm2({ message: "Show it now so you can copy it?", initialValue: false }))) {
|
|
3326
|
-
|
|
3449
|
+
log2.message(secrets.MYBASE_SECRETS_KEY);
|
|
3327
3450
|
}
|
|
3328
3451
|
}
|
|
3329
3452
|
outro(`Done. Config for this CLI: ${dim(tilde(configPath))}`);
|
|
@@ -3342,17 +3465,17 @@ var init_server_setup = __esm(async () => {
|
|
|
3342
3465
|
init_server_env();
|
|
3343
3466
|
init_server_sandbox_env();
|
|
3344
3467
|
await init_api();
|
|
3345
|
-
DEFAULT_DIR =
|
|
3468
|
+
DEFAULT_DIR = join6(homedir2(), "mybase");
|
|
3346
3469
|
USERNAME_PATTERN = /^[a-zA-Z0-9_.@-]{1,64}$/;
|
|
3347
3470
|
HOST_PATTERN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
3348
3471
|
TUNNEL_KEYS = ["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY"];
|
|
3349
3472
|
BIND_KEYS = ["POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"];
|
|
3350
3473
|
ui = {
|
|
3351
|
-
info: (m) => !jsonMode &&
|
|
3352
|
-
success: (m) => !jsonMode &&
|
|
3353
|
-
warn: (m) => !jsonMode &&
|
|
3354
|
-
error: (m) => !jsonMode &&
|
|
3355
|
-
step: (m) => !jsonMode &&
|
|
3474
|
+
info: (m) => !jsonMode && log2.info(m),
|
|
3475
|
+
success: (m) => !jsonMode && log2.success(m),
|
|
3476
|
+
warn: (m) => !jsonMode && log2.warn(m),
|
|
3477
|
+
error: (m) => !jsonMode && log2.error(m),
|
|
3478
|
+
step: (m) => !jsonMode && log2.step(m),
|
|
3356
3479
|
note: (m, title) => !jsonMode && note(m, title),
|
|
3357
3480
|
spinner() {
|
|
3358
3481
|
return jsonMode ? { start() {}, stop() {}, error() {}, message() {} } : spinner();
|
|
@@ -3369,7 +3492,7 @@ __export(exports_server_sandbox, {
|
|
|
3369
3492
|
sandboxWorkerHealth: () => sandboxWorkerHealth,
|
|
3370
3493
|
setupSandboxServer: () => setupSandboxServer
|
|
3371
3494
|
});
|
|
3372
|
-
import { join as
|
|
3495
|
+
import { join as join7 } from "path";
|
|
3373
3496
|
import { isIP as isIP3 } from "net";
|
|
3374
3497
|
async function publicIp() {
|
|
3375
3498
|
try {
|
|
@@ -3408,8 +3531,8 @@ async function refreshSandboxImages(dir, values, pull = true, run = hostRun) {
|
|
|
3408
3531
|
await pullImage(images.workerImage, run, pull);
|
|
3409
3532
|
const updates = { MYBASE_SANDBOX_IMAGE: images.image, MYBASE_SANDBOX_WORKER_IMAGE: images.workerImage };
|
|
3410
3533
|
if (Object.entries(updates).some(([key, value]) => values[key] !== value)) {
|
|
3411
|
-
const text = await Bun.file(
|
|
3412
|
-
await Bun.write(
|
|
3534
|
+
const text = await Bun.file(join7(dir, ".env")).text();
|
|
3535
|
+
await Bun.write(join7(dir, ".env"), mergeEnv(text, updates));
|
|
3413
3536
|
}
|
|
3414
3537
|
return { ...values, ...updates };
|
|
3415
3538
|
}
|
|
@@ -3424,7 +3547,7 @@ async function sandboxWorkerHealth(dir, run = hostRun) {
|
|
|
3424
3547
|
return null;
|
|
3425
3548
|
}
|
|
3426
3549
|
}
|
|
3427
|
-
async function probeRuntime(image, run) {
|
|
3550
|
+
async function probeRuntime(image, run, resources) {
|
|
3428
3551
|
const name = `mybase-sandbox-probe-${crypto.randomUUID()}`;
|
|
3429
3552
|
const code = "const timer=setTimeout(()=>process.exit(1),15000);const {chromium}=require('/opt/runtime/node_modules/playwright');chromium.launch({headless:true,chromiumSandbox:false,args:['--disable-dev-shm-usage']}).then(async b=>{await b.close();clearTimeout(timer)}).catch(()=>process.exit(1))";
|
|
3430
3553
|
try {
|
|
@@ -3439,16 +3562,17 @@ async function probeRuntime(image, run) {
|
|
|
3439
3562
|
"--read-only",
|
|
3440
3563
|
"--cap-drop=ALL",
|
|
3441
3564
|
"--security-opt=no-new-privileges:true",
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3565
|
+
`--cpus=${resources.cpus}`,
|
|
3566
|
+
`--memory=${resources.memoryMb}m`,
|
|
3567
|
+
`--memory-swap=${resources.memoryMb}m`,
|
|
3568
|
+
`--pids-limit=${resources.pids}`,
|
|
3569
|
+
`--ulimit=nproc=${resources.pids}:${resources.pids}`,
|
|
3447
3570
|
"--log-driver=none",
|
|
3448
3571
|
"--tmpfs",
|
|
3449
|
-
|
|
3572
|
+
`/tmp:rw,nosuid,nodev,size=${resources.tmpMb}m,uid=1000,gid=1000,mode=0700`,
|
|
3450
3573
|
"--tmpfs",
|
|
3451
|
-
|
|
3574
|
+
`/workspace:rw,nosuid,nodev,size=${resources.workspaceMb}m,uid=1000,gid=1000,mode=0700`,
|
|
3575
|
+
`--shm-size=${resources.shmMb}m`,
|
|
3452
3576
|
"--entrypoint",
|
|
3453
3577
|
"node",
|
|
3454
3578
|
image,
|
|
@@ -3466,11 +3590,12 @@ Check the host's gVisor and cgroup support. Ordinary containers will not be used
|
|
|
3466
3590
|
async function prepareSandboxHost(args, options = {}, overrides = {}) {
|
|
3467
3591
|
const io = { ...defaults, ...overrides };
|
|
3468
3592
|
const dir = await serverDir(args);
|
|
3469
|
-
const file = Bun.file(
|
|
3470
|
-
if (!await file.exists() || !await Bun.file(
|
|
3593
|
+
const file = Bun.file(join7(dir, ".env"));
|
|
3594
|
+
if (!await file.exists() || !await Bun.file(join7(dir, "compose.yml")).exists())
|
|
3471
3595
|
throw new CliError(`no mybase server in ${dir}; run mybase server setup --sandboxes first`, 4);
|
|
3472
3596
|
const text = await file.text();
|
|
3473
3597
|
const values = parseEnv(text);
|
|
3598
|
+
const config = sandboxConfig(values.MYBASE_SANDBOX_CONFIG);
|
|
3474
3599
|
const interactive = !jsonMode && process.stdin.isTTY === true && !args.includes("--yes");
|
|
3475
3600
|
if (!options.confirmed && !args.includes("--yes")) {
|
|
3476
3601
|
if (!interactive)
|
|
@@ -3517,7 +3642,7 @@ async function prepareSandboxHost(args, options = {}, overrides = {}) {
|
|
|
3517
3642
|
await pullImage(settings.MYBASE_SANDBOX_IMAGE, io.run, false);
|
|
3518
3643
|
await pullImage(settings.MYBASE_SANDBOX_WORKER_IMAGE, io.run, false);
|
|
3519
3644
|
s.message("Verifying Chromium under gVisor with bounded resources");
|
|
3520
|
-
await probeRuntime(settings.MYBASE_SANDBOX_IMAGE, io.run);
|
|
3645
|
+
await probeRuntime(settings.MYBASE_SANDBOX_IMAGE, io.run, config.resources);
|
|
3521
3646
|
await writeFiles(dir, mergeEnv(text, settings), interactive, true);
|
|
3522
3647
|
const current = { ...values, ...settings };
|
|
3523
3648
|
const url = localUrl(current);
|
|
@@ -3567,6 +3692,7 @@ var WORKER_HEALTH = "fetch('http://127.0.0.1:4010/health',{headers:{Authorizatio
|
|
|
3567
3692
|
var init_server_sandbox = __esm(async () => {
|
|
3568
3693
|
init_dist4();
|
|
3569
3694
|
init_public_ip();
|
|
3695
|
+
init_sandbox();
|
|
3570
3696
|
init_src();
|
|
3571
3697
|
init_output();
|
|
3572
3698
|
init_server_docker();
|
|
@@ -4881,6 +5007,611 @@ async function doctor(args) {
|
|
|
4881
5007
|
}
|
|
4882
5008
|
}
|
|
4883
5009
|
|
|
5010
|
+
// src/hosting.ts
|
|
5011
|
+
await init_api();
|
|
5012
|
+
import { existsSync } from "fs";
|
|
5013
|
+
import { rm as rm2 } from "fs/promises";
|
|
5014
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5015
|
+
import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
|
|
5016
|
+
|
|
5017
|
+
// src/oci-push.ts
|
|
5018
|
+
import { createHash } from "crypto";
|
|
5019
|
+
import { tmpdir } from "os";
|
|
5020
|
+
import { join as join3 } from "path";
|
|
5021
|
+
import { rm } from "fs/promises";
|
|
5022
|
+
var CHUNK_BYTES = 32 * 1024 * 1024;
|
|
5023
|
+
var text = (b) => new TextDecoder().decode(b).replace(/\0.*$/s, "");
|
|
5024
|
+
function tarSize(field) {
|
|
5025
|
+
if (field[0] & 128) {
|
|
5026
|
+
let n = 0;
|
|
5027
|
+
for (let i = 1;i < field.length; i++)
|
|
5028
|
+
n = n * 256 + field[i];
|
|
5029
|
+
return n;
|
|
5030
|
+
}
|
|
5031
|
+
return parseInt(text(field).trim() || "0", 8);
|
|
5032
|
+
}
|
|
5033
|
+
async function readTar(path) {
|
|
5034
|
+
const file = Bun.file(path);
|
|
5035
|
+
const entries = new Map;
|
|
5036
|
+
let at = 0;
|
|
5037
|
+
let longName = null;
|
|
5038
|
+
while (at + 512 <= file.size) {
|
|
5039
|
+
const h = new Uint8Array(await file.slice(at, at + 512).arrayBuffer());
|
|
5040
|
+
if (h.every((b) => b === 0))
|
|
5041
|
+
break;
|
|
5042
|
+
const size = tarSize(h.subarray(124, 136));
|
|
5043
|
+
const type = String.fromCharCode(h[156]);
|
|
5044
|
+
const prefix = text(h.subarray(345, 500));
|
|
5045
|
+
let name = longName ?? (prefix ? `${prefix}/${text(h.subarray(0, 100))}` : text(h.subarray(0, 100)));
|
|
5046
|
+
const body = at + 512;
|
|
5047
|
+
if (type === "L") {
|
|
5048
|
+
longName = text(new Uint8Array(await file.slice(body, body + size).arrayBuffer()));
|
|
5049
|
+
} else if (type === "x") {
|
|
5050
|
+
const pax = new TextDecoder().decode(await file.slice(body, body + size).arrayBuffer());
|
|
5051
|
+
longName = /\d+ path=([^\n]*)\n/.exec(pax)?.[1] ?? null;
|
|
5052
|
+
} else {
|
|
5053
|
+
longName = null;
|
|
5054
|
+
name = name.replace(/^\.\//, "");
|
|
5055
|
+
if (type === "0" || type === "\x00")
|
|
5056
|
+
entries.set(name, { name, offset: body, size });
|
|
5057
|
+
}
|
|
5058
|
+
at = body + Math.ceil(size / 512) * 512;
|
|
5059
|
+
}
|
|
5060
|
+
return entries;
|
|
5061
|
+
}
|
|
5062
|
+
async function savedImage(tar, entries) {
|
|
5063
|
+
const read = async (name) => {
|
|
5064
|
+
const e = entries.get(name);
|
|
5065
|
+
if (!e)
|
|
5066
|
+
throw new Error(`the saved image is missing ${name}`);
|
|
5067
|
+
return new Uint8Array(await Bun.file(tar).slice(e.offset, e.offset + e.size).arrayBuffer());
|
|
5068
|
+
};
|
|
5069
|
+
const manifest = JSON.parse(new TextDecoder().decode(await read("manifest.json")));
|
|
5070
|
+
if (manifest.length !== 1)
|
|
5071
|
+
throw new Error("expected exactly one image in the saved tar");
|
|
5072
|
+
const bytes = await read(manifest[0].Config);
|
|
5073
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
5074
|
+
const layers = manifest[0].Layers.map((name) => {
|
|
5075
|
+
const e = entries.get(name);
|
|
5076
|
+
if (!e)
|
|
5077
|
+
throw new Error(`the saved image is missing layer ${name}`);
|
|
5078
|
+
return e;
|
|
5079
|
+
});
|
|
5080
|
+
return { config: { bytes, architecture: parsed.architecture, os: parsed.os }, layers };
|
|
5081
|
+
}
|
|
5082
|
+
async function isGzip(tar, e) {
|
|
5083
|
+
const head = new Uint8Array(await Bun.file(tar).slice(e.offset, e.offset + 2).arrayBuffer());
|
|
5084
|
+
return head[0] === 31 && head[1] === 139;
|
|
5085
|
+
}
|
|
5086
|
+
async function sha256Stream(stream) {
|
|
5087
|
+
const hash = createHash("sha256");
|
|
5088
|
+
for await (const chunk of stream)
|
|
5089
|
+
hash.update(chunk);
|
|
5090
|
+
return `sha256:${hash.digest("hex")}`;
|
|
5091
|
+
}
|
|
5092
|
+
async function layerBlob(tar, e) {
|
|
5093
|
+
const slice = () => Bun.file(tar).slice(e.offset, e.offset + e.size);
|
|
5094
|
+
if (await isGzip(tar, e)) {
|
|
5095
|
+
const fromName = /blobs\/sha256\/([0-9a-f]{64})$/.exec(e.name)?.[1];
|
|
5096
|
+
const digest = fromName ? `sha256:${fromName}` : await sha256Stream(slice().stream());
|
|
5097
|
+
return { digest, size: e.size, mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", source: slice };
|
|
5098
|
+
}
|
|
5099
|
+
const out = join3(tmpdir(), `mybase-layer-${crypto.randomUUID()}.tar.gz`);
|
|
5100
|
+
const hash = createHash("sha256");
|
|
5101
|
+
const writer = Bun.file(out).writer();
|
|
5102
|
+
let size = 0;
|
|
5103
|
+
for await (const chunk of slice().stream().pipeThrough(new CompressionStream("gzip"))) {
|
|
5104
|
+
hash.update(chunk);
|
|
5105
|
+
writer.write(chunk);
|
|
5106
|
+
size += chunk.length;
|
|
5107
|
+
}
|
|
5108
|
+
await writer.end();
|
|
5109
|
+
return {
|
|
5110
|
+
digest: `sha256:${hash.digest("hex")}`,
|
|
5111
|
+
size,
|
|
5112
|
+
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
5113
|
+
source: () => Bun.file(out),
|
|
5114
|
+
cleanup: () => rm(out, { force: true })
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
5117
|
+
async function registry(t, path, init = {}, attempts = 3) {
|
|
5118
|
+
const url = path.startsWith("http") ? path : `${t.url}${path}`;
|
|
5119
|
+
for (let i = 1;; i++) {
|
|
5120
|
+
let res = null;
|
|
5121
|
+
try {
|
|
5122
|
+
res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${t.token}`, ...init.headers } });
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
if (i >= attempts)
|
|
5125
|
+
throw new Error(`can't reach the registry at ${t.url}: ${err.message}`);
|
|
5126
|
+
}
|
|
5127
|
+
if (res && (res.status < 500 || i >= attempts))
|
|
5128
|
+
return res;
|
|
5129
|
+
await Bun.sleep(1000 * i);
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
async function failure(res, what) {
|
|
5133
|
+
const body = await res.text().catch(() => "");
|
|
5134
|
+
let reason = body.slice(0, 300);
|
|
5135
|
+
try {
|
|
5136
|
+
const parsed = JSON.parse(body);
|
|
5137
|
+
reason = parsed.error ?? parsed.errors?.map((e) => e.message).join("; ") ?? reason;
|
|
5138
|
+
} catch {}
|
|
5139
|
+
return new Error(`${what} failed (${res.status}): ${reason}`);
|
|
5140
|
+
}
|
|
5141
|
+
async function uploadBlob(t, blob, progress) {
|
|
5142
|
+
const head = await registry(t, `/v2/${t.repo}/blobs/${blob.digest}`, { method: "HEAD" });
|
|
5143
|
+
if (head.ok)
|
|
5144
|
+
return progress({ kind: "skip", digest: blob.digest, size: blob.size });
|
|
5145
|
+
const start = await registry(t, `/v2/${t.repo}/blobs/uploads/`, { method: "POST", headers: { "Content-Length": "0" } });
|
|
5146
|
+
if (start.status !== 202)
|
|
5147
|
+
throw await failure(start, "starting an upload");
|
|
5148
|
+
let location = start.headers.get("Location");
|
|
5149
|
+
const absolute = (loc) => loc.startsWith("http") ? loc.replace(/^https?:\/\/[^/]+/, t.url) : `${t.url}${loc}`;
|
|
5150
|
+
location = absolute(location);
|
|
5151
|
+
const file = blob.source();
|
|
5152
|
+
const step = t.chunkBytes ?? CHUNK_BYTES;
|
|
5153
|
+
for (let sent = 0;sent < blob.size; sent += step) {
|
|
5154
|
+
const end = Math.min(sent + step, blob.size);
|
|
5155
|
+
const chunk = await file.slice(sent, end).arrayBuffer();
|
|
5156
|
+
const res = await registry(t, location, {
|
|
5157
|
+
method: "PATCH",
|
|
5158
|
+
headers: { "Content-Type": "application/octet-stream", "Content-Range": `${sent}-${end - 1}`, "Content-Length": String(end - sent) },
|
|
5159
|
+
body: chunk
|
|
5160
|
+
}, 1);
|
|
5161
|
+
if (res.status !== 202)
|
|
5162
|
+
throw await failure(res, "uploading a layer chunk");
|
|
5163
|
+
location = absolute(res.headers.get("Location"));
|
|
5164
|
+
progress({ kind: "upload", digest: blob.digest, sent: end, size: blob.size });
|
|
5165
|
+
}
|
|
5166
|
+
const done = await registry(t, `${location}${location.includes("?") ? "&" : "?"}digest=${encodeURIComponent(blob.digest)}`, {
|
|
5167
|
+
method: "PUT",
|
|
5168
|
+
headers: { "Content-Length": "0" }
|
|
5169
|
+
});
|
|
5170
|
+
if (done.status !== 201)
|
|
5171
|
+
throw await failure(done, "finishing an upload");
|
|
5172
|
+
progress({ kind: "done", digest: blob.digest, size: blob.size });
|
|
5173
|
+
}
|
|
5174
|
+
async function pushImage(tar, t, progress = () => {}) {
|
|
5175
|
+
const image = await savedImage(tar, await readTar(tar));
|
|
5176
|
+
const configDigest = `sha256:${createHash("sha256").update(image.config.bytes).digest("hex")}`;
|
|
5177
|
+
const configPath = join3(tmpdir(), `mybase-config-${crypto.randomUUID()}.json`);
|
|
5178
|
+
await Bun.write(configPath, image.config.bytes);
|
|
5179
|
+
const blobs = [];
|
|
5180
|
+
try {
|
|
5181
|
+
for (const layer of image.layers)
|
|
5182
|
+
blobs.push(await layerBlob(tar, layer));
|
|
5183
|
+
const config = { digest: configDigest, size: image.config.bytes.length, mediaType: "application/vnd.oci.image.config.v1+json", source: () => Bun.file(configPath) };
|
|
5184
|
+
for (const blob of [...blobs, config])
|
|
5185
|
+
await uploadBlob(t, blob, progress);
|
|
5186
|
+
const manifest = new TextEncoder().encode(JSON.stringify({
|
|
5187
|
+
schemaVersion: 2,
|
|
5188
|
+
mediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
5189
|
+
config: { mediaType: config.mediaType, digest: config.digest, size: config.size },
|
|
5190
|
+
layers: blobs.map((b) => ({ mediaType: b.mediaType, digest: b.digest, size: b.size }))
|
|
5191
|
+
}));
|
|
5192
|
+
const digest = `sha256:${createHash("sha256").update(manifest).digest("hex")}`;
|
|
5193
|
+
const res = await registry(t, `/v2/${t.repo}/manifests/${digest}`, {
|
|
5194
|
+
method: "PUT",
|
|
5195
|
+
headers: { "Content-Type": "application/vnd.oci.image.manifest.v1+json" },
|
|
5196
|
+
body: manifest
|
|
5197
|
+
});
|
|
5198
|
+
if (res.status !== 201)
|
|
5199
|
+
throw await failure(res, "pushing the manifest");
|
|
5200
|
+
return { digest, architecture: image.config.architecture };
|
|
5201
|
+
} finally {
|
|
5202
|
+
await rm(configPath, { force: true });
|
|
5203
|
+
for (const b of blobs)
|
|
5204
|
+
await b.cleanup?.();
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
// src/hosting.ts
|
|
5209
|
+
init_output();
|
|
5210
|
+
|
|
5211
|
+
// src/hosting-templates.ts
|
|
5212
|
+
var TEMPLATES = ["nextjs", "static", "node"];
|
|
5213
|
+
var install = `RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \\
|
|
5214
|
+
elif [ -f yarn.lock ]; then corepack enable yarn && yarn install --frozen-lockfile; \\
|
|
5215
|
+
elif [ -f package-lock.json ]; then npm ci; \\
|
|
5216
|
+
elif [ -f bun.lock ]; then npm install -g bun && bun install --frozen-lockfile; \\
|
|
5217
|
+
else npm install; fi`;
|
|
5218
|
+
var lockfiles = "COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* bun.lock* ./";
|
|
5219
|
+
var DOCKERFILES = {
|
|
5220
|
+
nextjs: `# Next.js, built with output: "standalone" (set it in next.config).
|
|
5221
|
+
FROM node:22-alpine AS deps
|
|
5222
|
+
WORKDIR /app
|
|
5223
|
+
${lockfiles}
|
|
5224
|
+
${install}
|
|
5225
|
+
|
|
5226
|
+
FROM node:22-alpine AS build
|
|
5227
|
+
WORKDIR /app
|
|
5228
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
5229
|
+
COPY . .
|
|
5230
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
5231
|
+
RUN mkdir -p public && npm run build
|
|
5232
|
+
|
|
5233
|
+
FROM node:22-alpine
|
|
5234
|
+
WORKDIR /app
|
|
5235
|
+
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
|
|
5236
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
5237
|
+
COPY --from=build --chown=app:app /app/public ./public
|
|
5238
|
+
COPY --from=build --chown=app:app /app/.next/standalone ./
|
|
5239
|
+
COPY --from=build --chown=app:app /app/.next/static ./.next/static
|
|
5240
|
+
USER app
|
|
5241
|
+
EXPOSE 3000
|
|
5242
|
+
CMD ["node", "server.js"]
|
|
5243
|
+
`,
|
|
5244
|
+
static: `# Static site: \`npm run build\` writes OUT_DIR, Caddy serves it on :3000.
|
|
5245
|
+
FROM node:22-alpine AS build
|
|
5246
|
+
WORKDIR /app
|
|
5247
|
+
${lockfiles}
|
|
5248
|
+
${install}
|
|
5249
|
+
COPY . .
|
|
5250
|
+
RUN npm run build
|
|
5251
|
+
|
|
5252
|
+
FROM caddy:2.10.2-alpine
|
|
5253
|
+
ARG OUT_DIR=dist
|
|
5254
|
+
COPY --from=build /app/\${OUT_DIR} /srv
|
|
5255
|
+
RUN printf '{\\n admin off\\n persist_config off\\n}\\n:3000 {\\n root * /srv\\n encode gzip\\n try_files {path} {path}/ /index.html\\n file_server\\n}\\n' > /etc/caddy/Caddyfile
|
|
5256
|
+
ENV XDG_CONFIG_HOME=/tmp XDG_DATA_HOME=/tmp
|
|
5257
|
+
USER nobody
|
|
5258
|
+
EXPOSE 3000
|
|
5259
|
+
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
|
5260
|
+
`,
|
|
5261
|
+
node: `# Bun or Node server: \`bun run start\` must listen on $PORT.
|
|
5262
|
+
FROM oven/bun:1.3-alpine
|
|
5263
|
+
WORKDIR /app
|
|
5264
|
+
COPY package.json bun.lock* package-lock.json* ./
|
|
5265
|
+
RUN bun install --production
|
|
5266
|
+
COPY . .
|
|
5267
|
+
ENV NODE_ENV=production PORT=3000
|
|
5268
|
+
USER bun
|
|
5269
|
+
EXPOSE 3000
|
|
5270
|
+
CMD ["bun", "run", "start"]
|
|
5271
|
+
`
|
|
5272
|
+
};
|
|
5273
|
+
var DOCKERIGNORE = `node_modules
|
|
5274
|
+
.git
|
|
5275
|
+
.next
|
|
5276
|
+
dist
|
|
5277
|
+
.env
|
|
5278
|
+
.env.*
|
|
5279
|
+
!.env.example
|
|
5280
|
+
npm-debug.log*
|
|
5281
|
+
Dockerfile
|
|
5282
|
+
.dockerignore
|
|
5283
|
+
`;
|
|
5284
|
+
var configFile = (project, site) => `import { defineConfig } from "@mybase-dev/sdk/config";
|
|
5285
|
+
|
|
5286
|
+
export default defineConfig({
|
|
5287
|
+
project: ${JSON.stringify(project)},
|
|
5288
|
+
hosting: {
|
|
5289
|
+
// \`mybase deploy\` builds ./Dockerfile and runs it at https://${site}.<your domain>.
|
|
5290
|
+
sites: { ${JSON.stringify(site)}: { port: 3000 } },
|
|
5291
|
+
},
|
|
5292
|
+
});
|
|
5293
|
+
`;
|
|
5294
|
+
|
|
5295
|
+
// src/hosting.ts
|
|
5296
|
+
await init_service_enable();
|
|
5297
|
+
var hostingUsage = ` hosting enable <project> Turn on hosting (sites at <site>.<domain>; needs \`mybase server hosting\` once)
|
|
5298
|
+
hosting sites <project> List sites, their URLs, and the latest deploy
|
|
5299
|
+
hosting create <project> <site> [--port 3000] [--health /] Add a site (names are unique across projects)
|
|
5300
|
+
hosting set <project> <site> [--port n] [--health /path] [--secrets-env <env> | --no-secrets-env]
|
|
5301
|
+
hosting env list|set|unset <project> <site> [KEY=VALUE\u2026 | KEY\u2026] The site's own env vars (applied on the next deploy)
|
|
5302
|
+
hosting deploys <project> <site> Deploy history
|
|
5303
|
+
hosting redeploy <project> <site> Restart the live image (picks up env changes)
|
|
5304
|
+
hosting rollback <project> <site> [<deploy id>] Go back to the previous (or a given) deploy
|
|
5305
|
+
hosting logs <project> <site> [--deploy <id>] [--tail 200] [--no-follow] Container output
|
|
5306
|
+
hosting delete <project> <site> [--yes] Remove a site, its containers, and its images
|
|
5307
|
+
hosting init [--template nextjs|static|node] [--project p] [--site s] Write a Dockerfile, .dockerignore, and mybase.config.ts
|
|
5308
|
+
hosting disable <project> [--yes] Turn hosting off: every site goes down
|
|
5309
|
+
deploy [--project p] [--site s] [--config mybase.config.ts] [--no-wait] Build here, push, and go live`;
|
|
5310
|
+
var base4 = (p) => `/projects/${encodeURIComponent(p)}/hosting`;
|
|
5311
|
+
var sitePath = (p, s) => `${base4(p)}/sites/${encodeURIComponent(s)}`;
|
|
5312
|
+
var short = (id) => id.slice(0, 8);
|
|
5313
|
+
var ago = (iso) => {
|
|
5314
|
+
const s = Math.round((Date.now() - +new Date(iso)) / 1000);
|
|
5315
|
+
return s < 60 ? `${s}s ago` : s < 3600 ? `${Math.round(s / 60)}m ago` : s < 86400 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`;
|
|
5316
|
+
};
|
|
5317
|
+
function printDeploy(d) {
|
|
5318
|
+
console.log([short(d.id), d.status.padEnd(8), d.digest.slice(7, 19), ago(d.createdAt).padEnd(8), d.createdBy, d.rollbackOf ? `rollback of ${short(d.rollbackOf)}` : "", d.error ?? ""].join(" ").trimEnd());
|
|
5319
|
+
}
|
|
5320
|
+
function printSite(s) {
|
|
5321
|
+
const status = s.latest ? `${s.latest.status}${s.latest.id !== s.liveDeploy && s.liveDeploy ? ` (live: ${short(s.liveDeploy)})` : ""}` : "never deployed";
|
|
5322
|
+
console.log(`${s.name.padEnd(16)} ${(s.url ?? `http://${s.name}.localhost`).padEnd(36)} :${String(s.port).padEnd(5)} ${status}`);
|
|
5323
|
+
}
|
|
5324
|
+
async function hosting(sub, args) {
|
|
5325
|
+
const { positional, flags } = parseArgs(args);
|
|
5326
|
+
const [project, site] = positional;
|
|
5327
|
+
switch (sub) {
|
|
5328
|
+
case "enable": {
|
|
5329
|
+
const p = need(project, "project name");
|
|
5330
|
+
await enableService(p, "hosting", flags);
|
|
5331
|
+
const o = await api(base4(p));
|
|
5332
|
+
print({ enabled: "hosting", project: p, ...o }, () => {
|
|
5333
|
+
console.log(`Hosting enabled for "${p}".`);
|
|
5334
|
+
if (!o.domain)
|
|
5335
|
+
console.log("No MYBASE_HOSTING_DOMAIN on the server: sites answer at http://<site>.localhost only (run `mybase server hosting`).");
|
|
5336
|
+
console.log(`
|
|
5337
|
+
Next, in your app's directory:
|
|
5338
|
+
mybase hosting init --project ${p} --site <name>
|
|
5339
|
+
mybase deploy`);
|
|
5340
|
+
});
|
|
5341
|
+
return true;
|
|
5342
|
+
}
|
|
5343
|
+
case "sites": {
|
|
5344
|
+
const p = need(project, "project name");
|
|
5345
|
+
const o = await api(base4(p));
|
|
5346
|
+
print(o, () => {
|
|
5347
|
+
if (!o.sites.length)
|
|
5348
|
+
return console.log(`No sites yet. Create one: mybase hosting create ${p} <site>`);
|
|
5349
|
+
for (const s of o.sites)
|
|
5350
|
+
printSite(s);
|
|
5351
|
+
console.log(`
|
|
5352
|
+
Images: ${o.registry}/<site> (${o.platform})`);
|
|
5353
|
+
});
|
|
5354
|
+
return true;
|
|
5355
|
+
}
|
|
5356
|
+
case "create": {
|
|
5357
|
+
const p = need(project, "project name");
|
|
5358
|
+
const s = await api(`${base4(p)}/sites`, {
|
|
5359
|
+
method: "POST",
|
|
5360
|
+
body: JSON.stringify({ name: need(site, "site name"), port: flagInt(flags, "port"), healthPath: flagString(flags, "health") })
|
|
5361
|
+
});
|
|
5362
|
+
print(s, () => console.log(`Site "${s.name}" created. It goes live at ${s.url ?? `http://${s.name}.localhost`} after the first deploy (mybase deploy).`));
|
|
5363
|
+
return true;
|
|
5364
|
+
}
|
|
5365
|
+
case "set": {
|
|
5366
|
+
const p = need(project, "project name");
|
|
5367
|
+
const body = { port: flagInt(flags, "port"), healthPath: flagString(flags, "health") };
|
|
5368
|
+
if (flags.has("no-secrets-env"))
|
|
5369
|
+
body.secretsEnv = null;
|
|
5370
|
+
else if (flags.has("secrets-env"))
|
|
5371
|
+
body.secretsEnv = flagString(flags, "secrets-env");
|
|
5372
|
+
const s = await api(sitePath(p, need(site, "site name")), { method: "PATCH", body: JSON.stringify(body) });
|
|
5373
|
+
print(s, () => {
|
|
5374
|
+
printSite(s);
|
|
5375
|
+
console.log(`secrets env: ${s.secretsEnv ?? "none"}. Env changes apply on the next deploy (mybase hosting redeploy ${p} ${s.name}).`);
|
|
5376
|
+
});
|
|
5377
|
+
return true;
|
|
5378
|
+
}
|
|
5379
|
+
case "env":
|
|
5380
|
+
return env(positional, flags);
|
|
5381
|
+
case "deploys": {
|
|
5382
|
+
const p = need(project, "project name");
|
|
5383
|
+
const list = await api(`${sitePath(p, need(site, "site name"))}/deploys`);
|
|
5384
|
+
print(list, () => list.length ? list.forEach(printDeploy) : console.log("No deploys yet."));
|
|
5385
|
+
return true;
|
|
5386
|
+
}
|
|
5387
|
+
case "redeploy":
|
|
5388
|
+
case "rollback": {
|
|
5389
|
+
const p = need(project, "project name");
|
|
5390
|
+
const s = need(site, "site name");
|
|
5391
|
+
const deploy = sub === "redeploy" ? await api(`${sitePath(p, s)}/deploys`, { method: "POST", body: "{}" }) : await api(`${sitePath(p, s)}/rollback`, { method: "POST", body: JSON.stringify(positional[2] ? { deployId: await fullDeployId(p, s, positional[2]) } : {}) });
|
|
5392
|
+
await finish(p, s, deploy, flags.has("no-wait"));
|
|
5393
|
+
return true;
|
|
5394
|
+
}
|
|
5395
|
+
case "logs": {
|
|
5396
|
+
const p = need(project, "project name");
|
|
5397
|
+
const s = need(site, "site name");
|
|
5398
|
+
const q = new URLSearchParams({ tail: String(flagInt(flags, "tail") ?? 200), follow: flags.has("no-follow") || jsonMode ? "0" : "1" });
|
|
5399
|
+
const deploy = flagString(flags, "deploy");
|
|
5400
|
+
if (deploy)
|
|
5401
|
+
q.set("deploy", await fullDeployId(p, s, deploy));
|
|
5402
|
+
const res = await api(`${sitePath(p, s)}/logs?${q}`, { raw: true });
|
|
5403
|
+
const text = jsonMode ? await res.text() : null;
|
|
5404
|
+
if (text !== null)
|
|
5405
|
+
print({ site: s, logs: text.split(`
|
|
5406
|
+
`).filter(Boolean) }, () => {});
|
|
5407
|
+
else
|
|
5408
|
+
for await (const chunk of res.body)
|
|
5409
|
+
process.stdout.write(chunk);
|
|
5410
|
+
return true;
|
|
5411
|
+
}
|
|
5412
|
+
case "delete": {
|
|
5413
|
+
const p = need(project, "project name");
|
|
5414
|
+
const s = need(site, "site name");
|
|
5415
|
+
confirm(flags.has("yes"), `Site "${s}" goes offline, and its deploys and images are deleted.`, s, "the site name");
|
|
5416
|
+
await api(sitePath(p, s), { method: "DELETE" });
|
|
5417
|
+
print({ deleted: s, project: p }, () => console.log(`Site "${s}" deleted.`));
|
|
5418
|
+
return true;
|
|
5419
|
+
}
|
|
5420
|
+
case "disable": {
|
|
5421
|
+
const p = need(project, "project name");
|
|
5422
|
+
confirm(flags.has("yes"), `Every site in "${p}" goes offline, and their deploys and images are deleted.`, p, "the project name");
|
|
5423
|
+
await api(`/projects/${encodeURIComponent(p)}/services/hosting`, { method: "DELETE" });
|
|
5424
|
+
print({ disabled: "hosting", project: p }, () => console.log(`Hosting disabled for "${p}".`));
|
|
5425
|
+
return true;
|
|
5426
|
+
}
|
|
5427
|
+
case "init":
|
|
5428
|
+
await init(flags);
|
|
5429
|
+
return true;
|
|
5430
|
+
case "deploy":
|
|
5431
|
+
await deploy(args);
|
|
5432
|
+
return true;
|
|
5433
|
+
}
|
|
5434
|
+
return false;
|
|
5435
|
+
}
|
|
5436
|
+
async function fullDeployId(p, s, id) {
|
|
5437
|
+
if (/^[0-9a-f-]{36}$/.test(id))
|
|
5438
|
+
return id;
|
|
5439
|
+
const match = (await api(`${sitePath(p, s)}/deploys`)).filter((d) => d.id.startsWith(id));
|
|
5440
|
+
if (match.length !== 1)
|
|
5441
|
+
throw new CliError(match.length ? `"${id}" matches more than one deploy` : `no deploy starting with "${id}"`, 4);
|
|
5442
|
+
return match[0].id;
|
|
5443
|
+
}
|
|
5444
|
+
async function env(positional, flags) {
|
|
5445
|
+
const [action, project, site, ...pairs] = positional;
|
|
5446
|
+
if (!action || !["list", "set", "unset"].includes(action))
|
|
5447
|
+
throw usageError("usage: mybase hosting env list|set|unset <project> <site> [KEY=VALUE\u2026 | KEY\u2026]");
|
|
5448
|
+
const p = need(project, "project name");
|
|
5449
|
+
const s = need(site, "site name");
|
|
5450
|
+
const path = `${sitePath(p, s)}/env`;
|
|
5451
|
+
const current = (await api(path)).env;
|
|
5452
|
+
if (action === "list") {
|
|
5453
|
+
const values = flags.has("values");
|
|
5454
|
+
print(values ? current : Object.keys(current), () => {
|
|
5455
|
+
if (!Object.keys(current).length)
|
|
5456
|
+
return console.log("No env vars. Set some: mybase hosting env set <project> <site> KEY=VALUE");
|
|
5457
|
+
for (const [k, v] of Object.entries(current))
|
|
5458
|
+
console.log(values ? `${k}=${v}` : k);
|
|
5459
|
+
});
|
|
5460
|
+
return true;
|
|
5461
|
+
}
|
|
5462
|
+
const next = { ...current };
|
|
5463
|
+
if (action === "set") {
|
|
5464
|
+
if (!pairs.length)
|
|
5465
|
+
throw usageError("give at least one KEY=VALUE");
|
|
5466
|
+
for (const pair of pairs) {
|
|
5467
|
+
const at = pair.indexOf("=");
|
|
5468
|
+
if (at < 1)
|
|
5469
|
+
throw usageError(`"${pair}" isn't KEY=VALUE`);
|
|
5470
|
+
next[pair.slice(0, at)] = pair.slice(at + 1);
|
|
5471
|
+
}
|
|
5472
|
+
} else {
|
|
5473
|
+
if (!pairs.length)
|
|
5474
|
+
throw usageError("give at least one KEY");
|
|
5475
|
+
for (const k of pairs)
|
|
5476
|
+
delete next[k];
|
|
5477
|
+
}
|
|
5478
|
+
await api(path, { method: "PUT", body: JSON.stringify({ env: next }) });
|
|
5479
|
+
print({ site: s, keys: Object.keys(next) }, () => console.log(`Saved ${Object.keys(next).length} env vars. They apply on the next deploy: mybase hosting redeploy ${p} ${s}`));
|
|
5480
|
+
return true;
|
|
5481
|
+
}
|
|
5482
|
+
async function init(flags) {
|
|
5483
|
+
const template = flagString(flags, "template") ?? "nextjs";
|
|
5484
|
+
if (!TEMPLATES.includes(template))
|
|
5485
|
+
throw usageError(`--template must be one of ${TEMPLATES.join(", ")}`);
|
|
5486
|
+
const dir = resolve4(flagString(flags, "dir") ?? ".");
|
|
5487
|
+
const written = [];
|
|
5488
|
+
const write = async (name, content) => {
|
|
5489
|
+
const path = join4(dir, name);
|
|
5490
|
+
if (existsSync(path) && !flags.has("force"))
|
|
5491
|
+
return console.error(`${name} exists; left as is (--force replaces it)`);
|
|
5492
|
+
await Bun.write(path, content);
|
|
5493
|
+
written.push(name);
|
|
5494
|
+
};
|
|
5495
|
+
await write("Dockerfile", DOCKERFILES[template]);
|
|
5496
|
+
await write(".dockerignore", DOCKERIGNORE);
|
|
5497
|
+
const project = flagString(flags, "project");
|
|
5498
|
+
const site = flagString(flags, "site");
|
|
5499
|
+
if (project && site)
|
|
5500
|
+
await write("mybase.config.ts", configFile(project, site));
|
|
5501
|
+
print({ template, written }, () => {
|
|
5502
|
+
if (written.length)
|
|
5503
|
+
console.log(`Wrote ${written.join(", ")} (${template}).`);
|
|
5504
|
+
if (template === "nextjs")
|
|
5505
|
+
console.log('Set output: "standalone" in next.config so the image stays small.');
|
|
5506
|
+
if (!(project && site))
|
|
5507
|
+
console.log("Add a hosting block to mybase.config.ts (or pass --project and --site here), then: mybase deploy");
|
|
5508
|
+
else
|
|
5509
|
+
console.log("Next: mybase deploy");
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
async function docker(args, quiet = false) {
|
|
5513
|
+
const child = Bun.spawn(["docker", ...args], { stdin: "ignore", stdout: quiet || jsonMode ? "ignore" : 2, stderr: "inherit" });
|
|
5514
|
+
if (await child.exited !== 0)
|
|
5515
|
+
throw new CliError(`docker ${args[0]} failed`);
|
|
5516
|
+
}
|
|
5517
|
+
var goArch = (arch) => arch === "x64" ? "amd64" : arch;
|
|
5518
|
+
async function deploy(args) {
|
|
5519
|
+
const { flags } = parseArgs(args);
|
|
5520
|
+
const configPath = flagString(flags, "config") ?? "mybase.config.ts";
|
|
5521
|
+
const config = existsSync(configPath) ? await readConfigFile(configPath) : null;
|
|
5522
|
+
const project = flagString(flags, "project") ?? config?.project;
|
|
5523
|
+
if (!project)
|
|
5524
|
+
throw usageError("which project? Pass --project, or run in a directory with a mybase.config.ts");
|
|
5525
|
+
const sites = config?.project === project ? config.hosting?.sites ?? {} : {};
|
|
5526
|
+
const names = Object.keys(sites);
|
|
5527
|
+
const site = flagString(flags, "site") ?? (names.length === 1 ? names[0] : undefined);
|
|
5528
|
+
if (!site)
|
|
5529
|
+
throw usageError(names.length ? `which site? --site ${names.join(" | --site ")}` : "which site? Pass --site, or add hosting.sites to mybase.config.ts");
|
|
5530
|
+
const settings = sites[site] ?? {};
|
|
5531
|
+
const root = config ? dirname3(resolve4(configPath)) : process.cwd();
|
|
5532
|
+
const context = resolve4(root, flagString(flags, "dir") ?? settings.context ?? ".");
|
|
5533
|
+
const dockerfile = resolve4(context, flagString(flags, "dockerfile") ?? settings.dockerfile ?? "Dockerfile");
|
|
5534
|
+
if (!existsSync(dockerfile))
|
|
5535
|
+
throw new CliError(`no Dockerfile at ${dockerfile}. Write one: mybase hosting init --template nextjs|static|node`);
|
|
5536
|
+
const token = apiToken();
|
|
5537
|
+
if (!token)
|
|
5538
|
+
throw new CliError("not signed in. Run `mybase login` (or set MYBASE_TOKEN).", 3);
|
|
5539
|
+
let existing = null;
|
|
5540
|
+
try {
|
|
5541
|
+
existing = await api(sitePath(project, site));
|
|
5542
|
+
} catch (err) {
|
|
5543
|
+
if (!(err instanceof CliError && err.status === 404 && /no site named/.test(err.message)))
|
|
5544
|
+
throw err;
|
|
5545
|
+
}
|
|
5546
|
+
if (!existing) {
|
|
5547
|
+
existing = await api(`${base4(project)}/sites`, { method: "POST", body: JSON.stringify({ name: site, port: settings.port, healthPath: settings.healthPath }) });
|
|
5548
|
+
log(`Created site "${site}".`);
|
|
5549
|
+
} else if (settings.port !== undefined && settings.port !== existing.port || settings.healthPath !== undefined && settings.healthPath !== existing.healthPath) {
|
|
5550
|
+
existing = await api(sitePath(project, site), { method: "PATCH", body: JSON.stringify({ port: settings.port, healthPath: settings.healthPath }) });
|
|
5551
|
+
}
|
|
5552
|
+
const health = await api("/health");
|
|
5553
|
+
const platform = `linux/${goArch(health.arch ?? process.arch)}`;
|
|
5554
|
+
const tag = `mybase-deploy/${project}-${site}:latest`;
|
|
5555
|
+
log(`Building ${site} for ${platform}\u2026`);
|
|
5556
|
+
await docker(["build", "--platform", platform, "-t", tag, "-f", dockerfile, context]);
|
|
5557
|
+
const tar = join4(tmpdir2(), `mybase-${project}-${site}-${crypto.randomUUID()}.tar`);
|
|
5558
|
+
try {
|
|
5559
|
+
await docker(["save", "-o", tar, tag], true);
|
|
5560
|
+
log(`Pushing to ${new URL(API_URL).host}/${project}/${site} (${CHUNK_BYTES / 1024 / 1024} MB chunks)\u2026`);
|
|
5561
|
+
let lastPct = -1;
|
|
5562
|
+
const { digest, architecture } = await pushImage(tar, { url: API_URL, token, repo: `${project}/${site}` }, (e) => {
|
|
5563
|
+
if (jsonMode)
|
|
5564
|
+
return;
|
|
5565
|
+
if (e.kind === "skip")
|
|
5566
|
+
process.stderr.write(` ${e.digest.slice(7, 19)} already there
|
|
5567
|
+
`);
|
|
5568
|
+
if (e.kind === "upload") {
|
|
5569
|
+
const pct = Math.floor((e.sent ?? 0) / e.size * 100);
|
|
5570
|
+
if (pct !== lastPct)
|
|
5571
|
+
process.stderr.write(`\r ${e.digest.slice(7, 19)} ${pct}% of ${(e.size / 1024 / 1024).toFixed(1)} MB`);
|
|
5572
|
+
lastPct = pct;
|
|
5573
|
+
}
|
|
5574
|
+
if (e.kind === "done")
|
|
5575
|
+
process.stderr.write(`\r ${e.digest.slice(7, 19)} pushed ${(e.size / 1024 / 1024).toFixed(1)} MB
|
|
5576
|
+
`);
|
|
5577
|
+
});
|
|
5578
|
+
if (architecture && `linux/${architecture}` !== platform)
|
|
5579
|
+
throw new CliError(`the image is ${architecture}, but the server runs ${platform}`);
|
|
5580
|
+
const body = { digest };
|
|
5581
|
+
const created = await api(`${sitePath(project, site)}/deploys`, { method: "POST", body: JSON.stringify(body) });
|
|
5582
|
+
await finish(project, site, created, flags.has("no-wait"));
|
|
5583
|
+
} finally {
|
|
5584
|
+
await rm2(tar, { force: true });
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
var log = (line) => {
|
|
5588
|
+
if (!jsonMode)
|
|
5589
|
+
console.error(line);
|
|
5590
|
+
};
|
|
5591
|
+
async function finish(project, site, deploy, noWait) {
|
|
5592
|
+
let d = deploy;
|
|
5593
|
+
if (!noWait) {
|
|
5594
|
+
log(`Deploy ${short(d.id)} queued; waiting for the container to answer\u2026`);
|
|
5595
|
+
const deadline = Date.now() + 20 * 60000;
|
|
5596
|
+
while ((d.status === "queued" || d.status === "starting") && Date.now() < deadline) {
|
|
5597
|
+
await Bun.sleep(2000);
|
|
5598
|
+
d = (await api(`${sitePath(project, site)}/deploys`)).find((x) => x.id === d.id) ?? d;
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
const s = await api(sitePath(project, site));
|
|
5602
|
+
print({ deploy: d, url: s.url }, () => {
|
|
5603
|
+
if (d.status === "live")
|
|
5604
|
+
console.log(`Live: ${s.url ?? `http://${site}.localhost`} (deploy ${short(d.id)})`);
|
|
5605
|
+
else if (d.status === "failed")
|
|
5606
|
+
console.log(`Deploy ${short(d.id)} failed: ${d.error}
|
|
5607
|
+
The previous deploy (if any) is still live.`);
|
|
5608
|
+
else
|
|
5609
|
+
console.log(`Deploy ${short(d.id)} is ${d.status}. Check: mybase hosting deploys ${project} ${site}`);
|
|
5610
|
+
});
|
|
5611
|
+
if (d.status === "failed")
|
|
5612
|
+
process.exitCode = 1;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
4884
5615
|
// src/queue.ts
|
|
4885
5616
|
init_src();
|
|
4886
5617
|
init_output();
|
|
@@ -4901,8 +5632,8 @@ var queueUsage = ` queue enable <project> Turn on queues
|
|
|
4901
5632
|
queue redrive <project> <dlq> [--to <queue>] Move dead letters back to their queues
|
|
4902
5633
|
queue stats <project> [--range 24h|7d] Sent, received, deleted, dead-lettered per queue
|
|
4903
5634
|
queue sync [--config <file>] [--dry-run] [--yes] Make code-defined queues match mybase.config.ts`;
|
|
4904
|
-
var
|
|
4905
|
-
var queuePath = (project, name, rest = "") => `${
|
|
5635
|
+
var base5 = (project) => `/projects/${encodeURIComponent(project)}/queue`;
|
|
5636
|
+
var queuePath = (project, name, rest = "") => `${base5(project)}/queues/${encodeURIComponent(name)}${rest}`;
|
|
4906
5637
|
function formatMs(ms) {
|
|
4907
5638
|
for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
|
|
4908
5639
|
if (ms >= size && ms % size === 0)
|
|
@@ -4926,7 +5657,7 @@ async function queue(sub, args) {
|
|
|
4926
5657
|
case "enable": {
|
|
4927
5658
|
const p = need(project, "project name");
|
|
4928
5659
|
const { linked } = await enableService(p, "queue", flags);
|
|
4929
|
-
const settings = await api(
|
|
5660
|
+
const settings = await api(base5(p));
|
|
4930
5661
|
print({ enabled: "queue", project: p, linked, ...settings }, () => {
|
|
4931
5662
|
console.log(`Queues enabled for "${p}"
|
|
4932
5663
|
`);
|
|
@@ -4944,7 +5675,7 @@ async function queue(sub, args) {
|
|
|
4944
5675
|
}
|
|
4945
5676
|
case "list": {
|
|
4946
5677
|
const p = need(project, "project name");
|
|
4947
|
-
const queues = await api(`${
|
|
5678
|
+
const queues = await api(`${base5(p)}/queues`);
|
|
4948
5679
|
print(queues, () => {
|
|
4949
5680
|
if (queues.length === 0)
|
|
4950
5681
|
console.log("No queues yet.");
|
|
@@ -4970,7 +5701,7 @@ async function queue(sub, args) {
|
|
|
4970
5701
|
contentDedup: flags.has("content-dedup") || undefined,
|
|
4971
5702
|
deadLetter: dlq ? { queue: dlq, maxReceives: flagInt(flags, "max-receives") ?? 5 } : undefined
|
|
4972
5703
|
};
|
|
4973
|
-
const q = await api(`${
|
|
5704
|
+
const q = await api(`${base5(p)}/queues`, { method: "POST", body: JSON.stringify(body) });
|
|
4974
5705
|
print(q, () => console.log(`Created ${q.fifo ? "FIFO" : "standard"} queue "${q.name}" (visibility ${formatMs(q.visibilityTimeoutMs)}, retention ${formatMs(q.retentionMs)})`));
|
|
4975
5706
|
return true;
|
|
4976
5707
|
}
|
|
@@ -5055,7 +5786,7 @@ async function queue(sub, args) {
|
|
|
5055
5786
|
const range = flagString(flags, "range") ?? "24h";
|
|
5056
5787
|
if (!QUEUE_STATS_RANGES.includes(range))
|
|
5057
5788
|
throw new Error(`--range must be one of ${QUEUE_STATS_RANGES.join(", ")}`);
|
|
5058
|
-
const stats = await api(`${
|
|
5789
|
+
const stats = await api(`${base5(p)}/stats?range=${range}`);
|
|
5059
5790
|
print(stats, () => {
|
|
5060
5791
|
const { queues } = stats;
|
|
5061
5792
|
console.log(`Last ${range}:`);
|
|
@@ -5084,7 +5815,7 @@ function parseDuration(value) {
|
|
|
5084
5815
|
async function sync2(file, dryRun, yes) {
|
|
5085
5816
|
const config = await readConfigFile(file);
|
|
5086
5817
|
const queues = Object.entries(config.queue?.queues ?? {}).map(([name, q]) => ({ ...q, name }));
|
|
5087
|
-
const path = `${
|
|
5818
|
+
const path = `${base5(config.project)}/queues`;
|
|
5088
5819
|
const plan = await api(path, { method: "PUT", body: JSON.stringify({ queues, dryRun: true }) });
|
|
5089
5820
|
if (dryRun)
|
|
5090
5821
|
return printSync(config.project, true, plan);
|
|
@@ -5110,8 +5841,8 @@ var vectorUsage = ` vector enable <project> Turn on vector + full-text s
|
|
|
5110
5841
|
vector documents <project> <index> [--limit 20] [--after <id>] List documents in id order
|
|
5111
5842
|
vector query <project> <index> [<text>] [--mode text|vector|hybrid] [--top 10] [--filter <json>]
|
|
5112
5843
|
vector sync [--config <file>] [--dry-run] [--yes] Make code-defined indexes match mybase.config.ts`;
|
|
5113
|
-
var
|
|
5114
|
-
var indexPath = (project, name, rest = "") => `${
|
|
5844
|
+
var base6 = (project) => `/projects/${encodeURIComponent(project)}/vector`;
|
|
5845
|
+
var indexPath = (project, name, rest = "") => `${base6(project)}/indexes/${encodeURIComponent(name)}${rest}`;
|
|
5115
5846
|
function describe(i) {
|
|
5116
5847
|
const vectors = i.dimensions ? `${i.dimensions}d ${i.metric}` : "text only";
|
|
5117
5848
|
return [i.name, vectors, i.language, i.embed ? "embeds" : "", `${i.documents} docs`, i.source].filter(Boolean).join("\t");
|
|
@@ -5130,7 +5861,7 @@ async function vector(sub, args) {
|
|
|
5130
5861
|
case "enable": {
|
|
5131
5862
|
const p = need(project, "project name");
|
|
5132
5863
|
const { linked } = await enableService(p, "vector", flags);
|
|
5133
|
-
const settings = await api(
|
|
5864
|
+
const settings = await api(base6(p));
|
|
5134
5865
|
print({ enabled: "vector", project: p, linked, ...settings }, () => {
|
|
5135
5866
|
console.log(`Vector search enabled for "${p}"
|
|
5136
5867
|
`);
|
|
@@ -5148,7 +5879,7 @@ async function vector(sub, args) {
|
|
|
5148
5879
|
}
|
|
5149
5880
|
case "settings": {
|
|
5150
5881
|
const p = need(project, "project name");
|
|
5151
|
-
const settings = await api(
|
|
5882
|
+
const settings = await api(base6(p));
|
|
5152
5883
|
print(settings, () => {
|
|
5153
5884
|
const e = settings.embedding;
|
|
5154
5885
|
console.log(e ? `embedding ${e.model} (${e.dimensions}d) at ${e.url}${e.hasKey ? ", API key stored" : ""}` : "embedding none (apps send their own vectors)");
|
|
@@ -5167,13 +5898,13 @@ async function vector(sub, args) {
|
|
|
5167
5898
|
const apiKey = flags.has("key") ? await readSecret("API key: ") : undefined;
|
|
5168
5899
|
body = { embedding: { url: need(flagString(flags, "url"), "--url"), model: need(flagString(flags, "model"), "--model"), dimensions: dims, apiKey } };
|
|
5169
5900
|
}
|
|
5170
|
-
const settings = await api(
|
|
5901
|
+
const settings = await api(base6(p), { method: "PUT", body: JSON.stringify(body) });
|
|
5171
5902
|
print(settings, () => console.log(settings.embedding ? `Embedding with ${settings.embedding.model} (${settings.embedding.dimensions}d)` : "Embedding provider removed"));
|
|
5172
5903
|
return true;
|
|
5173
5904
|
}
|
|
5174
5905
|
case "list": {
|
|
5175
5906
|
const p = need(project, "project name");
|
|
5176
|
-
const indexes = await api(`${
|
|
5907
|
+
const indexes = await api(`${base6(p)}/indexes`);
|
|
5177
5908
|
print(indexes, () => {
|
|
5178
5909
|
if (indexes.length === 0)
|
|
5179
5910
|
console.log("No indexes yet.");
|
|
@@ -5191,7 +5922,7 @@ async function vector(sub, args) {
|
|
|
5191
5922
|
language: flagString(flags, "lang"),
|
|
5192
5923
|
embed: flags.has("embed") || undefined
|
|
5193
5924
|
};
|
|
5194
|
-
const index = await api(`${
|
|
5925
|
+
const index = await api(`${base6(p)}/indexes`, { method: "POST", body: JSON.stringify(body) });
|
|
5195
5926
|
print(index, () => console.log(`Created index "${index.name}" (${index.dimensions ? `${index.dimensions}d ${index.metric}` : "text only"}, ${index.language})`));
|
|
5196
5927
|
return true;
|
|
5197
5928
|
}
|
|
@@ -5274,7 +6005,7 @@ async function vector(sub, args) {
|
|
|
5274
6005
|
async function sync3(file, dryRun, yes) {
|
|
5275
6006
|
const config = await readConfigFile(file);
|
|
5276
6007
|
const indexes = Object.entries(config.vector?.indexes ?? {}).map(([name, i]) => ({ ...i, name }));
|
|
5277
|
-
const path = `${
|
|
6008
|
+
const path = `${base6(config.project)}/indexes`;
|
|
5278
6009
|
const plan = await api(path, { method: "PUT", body: JSON.stringify({ indexes, dryRun: true }) });
|
|
5279
6010
|
if (dryRun)
|
|
5280
6011
|
return printSync(config.project, true, plan);
|
|
@@ -5299,7 +6030,7 @@ var ratelimitUsage = ` ratelimit enable <project> Turn on rate limits
|
|
|
5299
6030
|
ratelimit delete <project> <name> Delete a rule
|
|
5300
6031
|
ratelimit stats <project> [--range 24h|7d] Allowed and blocked requests per rule
|
|
5301
6032
|
ratelimit sync [--config <file>] [--dry-run] Make code-defined rules match mybase.config.ts`;
|
|
5302
|
-
var
|
|
6033
|
+
var base7 = (project) => `/projects/${encodeURIComponent(project)}/ratelimit`;
|
|
5303
6034
|
function formatWindow(ms) {
|
|
5304
6035
|
for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
|
|
5305
6036
|
if (ms % size === 0)
|
|
@@ -5323,7 +6054,7 @@ async function ratelimit(sub, args) {
|
|
|
5323
6054
|
case "enable": {
|
|
5324
6055
|
const p = need(project, "project name");
|
|
5325
6056
|
const { linked } = await enableService(p, "ratelimit", flags);
|
|
5326
|
-
const settings = await api(
|
|
6057
|
+
const settings = await api(base7(p));
|
|
5327
6058
|
print({ enabled: "ratelimit", project: p, linked, ...settings }, () => {
|
|
5328
6059
|
console.log(`Rate limits enabled for "${p}"
|
|
5329
6060
|
`);
|
|
@@ -5341,7 +6072,7 @@ async function ratelimit(sub, args) {
|
|
|
5341
6072
|
}
|
|
5342
6073
|
case "rules": {
|
|
5343
6074
|
const p = need(project, "project name");
|
|
5344
|
-
const rules = await api(`${
|
|
6075
|
+
const rules = await api(`${base7(p)}/rules`);
|
|
5345
6076
|
print(rules, () => {
|
|
5346
6077
|
if (rules.length === 0)
|
|
5347
6078
|
console.log("No rules yet. Inline limits still work.");
|
|
@@ -5361,14 +6092,14 @@ async function ratelimit(sub, args) {
|
|
|
5361
6092
|
algorithm: flagString(flags, "algorithm"),
|
|
5362
6093
|
burst: flagInt(flags, "burst")
|
|
5363
6094
|
};
|
|
5364
|
-
const r = await api(`${
|
|
6095
|
+
const r = await api(`${base7(p)}/rules`, { method: "POST", body: JSON.stringify(body) });
|
|
5365
6096
|
print(r, () => console.log(`Created rule "${r.name}": ${r.limit} per ${formatWindow(r.windowMs)} (${r.algorithm})`));
|
|
5366
6097
|
return true;
|
|
5367
6098
|
}
|
|
5368
6099
|
case "delete": {
|
|
5369
6100
|
const p = need(project, "project name");
|
|
5370
6101
|
const n = need(name, "rule name");
|
|
5371
|
-
await api(`${
|
|
6102
|
+
await api(`${base7(p)}/rules/${encodeURIComponent(n)}`, { method: "DELETE" });
|
|
5372
6103
|
print({ deleted: n }, () => console.log(`Deleted rule "${n}"`));
|
|
5373
6104
|
return true;
|
|
5374
6105
|
}
|
|
@@ -5377,7 +6108,7 @@ async function ratelimit(sub, args) {
|
|
|
5377
6108
|
const range = flagString(flags, "range") ?? "24h";
|
|
5378
6109
|
if (!RATELIMIT_STATS_RANGES.includes(range))
|
|
5379
6110
|
throw new Error(`--range must be one of ${RATELIMIT_STATS_RANGES.join(", ")}`);
|
|
5380
|
-
const stats = await api(`${
|
|
6111
|
+
const stats = await api(`${base7(p)}/stats?range=${range}`);
|
|
5381
6112
|
print(stats, () => {
|
|
5382
6113
|
const { rules } = stats;
|
|
5383
6114
|
console.log(`Last ${range}:`);
|
|
@@ -5408,7 +6139,7 @@ async function sync4(file, dryRun) {
|
|
|
5408
6139
|
burst: rule.burst
|
|
5409
6140
|
}))
|
|
5410
6141
|
};
|
|
5411
|
-
printSync(config.project, dryRun, await api(`${
|
|
6142
|
+
printSync(config.project, dryRun, await api(`${base7(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
|
|
5412
6143
|
}
|
|
5413
6144
|
|
|
5414
6145
|
// src/realtime.ts
|
|
@@ -5425,7 +6156,7 @@ var realtimeUsage = ` realtime enable <project> Turn on realtime and print
|
|
|
5425
6156
|
realtime channels <project> [--prefix p] Channels with subscribers right now
|
|
5426
6157
|
realtime publish <project> <channel> <event> [json] Send a message to everyone subscribed
|
|
5427
6158
|
realtime disable <project> [--yes] Turn it off (closes every connection)`;
|
|
5428
|
-
var
|
|
6159
|
+
var base8 = (project) => `/projects/${encodeURIComponent(project)}/realtime`;
|
|
5429
6160
|
function printSettings3(s) {
|
|
5430
6161
|
console.log(` socket ${s.socketUrl} (connectRealtime in @mybase-dev/sdk/realtime)`);
|
|
5431
6162
|
console.log(` publish POST ${s.publishUrl} (realtime:write key)`);
|
|
@@ -5450,7 +6181,7 @@ async function realtime(sub, args) {
|
|
|
5450
6181
|
case "enable": {
|
|
5451
6182
|
const p = need(project, "project name");
|
|
5452
6183
|
const { linked } = await enableService(p, "realtime", flags);
|
|
5453
|
-
const s = await api(
|
|
6184
|
+
const s = await api(base8(p));
|
|
5454
6185
|
print({ enabled: "realtime", project: p, linked, ...s }, () => {
|
|
5455
6186
|
console.log(`Realtime enabled for "${p}"
|
|
5456
6187
|
`);
|
|
@@ -5461,7 +6192,7 @@ async function realtime(sub, args) {
|
|
|
5461
6192
|
}
|
|
5462
6193
|
case "info": {
|
|
5463
6194
|
const p = need(project, "project name");
|
|
5464
|
-
const [s, stats] = await Promise.all([api(
|
|
6195
|
+
const [s, stats] = await Promise.all([api(base8(p)), api(`${base8(p)}/stats`)]);
|
|
5465
6196
|
const { secret: _secret, ...settings } = s;
|
|
5466
6197
|
print({ ...settings, stats }, () => {
|
|
5467
6198
|
printSettings3({ ...s, secret: "(mybase realtime key)" });
|
|
@@ -5478,7 +6209,7 @@ async function realtime(sub, args) {
|
|
|
5478
6209
|
const p = need(project, "project name");
|
|
5479
6210
|
if (flags.has("rotate"))
|
|
5480
6211
|
confirm(args.includes("--yes"), `Every open realtime connection on "${p}" closes, and tokens signed with the old secret stop working.`, p, "the project name");
|
|
5481
|
-
const s = flags.has("rotate") ? await api(`${
|
|
6212
|
+
const s = flags.has("rotate") ? await api(`${base8(p)}/secret/rotate`, { method: "POST" }) : await api(base8(p));
|
|
5482
6213
|
print({ secret: s.secret }, () => {
|
|
5483
6214
|
if (flags.has("rotate"))
|
|
5484
6215
|
console.log(`Rotated. Update MYBASE_REALTIME_SECRET where your server signs tokens.
|
|
@@ -5508,7 +6239,7 @@ async function realtime(sub, args) {
|
|
|
5508
6239
|
if (n !== undefined)
|
|
5509
6240
|
body[key] = n;
|
|
5510
6241
|
}
|
|
5511
|
-
const s = Object.keys(body).length ? await api(
|
|
6242
|
+
const s = Object.keys(body).length ? await api(base8(p), { method: "PATCH", body: JSON.stringify(body) }) : await api(base8(p));
|
|
5512
6243
|
const { secret: _secret, ...shown } = s;
|
|
5513
6244
|
print(shown, () => printSettings3({ ...s, secret: "(mybase realtime key)" }));
|
|
5514
6245
|
return true;
|
|
@@ -5516,7 +6247,7 @@ async function realtime(sub, args) {
|
|
|
5516
6247
|
case "channels": {
|
|
5517
6248
|
const p = need(project, "project name");
|
|
5518
6249
|
const prefix = flagString(flags, "prefix");
|
|
5519
|
-
const { channels } = await api(`${
|
|
6250
|
+
const { channels } = await api(`${base8(p)}/channels${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`);
|
|
5520
6251
|
print(channels, () => {
|
|
5521
6252
|
if (!channels.length)
|
|
5522
6253
|
return console.log("No channels have subscribers right now.");
|
|
@@ -5535,7 +6266,7 @@ async function realtime(sub, args) {
|
|
|
5535
6266
|
payload = data;
|
|
5536
6267
|
}
|
|
5537
6268
|
}
|
|
5538
|
-
const { ids } = await api(`${
|
|
6269
|
+
const { ids } = await api(`${base8(p)}/publish`, {
|
|
5539
6270
|
method: "POST",
|
|
5540
6271
|
body: JSON.stringify({ channel: need(channel, "channel"), event: need(event, "event"), data: payload })
|
|
5541
6272
|
});
|
|
@@ -5594,8 +6325,8 @@ async function target(flags, needEnv = true) {
|
|
|
5594
6325
|
}
|
|
5595
6326
|
return { project, env: env ?? "" };
|
|
5596
6327
|
}
|
|
5597
|
-
var
|
|
5598
|
-
var envPath = (project, env, rest = "") => `${
|
|
6328
|
+
var base9 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
|
|
6329
|
+
var envPath = (project, env, rest = "") => `${base9(project)}/envs/${encodeURIComponent(env)}${rest}`;
|
|
5599
6330
|
var fetchSecrets = (project, env) => api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}`);
|
|
5600
6331
|
var describeEnv = (e) => [e.name, e.parent ? `branch of ${e.parent}` : "root", `${e.keys} keys`, `rev ${e.revision}`, e.linked ? "linked" : ""].filter(Boolean).join("\t");
|
|
5601
6332
|
async function readStdin() {
|
|
@@ -5614,7 +6345,7 @@ async function secrets(sub, args) {
|
|
|
5614
6345
|
case "enable": {
|
|
5615
6346
|
const p = need(positional[0] ?? flagString(flags, "project"), "project name");
|
|
5616
6347
|
await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "PUT" });
|
|
5617
|
-
const overview = await api(
|
|
6348
|
+
const overview = await api(base9(p));
|
|
5618
6349
|
print({ enabled: "secrets", project: p, ...overview }, () => {
|
|
5619
6350
|
console.log(`Secrets enabled for "${p}": ${overview.envs.map((e) => e.name).join(", ")}
|
|
5620
6351
|
`);
|
|
@@ -5634,7 +6365,7 @@ async function secrets(sub, args) {
|
|
|
5634
6365
|
}
|
|
5635
6366
|
case "setup": {
|
|
5636
6367
|
const { project, env } = await target(flags);
|
|
5637
|
-
const overview = await api(
|
|
6368
|
+
const overview = await api(base9(project));
|
|
5638
6369
|
if (!overview.envs.some((e) => e.name === env)) {
|
|
5639
6370
|
throw usageError(`"${project}" has no env "${env}" (envs: ${overview.envs.map((e) => e.name).join(", ")})`);
|
|
5640
6371
|
}
|
|
@@ -5644,7 +6375,7 @@ async function secrets(sub, args) {
|
|
|
5644
6375
|
}
|
|
5645
6376
|
case "envs": {
|
|
5646
6377
|
const { project } = await target(flags, false);
|
|
5647
|
-
const overview = await api(
|
|
6378
|
+
const overview = await api(base9(project));
|
|
5648
6379
|
print(overview.envs, () => {
|
|
5649
6380
|
for (const e of overview.envs)
|
|
5650
6381
|
console.log(describeEnv(e));
|
|
@@ -5655,7 +6386,7 @@ async function secrets(sub, args) {
|
|
|
5655
6386
|
const [action, name, next] = positional;
|
|
5656
6387
|
const { project } = await target(flags, false);
|
|
5657
6388
|
if (action === "create") {
|
|
5658
|
-
const env = await api(`${
|
|
6389
|
+
const env = await api(`${base9(project)}/envs`, {
|
|
5659
6390
|
method: "POST",
|
|
5660
6391
|
body: JSON.stringify({ name: need(name, "env name"), parent: flagString(flags, "from") ?? null })
|
|
5661
6392
|
});
|
|
@@ -5758,7 +6489,7 @@ async function secrets(sub, args) {
|
|
|
5758
6489
|
const { project, env } = await target(flags);
|
|
5759
6490
|
const envs = env.split(",").map((e) => e.trim()).filter(Boolean);
|
|
5760
6491
|
confirm(yes, `This rotates the credentials of "${project}" (database password, storage key, cache password if the cache is on, and the API key link made before) and stores the new ones, with the other services' keys, in ${envs.join(", ")} plus any env linked before. Apps still using the old credentials will stop working.`, project, "the project name");
|
|
5761
|
-
const result = await api(`${
|
|
6492
|
+
const result = await api(`${base9(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
|
|
5762
6493
|
print(result, () => {
|
|
5763
6494
|
console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
|
|
5764
6495
|
console.log("Restart your apps with `mybase run` to pick them up.");
|
|
@@ -5768,7 +6499,7 @@ async function secrets(sub, args) {
|
|
|
5768
6499
|
case "unlink": {
|
|
5769
6500
|
const { project, env } = await target(flags);
|
|
5770
6501
|
confirm(yes, `This removes the project credentials from "${env}". They stay valid; nothing is rotated.`, env, "the env name");
|
|
5771
|
-
await api(`${
|
|
6502
|
+
await api(`${base9(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
|
|
5772
6503
|
print({ unlinked: env, project }, () => console.log(`Removed the credentials from "${env}"`));
|
|
5773
6504
|
return true;
|
|
5774
6505
|
}
|
|
@@ -5856,10 +6587,10 @@ await init_service_enable();
|
|
|
5856
6587
|
var sandboxUsage = ` sandbox enable <project> [--setup-worker] [--dir <dir>] [--yes]
|
|
5857
6588
|
Enable isolated compute; --setup-worker configures the local host first
|
|
5858
6589
|
sandbox disable <project> [--yes] Stop sandboxes and delete their artifacts
|
|
5859
|
-
sandbox create <project> --key <key> [--ttl
|
|
6590
|
+
sandbox create <project> --key <key> [--ttl <seconds>] [--idle <seconds>]
|
|
5860
6591
|
sandbox list|get|usage <project> [<id>]
|
|
5861
6592
|
sandbox terminate <project> <id> [--yes]
|
|
5862
|
-
sandbox exec <project> <id> --key <key> [--code file.js | --command '["node","script.js"]'] [--timeout
|
|
6593
|
+
sandbox exec <project> <id> --key <key> [--code file.js | --command '["node","script.js"]'] [--timeout <ms>]
|
|
5863
6594
|
sandbox runs|artifacts|browser <project> <id>
|
|
5864
6595
|
sandbox run <project> <id> <execution>
|
|
5865
6596
|
sandbox cancel <project> <id> <execution> [--yes]
|
|
@@ -5867,7 +6598,8 @@ var sandboxUsage = ` sandbox enable <project> [--setup-worker] [--dir <dir>] [-
|
|
|
5867
6598
|
sandbox export <project> <id> <workspace-path> [--type image/png]
|
|
5868
6599
|
sandbox artifact <project> <id> <artifact-id> <local-file>
|
|
5869
6600
|
Creation/execution return promptly. Poll get/run. Cancelling execution terminates sandbox.
|
|
5870
|
-
|
|
6601
|
+
No default lifetime, idle expiry, or execution timeout. Set optional deadlines as needed.
|
|
6602
|
+
Browser credentials expire after 30s; authenticated CDP connections have no fixed lifetime.`;
|
|
5871
6603
|
async function sandbox(sub, args) {
|
|
5872
6604
|
const known = ["enable", "disable", "create", "list", "get", "usage", "terminate", "exec", "runs", "run", "cancel", "upload", "download", "export", "artifacts", "artifact", "browser"];
|
|
5873
6605
|
if (!known.includes(sub))
|
|
@@ -5943,8 +6675,8 @@ async function sandbox(sub, args) {
|
|
|
5943
6675
|
|
|
5944
6676
|
// src/server.ts
|
|
5945
6677
|
init_dist4();
|
|
5946
|
-
import { join as
|
|
5947
|
-
import { styleText as
|
|
6678
|
+
import { join as join9 } from "path";
|
|
6679
|
+
import { styleText as styleText5 } from "util";
|
|
5948
6680
|
|
|
5949
6681
|
// ../../packages/shared/src/logger.ts
|
|
5950
6682
|
var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
@@ -5987,6 +6719,8 @@ var SERVICE_COLORS = {
|
|
|
5987
6719
|
realtime: color.magenta,
|
|
5988
6720
|
sandbox: color.yellow,
|
|
5989
6721
|
"sandbox-worker": color.yellow,
|
|
6722
|
+
hosting: color.green,
|
|
6723
|
+
"hosting-worker": color.green,
|
|
5990
6724
|
"db-public": color.cyan,
|
|
5991
6725
|
dev: color.gray
|
|
5992
6726
|
};
|
|
@@ -6059,9 +6793,125 @@ init_server_env();
|
|
|
6059
6793
|
init_server_sandbox_env();
|
|
6060
6794
|
await __promiseAll([
|
|
6061
6795
|
init_api(),
|
|
6062
|
-
init_server_sandbox()
|
|
6063
|
-
init_server_setup()
|
|
6796
|
+
init_server_sandbox()
|
|
6064
6797
|
]);
|
|
6798
|
+
|
|
6799
|
+
// src/server-hosting.ts
|
|
6800
|
+
init_dist4();
|
|
6801
|
+
init_cloudflare2();
|
|
6802
|
+
init_output();
|
|
6803
|
+
init_server_docker();
|
|
6804
|
+
init_server_env();
|
|
6805
|
+
await init_server_setup();
|
|
6806
|
+
import { join as join8 } from "path";
|
|
6807
|
+
import { styleText as styleText4 } from "util";
|
|
6808
|
+
var LOCAL_HOSTING_WORKER = "http://hosting-worker:4020";
|
|
6809
|
+
var DOMAIN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
6810
|
+
function guessDomain(values) {
|
|
6811
|
+
try {
|
|
6812
|
+
const host = new URL(values.MYBASE_PUBLIC_URL ?? "").hostname;
|
|
6813
|
+
const parts = host.split(".");
|
|
6814
|
+
return parts.length >= 3 && DOMAIN.test(host) ? parts.slice(1).join(".") : null;
|
|
6815
|
+
} catch {
|
|
6816
|
+
return null;
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
function hostingSettings(values, domain) {
|
|
6820
|
+
const key = values.MYBASE_HOSTING_WORKER_KEY || Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex");
|
|
6821
|
+
return {
|
|
6822
|
+
COMPOSE_PROFILES: composeProfile(values.COMPOSE_PROFILES, "hosting", true),
|
|
6823
|
+
MYBASE_HOSTING_WORKER_URL: LOCAL_HOSTING_WORKER,
|
|
6824
|
+
MYBASE_HOSTING_WORKER_KEY: key,
|
|
6825
|
+
MYBASE_HOSTING_DOMAIN: domain
|
|
6826
|
+
};
|
|
6827
|
+
}
|
|
6828
|
+
async function ask2(answer) {
|
|
6829
|
+
const value = await answer;
|
|
6830
|
+
if (isCancel(value))
|
|
6831
|
+
throw new CliError("cancelled; nothing changed");
|
|
6832
|
+
return value;
|
|
6833
|
+
}
|
|
6834
|
+
async function routeWildcard(tunnelToken, domain, apiToken, interactive) {
|
|
6835
|
+
const tunnelId = tunnelIdFromToken(tunnelToken);
|
|
6836
|
+
if (!tunnelId)
|
|
6837
|
+
throw new CliError("CLOUDFLARE_TUNNEL_TOKEN in .env isn't a tunnel token");
|
|
6838
|
+
let token = apiToken;
|
|
6839
|
+
if (!token) {
|
|
6840
|
+
if (!interactive)
|
|
6841
|
+
return false;
|
|
6842
|
+
ui.note([`Same token as the tunnel setup: ${TOKEN_URL}`, ...TOKEN_PERMISSIONS.map((perm) => ` \u2022 ${perm}`), "", "Used now to add *." + domain + ", not saved."].join(`
|
|
6843
|
+
`), "Cloudflare API token");
|
|
6844
|
+
token = (await ask2(password({ message: "Cloudflare API token (empty to skip)" }))).trim();
|
|
6845
|
+
if (!token)
|
|
6846
|
+
return false;
|
|
6847
|
+
}
|
|
6848
|
+
const cf = cloudflare(token);
|
|
6849
|
+
const zone = zoneOf(await cf.listZones(), domain);
|
|
6850
|
+
if (!zone)
|
|
6851
|
+
throw new CliError(`${domain} isn't an active zone on this Cloudflare token`);
|
|
6852
|
+
if (zone.name !== domain)
|
|
6853
|
+
ui.warn(`${domain} is under ${zone.name}: Cloudflare's free certificate only covers one level (*.${zone.name}), so https://<site>.${domain} needs Advanced Certificate Manager.`);
|
|
6854
|
+
const wildcard = `*.${domain}`;
|
|
6855
|
+
const record = await cf.findRecord(zone.id, wildcard).catch(() => null);
|
|
6856
|
+
if (record && !pointsAtTunnel(record, tunnelId)) {
|
|
6857
|
+
if (!interactive)
|
|
6858
|
+
throw new CliError(`${wildcard} already points at ${record.content}; replace it by hand or rerun in a terminal`);
|
|
6859
|
+
const replace = await ask2(confirm2({ message: `${wildcard} already points at ${record.content}. Replace it?`, initialValue: false }));
|
|
6860
|
+
if (!replace)
|
|
6861
|
+
return false;
|
|
6862
|
+
}
|
|
6863
|
+
const current = await cf.getIngress(zone.account.id, tunnelId);
|
|
6864
|
+
await cf.putIngress(zone.account.id, tunnelId, mergeIngress(current, [{ hostname: wildcard, service: "http://caddy:80" }]));
|
|
6865
|
+
await cf.upsertCname(zone.id, wildcard, tunnelId);
|
|
6866
|
+
return true;
|
|
6867
|
+
}
|
|
6868
|
+
async function setupHostingServer(args) {
|
|
6869
|
+
const dir = await serverDir(args);
|
|
6870
|
+
const envFile = Bun.file(join8(dir, ".env"));
|
|
6871
|
+
if (!await envFile.exists())
|
|
6872
|
+
throw new CliError(`no mybase server in ${dir}. Run \`mybase server setup\` (or pass --dir).`, 4);
|
|
6873
|
+
const text = await envFile.text();
|
|
6874
|
+
const values = parseEnv(text);
|
|
6875
|
+
const interactive = !jsonMode && process.stdin.isTTY === true;
|
|
6876
|
+
if (!jsonMode)
|
|
6877
|
+
intro(`${styleText4("bold", " mybase ")} ${styleText4("dim", "server hosting")}`);
|
|
6878
|
+
if (args.includes("--off")) {
|
|
6879
|
+
await Bun.write(join8(dir, ".env"), mergeEnv(text, { COMPOSE_PROFILES: composeProfile(values.COMPOSE_PROFILES, "hosting", false), MYBASE_HOSTING_WORKER_URL: null }));
|
|
6880
|
+
await compose(dir, ["--profile", "hosting", "rm", "-sf", "caddy", "registry", "hosting-worker"]);
|
|
6881
|
+
await startStack(dir, values, false);
|
|
6882
|
+
return print({ dir, hosting: false }, () => outro("Hosting off: sites aren't reachable. Turn it off per project to remove their containers."));
|
|
6883
|
+
}
|
|
6884
|
+
let domain = flag(args, "--domain")?.toLowerCase() ?? null;
|
|
6885
|
+
if (!domain && interactive) {
|
|
6886
|
+
const guess = values.MYBASE_HOSTING_DOMAIN || guessDomain(values) || "";
|
|
6887
|
+
domain = (await ask2(text2({ message: "Base domain for sites (<site>.<domain>); empty for LAN only", placeholder: "example.com", defaultValue: guess, initialValue: guess }))).trim().toLowerCase() || null;
|
|
6888
|
+
} else if (!domain)
|
|
6889
|
+
domain = values.MYBASE_HOSTING_DOMAIN || null;
|
|
6890
|
+
if (domain && !DOMAIN.test(domain))
|
|
6891
|
+
throw usageError(`"${domain}" isn't a domain name`);
|
|
6892
|
+
const updates = hostingSettings(values, domain);
|
|
6893
|
+
let routed = false;
|
|
6894
|
+
if (domain && values.CLOUDFLARE_TUNNEL_TOKEN) {
|
|
6895
|
+
try {
|
|
6896
|
+
routed = await routeWildcard(values.CLOUDFLARE_TUNNEL_TOKEN, domain, flag(args, "--cf-token") ?? process.env.CLOUDFLARE_API_TOKEN, interactive);
|
|
6897
|
+
} catch (err) {
|
|
6898
|
+
throw new CliError(err instanceof CloudflareError ? err.message : err.message);
|
|
6899
|
+
}
|
|
6900
|
+
}
|
|
6901
|
+
await Bun.write(join8(dir, ".env"), mergeEnv(text, updates));
|
|
6902
|
+
const version = await startStack(dir, values);
|
|
6903
|
+
const url = domain ? `https://<site>.${domain}` : `http://<site>.localhost:${values.MYBASE_HOSTING_PORT || "80"}`;
|
|
6904
|
+
print({ dir, version, hosting: true, domain, wildcardRouted: routed }, () => {
|
|
6905
|
+
if (domain && values.CLOUDFLARE_TUNNEL_TOKEN && !routed)
|
|
6906
|
+
ui.warn(`*.${domain} isn't routed yet. In the tunnel's public hostnames, add *.${domain} \u2192 http://caddy:80, and a proxied CNAME *.${domain} \u2192 <tunnel id>.cfargotunnel.com. Or rerun with --cf-token.`);
|
|
6907
|
+
if (domain && !values.CLOUDFLARE_TUNNEL_TOKEN)
|
|
6908
|
+
ui.info(`No tunnel: point *.${domain} at this machine yourself (port ${values.MYBASE_HOSTING_PORT || "80"}, plain HTTP), or run mybase server tunnel first.`);
|
|
6909
|
+
outro(`Hosting is on. Sites answer at ${url}. Next: mybase hosting enable <project>, then mybase deploy.`);
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
// src/server.ts
|
|
6914
|
+
await init_server_setup();
|
|
6065
6915
|
var serverUsage = ` server setup [--dir <dir>] Install mybase on this machine: checks Docker, generates keys, sets up
|
|
6066
6916
|
LAN/Tailscale access, a Cloudflare Tunnel, start on boot, and the admin
|
|
6067
6917
|
[--yes] [--public-host <h>] [--tunnel-token <t> --domain <d>] [--port <n>] [--no-autostart]
|
|
@@ -6071,6 +6921,9 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
|
|
|
6071
6921
|
Set up sandbox host support; optionally enable it for a project
|
|
6072
6922
|
[--deny-hosts <host,ip,\u2026>] [--image <guest>] [--worker-image <worker>]
|
|
6073
6923
|
Public IP/hostnames, keys and release images are detected automatically
|
|
6924
|
+
server hosting [--dir <dir>] [--domain <example.com>] [--cf-token <t>] | --off
|
|
6925
|
+
Run sites here: registry, Caddy, and the hosting worker; with a tunnel,
|
|
6926
|
+
routes *.<domain> to them (the Cloudflare token isn't saved)
|
|
6074
6927
|
server status [--dir <dir>] Containers, health, and whether mybase starts on boot
|
|
6075
6928
|
server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
|
|
6076
6929
|
server restart [--dir <dir>] Restart every container (API, Postgres, storage, cache, search, tunnel)
|
|
@@ -6083,19 +6936,19 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
|
|
|
6083
6936
|
[--host <db.domain> --dns-token <t> [--email <e>] [--staging]] | --off
|
|
6084
6937
|
server autostart on|off Start mybase when this machine boots (and restart it after crashes)
|
|
6085
6938
|
Default dir: the last one setup used, else ~/mybase`;
|
|
6086
|
-
var dim2 = (s) =>
|
|
6087
|
-
var green = (s) =>
|
|
6088
|
-
var red = (s) =>
|
|
6939
|
+
var dim2 = (s) => styleText5("dim", s);
|
|
6940
|
+
var green = (s) => styleText5("green", s);
|
|
6941
|
+
var red = (s) => styleText5("red", s);
|
|
6089
6942
|
async function loadEnv(dir) {
|
|
6090
|
-
const file = Bun.file(
|
|
6091
|
-
if (!await file.exists() || !await Bun.file(
|
|
6943
|
+
const file = Bun.file(join9(dir, ".env"));
|
|
6944
|
+
if (!await file.exists() || !await Bun.file(join9(dir, "compose.yml")).exists()) {
|
|
6092
6945
|
throw new CliError(`no mybase server in ${dir}. Run \`mybase server setup\` (or pass --dir).`, 4);
|
|
6093
6946
|
}
|
|
6094
6947
|
const text = await file.text();
|
|
6095
6948
|
return { text, values: parseEnv(text) };
|
|
6096
6949
|
}
|
|
6097
6950
|
async function saveEnv(dir, text) {
|
|
6098
|
-
await Bun.write(
|
|
6951
|
+
await Bun.write(join9(dir, ".env"), text);
|
|
6099
6952
|
}
|
|
6100
6953
|
async function status(args) {
|
|
6101
6954
|
const dir = await serverDir(args);
|
|
@@ -6153,7 +7006,7 @@ async function update(args) {
|
|
|
6153
7006
|
const { values } = await loadEnv(dir);
|
|
6154
7007
|
const interactive = !jsonMode && process.stdin.isTTY === true && !args.includes("--yes");
|
|
6155
7008
|
if (!jsonMode)
|
|
6156
|
-
intro(`${
|
|
7009
|
+
intro(`${styleText5("bold", " mybase ")} ${dim2("server update")}`);
|
|
6157
7010
|
const replaced = await writeCompose(dir, interactive, true);
|
|
6158
7011
|
if (replaced)
|
|
6159
7012
|
ui.success(`compose.yml updated to this CLI's (${CLI_VERSION})`);
|
|
@@ -6264,7 +7117,7 @@ async function tunnel(args) {
|
|
|
6264
7117
|
const { text, values } = await loadEnv(dir);
|
|
6265
7118
|
const interactive = !jsonMode && process.stdin.isTTY === true;
|
|
6266
7119
|
if (!jsonMode)
|
|
6267
|
-
intro(`${
|
|
7120
|
+
intro(`${styleText5("bold", " mybase ")} ${dim2("server tunnel")}`);
|
|
6268
7121
|
let updates;
|
|
6269
7122
|
let host = null;
|
|
6270
7123
|
if (args.includes("--off")) {
|
|
@@ -6299,7 +7152,7 @@ async function databasePublic(args) {
|
|
|
6299
7152
|
const interactive = !jsonMode && process.stdin.isTTY === true;
|
|
6300
7153
|
const off = args.includes("--off");
|
|
6301
7154
|
if (!jsonMode)
|
|
6302
|
-
intro(`${
|
|
7155
|
+
intro(`${styleText5("bold", " mybase ")} ${dim2("server database-public")}`);
|
|
6303
7156
|
let updates;
|
|
6304
7157
|
if (off) {
|
|
6305
7158
|
updates = Object.fromEntries(DB_PUBLIC_KEYS.map((k) => [k, null]));
|
|
@@ -6414,6 +7267,8 @@ async function server(command, args) {
|
|
|
6414
7267
|
await setup(args);
|
|
6415
7268
|
else if (command === "sandbox")
|
|
6416
7269
|
await setupSandboxServer(args);
|
|
7270
|
+
else if (command === "hosting")
|
|
7271
|
+
await setupHostingServer(args);
|
|
6417
7272
|
else if (command === "status")
|
|
6418
7273
|
await status(args);
|
|
6419
7274
|
else if (command === "update")
|
|
@@ -6437,8 +7292,8 @@ async function server(command, args) {
|
|
|
6437
7292
|
init_config();
|
|
6438
7293
|
init_output();
|
|
6439
7294
|
await init_api();
|
|
6440
|
-
import { chmod as chmod3, rename, rm as
|
|
6441
|
-
import { join as
|
|
7295
|
+
import { chmod as chmod3, rename, rm as rm4 } from "fs/promises";
|
|
7296
|
+
import { join as join10 } from "path";
|
|
6442
7297
|
var updateUsage = ` update [--version <v>] [--cli] [--yes]
|
|
6443
7298
|
Update this CLI to the latest release (or <v>), then the server on this
|
|
6444
7299
|
machine, if there is one. --cli: only the CLI`;
|
|
@@ -6493,11 +7348,11 @@ async function replaceBinary(version) {
|
|
|
6493
7348
|
}
|
|
6494
7349
|
const check = Bun.spawnSync([temp, "help"], { stdout: "ignore", stderr: "ignore" });
|
|
6495
7350
|
if (check.exitCode !== 0) {
|
|
6496
|
-
await
|
|
7351
|
+
await rm4(temp, { force: true });
|
|
6497
7352
|
throw new CliError(`the downloaded binary doesn't run on this machine; kept ${CLI_VERSION}`);
|
|
6498
7353
|
}
|
|
6499
7354
|
if (process.platform === "win32") {
|
|
6500
|
-
await
|
|
7355
|
+
await rm4(`${target}.old`, { force: true });
|
|
6501
7356
|
await rename(target, `${target}.old`);
|
|
6502
7357
|
}
|
|
6503
7358
|
await rename(temp, target);
|
|
@@ -6536,7 +7391,7 @@ async function update2(args) {
|
|
|
6536
7391
|
}
|
|
6537
7392
|
}
|
|
6538
7393
|
const dir = (await loadConfig()).server;
|
|
6539
|
-
const installed = dir && await Bun.file(
|
|
7394
|
+
const installed = dir && await Bun.file(join10(dir, "compose.yml")).exists();
|
|
6540
7395
|
if (installed && !args.includes("--cli")) {
|
|
6541
7396
|
let go = yes || jsonMode;
|
|
6542
7397
|
if (!go && process.stdin.isTTY) {
|
|
@@ -6566,7 +7421,7 @@ var usersUsage = ` users enable <project> Turn on users: an auth schema
|
|
|
6566
7421
|
users revoke <project> <user-id> [<session-id>] Sign a user out (one session, or everywhere)
|
|
6567
7422
|
users delete <project> <user-id> [--yes] Delete a user and their sessions
|
|
6568
7423
|
users disable <project> [--yes] Turn it off, deleting every user account`;
|
|
6569
|
-
var
|
|
7424
|
+
var base10 = (project) => `/projects/${encodeURIComponent(project)}/users`;
|
|
6570
7425
|
function authSnippet(project, apiUrl) {
|
|
6571
7426
|
return `// auth.ts: npm i better-auth pg @mybase-dev/sdk
|
|
6572
7427
|
import { betterAuth } from "better-auth";
|
|
@@ -6597,7 +7452,7 @@ async function users(sub, args) {
|
|
|
6597
7452
|
case "enable": {
|
|
6598
7453
|
const p = need(project, "project name");
|
|
6599
7454
|
const { linked } = await enableService(p, "users", flags);
|
|
6600
|
-
const o = await api(
|
|
7455
|
+
const o = await api(base10(p));
|
|
6601
7456
|
print({ enabled: "users", project: p, linked, ...o }, () => {
|
|
6602
7457
|
console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
|
|
6603
7458
|
`);
|
|
@@ -6611,7 +7466,7 @@ async function users(sub, args) {
|
|
|
6611
7466
|
}
|
|
6612
7467
|
case "init": {
|
|
6613
7468
|
const p = need(project, "project name");
|
|
6614
|
-
const o = await api(
|
|
7469
|
+
const o = await api(base10(p));
|
|
6615
7470
|
const env = flagString(flags, "env");
|
|
6616
7471
|
let stored = null;
|
|
6617
7472
|
if (env) {
|
|
@@ -6634,7 +7489,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
|
|
|
6634
7489
|
case "list": {
|
|
6635
7490
|
const p = need(project, "project name");
|
|
6636
7491
|
const search = flagString(flags, "search");
|
|
6637
|
-
const list = await api(`${
|
|
7492
|
+
const list = await api(`${base10(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
|
|
6638
7493
|
print(list, () => {
|
|
6639
7494
|
if (list.users.length === 0)
|
|
6640
7495
|
console.log(search ? "No users match." : "No users yet.");
|
|
@@ -6647,7 +7502,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
|
|
|
6647
7502
|
}
|
|
6648
7503
|
case "show": {
|
|
6649
7504
|
const p = need(project, "project name");
|
|
6650
|
-
const u = await api(`${
|
|
7505
|
+
const u = await api(`${base10(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
|
|
6651
7506
|
print(u, () => {
|
|
6652
7507
|
console.log(`${u.email ?? u.id}${u.name ? ` (${u.name})` : ""}${u.banned ? " BANNED" : ""}`);
|
|
6653
7508
|
console.log(` id ${u.id}`);
|
|
@@ -6665,10 +7520,10 @@ Active sessions (${u.sessions.length}):`);
|
|
|
6665
7520
|
const p = need(project, "project name");
|
|
6666
7521
|
const id = encodeURIComponent(need(userId, "user id"));
|
|
6667
7522
|
if (sessionId) {
|
|
6668
|
-
await api(`${
|
|
7523
|
+
await api(`${base10(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
6669
7524
|
print({ revoked: 1 }, () => console.log("Signed out that session."));
|
|
6670
7525
|
} else {
|
|
6671
|
-
const r = await api(`${
|
|
7526
|
+
const r = await api(`${base10(p)}/accounts/${id}/sessions`, { method: "DELETE" });
|
|
6672
7527
|
print(r, () => console.log(`Signed out ${r.revoked} session(s).`));
|
|
6673
7528
|
}
|
|
6674
7529
|
return true;
|
|
@@ -6677,7 +7532,7 @@ Active sessions (${u.sessions.length}):`);
|
|
|
6677
7532
|
const p = need(project, "project name");
|
|
6678
7533
|
const id = need(userId, "user id");
|
|
6679
7534
|
confirm(args.includes("--yes"), `This deletes user ${id} of "${p}" and signs them out.`, id, "the user id");
|
|
6680
|
-
await api(`${
|
|
7535
|
+
await api(`${base10(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
6681
7536
|
print({ deleted: id }, () => console.log(`Deleted user ${id}`));
|
|
6682
7537
|
return true;
|
|
6683
7538
|
}
|
|
@@ -6721,6 +7576,7 @@ var USAGE = {
|
|
|
6721
7576
|
users: usersUsage,
|
|
6722
7577
|
captcha: captchaUsage,
|
|
6723
7578
|
realtime: realtimeUsage,
|
|
7579
|
+
hosting: hostingUsage,
|
|
6724
7580
|
run: runUsage
|
|
6725
7581
|
};
|
|
6726
7582
|
var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
|
|
@@ -6859,6 +7715,12 @@ async function main(args) {
|
|
|
6859
7715
|
if (await realtime(sub, rest))
|
|
6860
7716
|
return;
|
|
6861
7717
|
}
|
|
7718
|
+
if (command === "hosting" && sub) {
|
|
7719
|
+
if (await hosting(sub, rest))
|
|
7720
|
+
return;
|
|
7721
|
+
}
|
|
7722
|
+
if (command === "deploy")
|
|
7723
|
+
return deploy(args.slice(1));
|
|
6862
7724
|
if (command && command !== "help")
|
|
6863
7725
|
throw usageError(`unknown command: mybase ${args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
|
|
6864
7726
|
|