@mybase-dev/cli 1.0.12 → 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.
Files changed (2) hide show
  1. package/dist/mybase.js +944 -124
  2. 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.12",
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
- }, log, cancel = (o = "", t) => {
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, text = (t) => new n({
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
- log = {
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
- log.message(s, { ...e, symbol: styleText2("blue", S_INFO) });
1666
+ log2.message(s, { ...e, symbol: styleText2("blue", S_INFO) });
1666
1667
  },
1667
1668
  success: (s, e) => {
1668
- log.message(s, { ...e, symbol: styleText2("green", S_SUCCESS) });
1669
+ log2.message(s, { ...e, symbol: styleText2("green", S_SUCCESS) });
1669
1670
  },
1670
1671
  step: (s, e) => {
1671
- log.message(s, { ...e, symbol: styleText2("green", S_STEP_SUBMIT) });
1672
+ log2.message(s, { ...e, symbol: styleText2("green", S_STEP_SUBMIT) });
1672
1673
  },
1673
1674
  warn: (s, e) => {
1674
- log.message(s, { ...e, symbol: styleText2("yellow", S_WARN) });
1675
+ log2.message(s, { ...e, symbol: styleText2("yellow", S_WARN) });
1675
1676
  },
1676
1677
  warning: (s, e) => {
1677
- log.warn(s, e);
1678
+ log2.warn(s, e);
1678
1679
  },
1679
1680
  error: (s, e) => {
1680
- log.message(s, { ...e, symbol: styleText2("red", S_ERROR) });
1681
+ log2.message(s, { ...e, symbol: styleText2("red", S_ERROR) });
1681
1682
  }
1682
1683
  };
1683
1684
  u2 = {
@@ -1868,8 +1869,17 @@ services:
1868
1869
  MYBASE_SANDBOX_WORKER_URL: \${MYBASE_SANDBOX_WORKER_URL:-}
1869
1870
  MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
1870
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}
1871
1878
  ports:
1872
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
1873
1883
 
1874
1884
  # Official Postgres plus extensions projects can enable (see postgres.Dockerfile).
1875
1885
  # Upgrading from postgres:17-alpine: the data volume is compatible, but text sort order
@@ -1967,6 +1977,60 @@ services:
1967
1977
  start_period: 30s
1968
1978
  # Private Compose network only. No published worker or browser-debugging ports.
1969
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
+
1970
2034
  # Cloudflare Tunnel: public HTTPS without opening router ports. Off unless COMPOSE_PROFILES=tunnel.
1971
2035
  # Hostnames are set up in the Cloudflare dashboard (see the top of this file).
1972
2036
  cloudflared:
@@ -1985,6 +2049,11 @@ services:
1985
2049
  - mybase
1986
2050
 
1987
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
+
1988
2057
  garage:
1989
2058
  content: |
1990
2059
  metadata_dir = "/var/lib/garage/meta"
@@ -2029,6 +2098,10 @@ volumes:
2029
2098
  garage-meta:
2030
2099
  garage-data:
2031
2100
  valkey-data:
2101
+ registry-data:
2102
+ caddy-admin:
2103
+ caddy-config:
2104
+ caddy-data:
2032
2105
  `;
2033
2106
  var init_compose = () => {};
2034
2107
 
@@ -2138,7 +2211,7 @@ function versionAtLeast(version, min) {
2138
2211
  return (a[i] ?? 0) > (b[i] ?? 0);
2139
2212
  return true;
2140
2213
  }
2141
- 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";
2142
2215
  var init_server_env = __esm(() => {
2143
2216
  SECRET_KEYS = {
2144
2217
  POSTGRES_PASSWORD: { bytes: 24, encoding: "hex" },
@@ -2173,7 +2246,7 @@ var init_server_env = __esm(() => {
2173
2246
  });
2174
2247
 
2175
2248
  // src/server-docker.ts
2176
- import { existsSync, readFileSync } from "fs";
2249
+ import { existsSync as existsSync2, readFileSync } from "fs";
2177
2250
  import { statfs } from "fs/promises";
2178
2251
  import { freemem, hostname as hostname2, networkInterfaces, totalmem } from "os";
2179
2252
  function run2(cmd, cwd) {
@@ -2219,7 +2292,7 @@ async function compose(dir, args) {
2219
2292
  return { ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() };
2220
2293
  }
2221
2294
  async function stackExists(dir) {
2222
- if (!existsSync(`${dir}/compose.yml`))
2295
+ if (!existsSync2(`${dir}/compose.yml`))
2223
2296
  return false;
2224
2297
  const ps = await compose(dir, ["ps", "-a", "-q"]);
2225
2298
  return ps.ok && ps.stdout.length > 0;
@@ -2271,7 +2344,7 @@ function machine() {
2271
2344
  }
2272
2345
  async function freeDiskGb(dir) {
2273
2346
  for (let d = dir;; d = d.replace(/\/[^/]*$/, "") || "/") {
2274
- if (existsSync(d)) {
2347
+ if (existsSync2(d)) {
2275
2348
  try {
2276
2349
  const s = await statfs(d);
2277
2350
  return s.bavail * s.bsize / 2 ** 30;
@@ -2368,7 +2441,7 @@ var MIN_COMPOSE = "2.23.1", DOCKER_INSTALL_URL = "https://docs.docker.com/engine
2368
2441
  var init_server_docker = __esm(() => {
2369
2442
  init_compose();
2370
2443
  init_server_env();
2371
- 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"];
2372
2445
  LOG_ALIASES = {
2373
2446
  api: "mybase",
2374
2447
  dashboard: "mybase",
@@ -2380,16 +2453,18 @@ var init_server_docker = __esm(() => {
2380
2453
  search: "searxng",
2381
2454
  tunnel: "cloudflared",
2382
2455
  sandbox: "sandbox-worker",
2383
- worker: "sandbox-worker"
2456
+ worker: "sandbox-worker",
2457
+ hosting: "hosting-worker",
2458
+ proxy: "caddy"
2384
2459
  };
2385
2460
  });
2386
2461
 
2387
2462
  // src/server-gvisor.ts
2388
- import { createHash } from "crypto";
2389
- import { access, mkdtemp, rm } from "fs/promises";
2463
+ import { createHash as createHash2 } from "crypto";
2464
+ import { access, mkdtemp, rm as rm3 } from "fs/promises";
2390
2465
  import { constants as constants2 } from "fs";
2391
- import { tmpdir } from "os";
2392
- import { join as join3 } from "path";
2466
+ import { tmpdir as tmpdir3 } from "os";
2467
+ import { join as join5 } from "path";
2393
2468
  async function tail(stream) {
2394
2469
  const decoder = new TextDecoder;
2395
2470
  let text = "";
@@ -2443,7 +2518,7 @@ async function installGvisor(info, run = hostRun) {
2443
2518
  break;
2444
2519
  } catch {}
2445
2520
  }
2446
- const root = await mkdtemp(join3(tmpdir(), "mybase-gvisor-"));
2521
+ const root = await mkdtemp(join5(tmpdir3(), "mybase-gvisor-"));
2447
2522
  try {
2448
2523
  let archive = null;
2449
2524
  if (!binary) {
@@ -2456,9 +2531,9 @@ async function installGvisor(info, run = hostRun) {
2456
2531
  const response = await fetch(`${base}/gvisor.tar.bz2`, { signal: AbortSignal.timeout(180000) });
2457
2532
  if (!response.ok || !response.body)
2458
2533
  throw new CliError("couldn't download the gVisor release bundle");
2459
- archive = join3(root, "gvisor.tar.bz2");
2534
+ archive = join5(root, "gvisor.tar.bz2");
2460
2535
  const sink = Bun.file(archive).writer();
2461
- const hash = createHash("sha512");
2536
+ const hash = createHash2("sha512");
2462
2537
  let size = 0;
2463
2538
  try {
2464
2539
  for await (const chunk of response.body) {
@@ -2501,7 +2576,7 @@ async function installGvisor(info, run = hostRun) {
2501
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");
2502
2577
  return true;
2503
2578
  } finally {
2504
- await rm(root, { recursive: true, force: true });
2579
+ await rm3(root, { recursive: true, force: true });
2505
2580
  }
2506
2581
  }
2507
2582
  var hostRun = async (args, options = {}) => {
@@ -2638,6 +2713,10 @@ function cloudflare(token, fetcher = fetch, permissions = TOKEN_PERMISSIONS) {
2638
2713
  putIngress: (accountId, tunnelId, rules) => call("PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {
2639
2714
  config: { ingress: [...rules, { service: "http_status:404" }] }
2640
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
+ },
2641
2720
  async findRecord(zoneId, name) {
2642
2721
  const records = await call("GET", `/zones/${zoneId}/dns_records?name=${encodeURIComponent(name)}`);
2643
2722
  return records[0] ?? null;
@@ -2653,6 +2732,11 @@ function cloudflare(token, fetcher = fetch, permissions = TOKEN_PERMISSIONS) {
2653
2732
  tunnelToken: (accountId, tunnelId) => call("GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`)
2654
2733
  };
2655
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
+ }
2656
2740
  function zoneOf(zones, host) {
2657
2741
  const name = host.toLowerCase().replace(/\.$/, "");
2658
2742
  let best = null;
@@ -2679,7 +2763,7 @@ var init_cloudflare2 = __esm(() => {
2679
2763
  // src/server-setup.ts
2680
2764
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
2681
2765
  import { homedir as homedir2, hostname as hostname3 } from "os";
2682
- import { join as join4, resolve as resolve4 } from "path";
2766
+ import { join as join6, resolve as resolve5 } from "path";
2683
2767
  import { styleText as styleText3 } from "util";
2684
2768
  function flag(args, name) {
2685
2769
  const i = args.indexOf(name);
@@ -2693,7 +2777,7 @@ function flag(args, name) {
2693
2777
  async function serverDir(args) {
2694
2778
  const given = flag(args, "--dir");
2695
2779
  if (given)
2696
- return resolve4(given.replace(/^~(?=$|\/)/, homedir2()));
2780
+ return resolve5(given.replace(/^~(?=$|\/)/, homedir2()));
2697
2781
  return (await loadConfig()).server ?? DEFAULT_DIR;
2698
2782
  }
2699
2783
  async function ask(answer) {
@@ -2732,13 +2816,13 @@ async function preflight(dir) {
2732
2816
  async function chooseDir(args, interactive) {
2733
2817
  let dir = await serverDir(args);
2734
2818
  if (interactive && !args.includes("--dir")) {
2735
- const answer = await ask(text({
2819
+ const answer = await ask(text2({
2736
2820
  message: "Where should mybase keep its files?",
2737
2821
  placeholder: tilde(dir),
2738
2822
  defaultValue: dir,
2739
2823
  validate: (v) => v && !v.trim().startsWith("/") && !v.trim().startsWith("~") ? "use an absolute path (or ~/\u2026)" : undefined
2740
2824
  }));
2741
- dir = resolve4(answer.trim().replace(/^~(?=$|\/)/, homedir2()));
2825
+ dir = resolve5(answer.trim().replace(/^~(?=$|\/)/, homedir2()));
2742
2826
  }
2743
2827
  const other = otherInstall(dir);
2744
2828
  if (other) {
@@ -2755,7 +2839,7 @@ async function chooseHost(current) {
2755
2839
  const picked = await ask(select({ message: "Which address should apps on your network use?", options, initialValue: current ?? options[0]?.value }));
2756
2840
  if (picked)
2757
2841
  return picked;
2758
- return (await ask(text({ message: "Address (hostname or IP)", validate: (v) => v?.trim() ? undefined : "required" }))).trim();
2842
+ return (await ask(text2({ message: "Address (hostname or IP)", validate: (v) => v?.trim() ? undefined : "required" }))).trim();
2759
2843
  }
2760
2844
  async function tunnelStep() {
2761
2845
  const mode = await ask(select({
@@ -2816,8 +2900,8 @@ async function autoTunnel() {
2816
2900
  let apiHost = "";
2817
2901
  let s3Host = "";
2818
2902
  for (;; ) {
2819
- const apiSub = await ask(text({ message: "Subdomain for the dashboard + API", placeholder: "mybase", defaultValue: "mybase" }));
2820
- const s3Sub = await ask(text({ message: "Subdomain for S3 storage", placeholder: "s3", defaultValue: "s3" }));
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" }));
2821
2905
  apiHost = `${apiSub.trim()}.${zone.name}`;
2822
2906
  s3Host = `${s3Sub.trim()}.${zone.name}`;
2823
2907
  if (validHost(apiHost) || validHost(s3Host) || apiHost === s3Host) {
@@ -2838,10 +2922,11 @@ async function autoTunnel() {
2838
2922
  }
2839
2923
  s.start("Routing hostnames to the tunnel");
2840
2924
  try {
2841
- await cf.putIngress(zone.account.id, tunnel.id, [
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, [
2842
2927
  { hostname: apiHost, service: "http://mybase:4000" },
2843
2928
  { hostname: s3Host, service: "http://garage:3900" }
2844
- ]);
2929
+ ]));
2845
2930
  await cf.upsertCname(zone.id, apiHost, tunnel.id);
2846
2931
  await cf.upsertCname(zone.id, s3Host, tunnel.id);
2847
2932
  const tunnelToken = await cf.tunnelToken(zone.account.id, tunnel.id);
@@ -2868,9 +2953,9 @@ async function manualTunnel() {
2868
2953
  ].join(`
2869
2954
  `), "Cloudflare Tunnel, by hand");
2870
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)" }));
2871
- const apiHost = (await ask(text({ message: "Hostname for the dashboard + API", placeholder: "mybase.example.com", validate: validHost }))).trim();
2956
+ const apiHost = (await ask(text2({ message: "Hostname for the dashboard + API", placeholder: "mybase.example.com", validate: validHost }))).trim();
2872
2957
  const defaultS3 = apiHost.replace(/^[^.]+/, "s3");
2873
- const s3Host = (await ask(text({ message: "Hostname for S3", placeholder: defaultS3, defaultValue: defaultS3, validate: (v) => validHost(v || defaultS3) }))).trim();
2958
+ const s3Host = (await ask(text2({ message: "Hostname for S3", placeholder: defaultS3, defaultValue: defaultS3, validate: (v) => validHost(v || defaultS3) }))).trim();
2874
2959
  return { env: tunnelEnv(token.trim(), apiHost, s3Host), apiHost, s3Host };
2875
2960
  }
2876
2961
  async function bindStep(lan, existing) {
@@ -2916,7 +3001,7 @@ async function dbPublicStep(existing) {
2916
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 }));
2917
3002
  const chosen = zones.find((z) => z.id === zoneId);
2918
3003
  const suggested = existing.MYBASE_DB_PUBLIC_HOST && zoneOf([chosen], existing.MYBASE_DB_PUBLIC_HOST) ? existing.MYBASE_DB_PUBLIC_HOST : `db.${chosen.name}`;
2919
- const host = (await ask(text({
3004
+ const host = (await ask(text2({
2920
3005
  message: "Hostname for the database",
2921
3006
  placeholder: suggested,
2922
3007
  defaultValue: suggested,
@@ -2934,7 +3019,7 @@ async function dbPublicStep(existing) {
2934
3019
  continue;
2935
3020
  await cf.deleteRecord(chosen.id, record.id);
2936
3021
  }
2937
- const email = (await ask(text({ message: "Email for Let's Encrypt expiry notices (optional)", placeholder: "skip" }))).trim();
3022
+ const email = (await ask(text2({ message: "Email for Let's Encrypt expiry notices (optional)", placeholder: "skip" }))).trim();
2938
3023
  return {
2939
3024
  MYBASE_DB_PUBLIC: "on",
2940
3025
  MYBASE_DB_PUBLIC_HOST: host,
@@ -2954,7 +3039,7 @@ async function adminStep() {
2954
3039
  }));
2955
3040
  if (when === "later")
2956
3041
  return null;
2957
- const username = (await ask(text({ message: "Admin username", placeholder: "admin", defaultValue: "admin", validate: (v) => !v || USERNAME_PATTERN.test(v) ? undefined : "letters, digits, or . _ @ -" }))).trim();
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();
2958
3043
  for (;; ) {
2959
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` }));
2960
3045
  const again = await ask(password({ message: "Same password again" }));
@@ -3007,7 +3092,7 @@ async function advancedStep(env) {
3007
3092
  else if (env[key] !== undefined)
3008
3093
  out[key] = null;
3009
3094
  };
3010
- const port = (key, label, fallback) => ask(text({ message: `${label} port`, placeholder: env[key] ?? fallback, defaultValue: env[key] ?? fallback, validate: (v) => v ? validPort(v) : undefined })).then((v) => set(key, v.trim(), fallback));
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));
3011
3096
  await port("MYBASE_PORT", "Dashboard + API", "4000");
3012
3097
  await port("POSTGRES_PORT", "Postgres", "5432");
3013
3098
  await port("GARAGE_S3_PORT", "S3", "3900");
@@ -3018,7 +3103,7 @@ async function advancedStep(env) {
3018
3103
  options: ["64mb", "128mb", "256mb", "512mb", "1gb"].map((v) => ({ value: v, label: v, hint: v === "64mb" ? "default, fine for a Pi" : undefined }))
3019
3104
  }));
3020
3105
  set("CACHE_MAXMEMORY", memory, "64mb");
3021
- const capacity = await ask(text({ 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" }));
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" }));
3022
3107
  set("GARAGE_CAPACITY", capacity.trim(), "100G");
3023
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 })) }));
3024
3109
  set("LOG_LEVEL", level, "info");
@@ -3041,28 +3126,28 @@ async function portStep(env, interactive) {
3041
3126
  let next = Number(current) + 1;
3042
3127
  while (!portFree(next) && next < 65535)
3043
3128
  next++;
3044
- const answer = await ask(text({ 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 }));
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 }));
3045
3130
  out[key] = answer.trim();
3046
3131
  }
3047
3132
  return out;
3048
3133
  }
3049
3134
  async function writeFiles(dir, envText, interactive, replaceCompose = interactive) {
3050
3135
  await mkdir3(dir, { recursive: true });
3051
- const envPath = join4(dir, ".env");
3136
+ const envPath = join6(dir, ".env");
3052
3137
  await Bun.write(envPath, envText);
3053
3138
  await chmod2(envPath, 384);
3054
3139
  await writeCompose(dir, interactive, replaceCompose);
3055
3140
  }
3056
3141
  async function writeCompose(dir, interactive, replace) {
3057
- const file = Bun.file(join4(dir, "compose.yml"));
3142
+ const file = Bun.file(join6(dir, "compose.yml"));
3058
3143
  if (await file.exists()) {
3059
3144
  const current = await file.text();
3060
3145
  if (current === compose_default)
3061
3146
  return false;
3062
- const ok = interactive ? await ask(confirm2({ message: `${tilde(join4(dir, "compose.yml"))} differs from this CLI's (${CLI_VERSION}). Replace it? (the old one is kept as compose.yml.bak)` })) : replace;
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;
3063
3148
  if (!ok)
3064
3149
  return false;
3065
- await Bun.write(join4(dir, "compose.yml.bak"), current);
3150
+ await Bun.write(join6(dir, "compose.yml.bak"), current);
3066
3151
  }
3067
3152
  await Bun.write(file, compose_default);
3068
3153
  return true;
@@ -3117,7 +3202,7 @@ ${tail2(pulled.stderr || pulled.stdout)}${pullHint(pulled.stderr || pulled.stdou
3117
3202
  } else
3118
3203
  s.stop("Images downloaded");
3119
3204
  }
3120
- const current = parseEnv(await Bun.file(join4(dir, ".env")).text());
3205
+ const current = parseEnv(await Bun.file(join6(dir, ".env")).text());
3121
3206
  if (sandboxProfileOn(current)) {
3122
3207
  s.start("Updating sandbox runtime images");
3123
3208
  await init_server_sandbox();
@@ -3190,7 +3275,7 @@ async function setup(args) {
3190
3275
  ${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any time.")}`);
3191
3276
  const dir = await chooseDir(args, interactive);
3192
3277
  await preflight(dir);
3193
- const envFile = Bun.file(join4(dir, ".env"));
3278
+ const envFile = Bun.file(join6(dir, ".env"));
3194
3279
  const envText = await envFile.exists() ? await envFile.text() : null;
3195
3280
  const existing = envText ? parseEnv(envText) : {};
3196
3281
  const running = await stackExists(dir);
@@ -3307,7 +3392,7 @@ ${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any tim
3307
3392
  ui.note([
3308
3393
  ...Object.entries(values).map(([k, v]) => `${k.padEnd(24)} ${isSecretKey(k) ? dim(maskSecret(v)) : v}`),
3309
3394
  "",
3310
- dim(`Writes ${tilde(join4(dir, ".env"))} (only you can read it) and ${tilde(join4(dir, "compose.yml"))}`)
3395
+ dim(`Writes ${tilde(join6(dir, ".env"))} (only you can read it) and ${tilde(join6(dir, "compose.yml"))}`)
3311
3396
  ].join(`
3312
3397
  `), "Settings");
3313
3398
  if (interactive && !await ask(confirm2({ message: running ? "Save and restart mybase?" : "Save and start mybase?" }))) {
@@ -3358,10 +3443,10 @@ ${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any tim
3358
3443
  ].join(`
3359
3444
  `), "mybase is running");
3360
3445
  if (secrets.MYBASE_SECRETS_KEY) {
3361
- ui.warn(`${bold("Back up MYBASE_SECRETS_KEY")} (in ${tilde(join4(dir, ".env"))}) somewhere other than this machine,
3446
+ ui.warn(`${bold("Back up MYBASE_SECRETS_KEY")} (in ${tilde(join6(dir, ".env"))}) somewhere other than this machine,
3362
3447
  like a password manager. If it's lost, every stored secret is lost with it.`);
3363
3448
  if (interactive && await ask(confirm2({ message: "Show it now so you can copy it?", initialValue: false }))) {
3364
- log.message(secrets.MYBASE_SECRETS_KEY);
3449
+ log2.message(secrets.MYBASE_SECRETS_KEY);
3365
3450
  }
3366
3451
  }
3367
3452
  outro(`Done. Config for this CLI: ${dim(tilde(configPath))}`);
@@ -3380,17 +3465,17 @@ var init_server_setup = __esm(async () => {
3380
3465
  init_server_env();
3381
3466
  init_server_sandbox_env();
3382
3467
  await init_api();
3383
- DEFAULT_DIR = join4(homedir2(), "mybase");
3468
+ DEFAULT_DIR = join6(homedir2(), "mybase");
3384
3469
  USERNAME_PATTERN = /^[a-zA-Z0-9_.@-]{1,64}$/;
3385
3470
  HOST_PATTERN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
3386
3471
  TUNNEL_KEYS = ["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY"];
3387
3472
  BIND_KEYS = ["POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"];
3388
3473
  ui = {
3389
- info: (m) => !jsonMode && log.info(m),
3390
- success: (m) => !jsonMode && log.success(m),
3391
- warn: (m) => !jsonMode && log.warn(m),
3392
- error: (m) => !jsonMode && log.error(m),
3393
- step: (m) => !jsonMode && log.step(m),
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),
3394
3479
  note: (m, title) => !jsonMode && note(m, title),
3395
3480
  spinner() {
3396
3481
  return jsonMode ? { start() {}, stop() {}, error() {}, message() {} } : spinner();
@@ -3407,7 +3492,7 @@ __export(exports_server_sandbox, {
3407
3492
  sandboxWorkerHealth: () => sandboxWorkerHealth,
3408
3493
  setupSandboxServer: () => setupSandboxServer
3409
3494
  });
3410
- import { join as join5 } from "path";
3495
+ import { join as join7 } from "path";
3411
3496
  import { isIP as isIP3 } from "net";
3412
3497
  async function publicIp() {
3413
3498
  try {
@@ -3446,8 +3531,8 @@ async function refreshSandboxImages(dir, values, pull = true, run = hostRun) {
3446
3531
  await pullImage(images.workerImage, run, pull);
3447
3532
  const updates = { MYBASE_SANDBOX_IMAGE: images.image, MYBASE_SANDBOX_WORKER_IMAGE: images.workerImage };
3448
3533
  if (Object.entries(updates).some(([key, value]) => values[key] !== value)) {
3449
- const text = await Bun.file(join5(dir, ".env")).text();
3450
- await Bun.write(join5(dir, ".env"), mergeEnv(text, updates));
3534
+ const text = await Bun.file(join7(dir, ".env")).text();
3535
+ await Bun.write(join7(dir, ".env"), mergeEnv(text, updates));
3451
3536
  }
3452
3537
  return { ...values, ...updates };
3453
3538
  }
@@ -3505,8 +3590,8 @@ Check the host's gVisor and cgroup support. Ordinary containers will not be used
3505
3590
  async function prepareSandboxHost(args, options = {}, overrides = {}) {
3506
3591
  const io = { ...defaults, ...overrides };
3507
3592
  const dir = await serverDir(args);
3508
- const file = Bun.file(join5(dir, ".env"));
3509
- if (!await file.exists() || !await Bun.file(join5(dir, "compose.yml")).exists())
3593
+ const file = Bun.file(join7(dir, ".env"));
3594
+ if (!await file.exists() || !await Bun.file(join7(dir, "compose.yml")).exists())
3510
3595
  throw new CliError(`no mybase server in ${dir}; run mybase server setup --sandboxes first`, 4);
3511
3596
  const text = await file.text();
3512
3597
  const values = parseEnv(text);
@@ -4922,6 +5007,611 @@ async function doctor(args) {
4922
5007
  }
4923
5008
  }
4924
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
+
4925
5615
  // src/queue.ts
4926
5616
  init_src();
4927
5617
  init_output();
@@ -4942,8 +5632,8 @@ var queueUsage = ` queue enable <project> Turn on queues
4942
5632
  queue redrive <project> <dlq> [--to <queue>] Move dead letters back to their queues
4943
5633
  queue stats <project> [--range 24h|7d] Sent, received, deleted, dead-lettered per queue
4944
5634
  queue sync [--config <file>] [--dry-run] [--yes] Make code-defined queues match mybase.config.ts`;
4945
- var base4 = (project) => `/projects/${encodeURIComponent(project)}/queue`;
4946
- var queuePath = (project, name, rest = "") => `${base4(project)}/queues/${encodeURIComponent(name)}${rest}`;
5635
+ var base5 = (project) => `/projects/${encodeURIComponent(project)}/queue`;
5636
+ var queuePath = (project, name, rest = "") => `${base5(project)}/queues/${encodeURIComponent(name)}${rest}`;
4947
5637
  function formatMs(ms) {
4948
5638
  for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
4949
5639
  if (ms >= size && ms % size === 0)
@@ -4967,7 +5657,7 @@ async function queue(sub, args) {
4967
5657
  case "enable": {
4968
5658
  const p = need(project, "project name");
4969
5659
  const { linked } = await enableService(p, "queue", flags);
4970
- const settings = await api(base4(p));
5660
+ const settings = await api(base5(p));
4971
5661
  print({ enabled: "queue", project: p, linked, ...settings }, () => {
4972
5662
  console.log(`Queues enabled for "${p}"
4973
5663
  `);
@@ -4985,7 +5675,7 @@ async function queue(sub, args) {
4985
5675
  }
4986
5676
  case "list": {
4987
5677
  const p = need(project, "project name");
4988
- const queues = await api(`${base4(p)}/queues`);
5678
+ const queues = await api(`${base5(p)}/queues`);
4989
5679
  print(queues, () => {
4990
5680
  if (queues.length === 0)
4991
5681
  console.log("No queues yet.");
@@ -5011,7 +5701,7 @@ async function queue(sub, args) {
5011
5701
  contentDedup: flags.has("content-dedup") || undefined,
5012
5702
  deadLetter: dlq ? { queue: dlq, maxReceives: flagInt(flags, "max-receives") ?? 5 } : undefined
5013
5703
  };
5014
- const q = await api(`${base4(p)}/queues`, { method: "POST", body: JSON.stringify(body) });
5704
+ const q = await api(`${base5(p)}/queues`, { method: "POST", body: JSON.stringify(body) });
5015
5705
  print(q, () => console.log(`Created ${q.fifo ? "FIFO" : "standard"} queue "${q.name}" (visibility ${formatMs(q.visibilityTimeoutMs)}, retention ${formatMs(q.retentionMs)})`));
5016
5706
  return true;
5017
5707
  }
@@ -5096,7 +5786,7 @@ async function queue(sub, args) {
5096
5786
  const range = flagString(flags, "range") ?? "24h";
5097
5787
  if (!QUEUE_STATS_RANGES.includes(range))
5098
5788
  throw new Error(`--range must be one of ${QUEUE_STATS_RANGES.join(", ")}`);
5099
- const stats = await api(`${base4(p)}/stats?range=${range}`);
5789
+ const stats = await api(`${base5(p)}/stats?range=${range}`);
5100
5790
  print(stats, () => {
5101
5791
  const { queues } = stats;
5102
5792
  console.log(`Last ${range}:`);
@@ -5125,7 +5815,7 @@ function parseDuration(value) {
5125
5815
  async function sync2(file, dryRun, yes) {
5126
5816
  const config = await readConfigFile(file);
5127
5817
  const queues = Object.entries(config.queue?.queues ?? {}).map(([name, q]) => ({ ...q, name }));
5128
- const path = `${base4(config.project)}/queues`;
5818
+ const path = `${base5(config.project)}/queues`;
5129
5819
  const plan = await api(path, { method: "PUT", body: JSON.stringify({ queues, dryRun: true }) });
5130
5820
  if (dryRun)
5131
5821
  return printSync(config.project, true, plan);
@@ -5151,8 +5841,8 @@ var vectorUsage = ` vector enable <project> Turn on vector + full-text s
5151
5841
  vector documents <project> <index> [--limit 20] [--after <id>] List documents in id order
5152
5842
  vector query <project> <index> [<text>] [--mode text|vector|hybrid] [--top 10] [--filter <json>]
5153
5843
  vector sync [--config <file>] [--dry-run] [--yes] Make code-defined indexes match mybase.config.ts`;
5154
- var base5 = (project) => `/projects/${encodeURIComponent(project)}/vector`;
5155
- var indexPath = (project, name, rest = "") => `${base5(project)}/indexes/${encodeURIComponent(name)}${rest}`;
5844
+ var base6 = (project) => `/projects/${encodeURIComponent(project)}/vector`;
5845
+ var indexPath = (project, name, rest = "") => `${base6(project)}/indexes/${encodeURIComponent(name)}${rest}`;
5156
5846
  function describe(i) {
5157
5847
  const vectors = i.dimensions ? `${i.dimensions}d ${i.metric}` : "text only";
5158
5848
  return [i.name, vectors, i.language, i.embed ? "embeds" : "", `${i.documents} docs`, i.source].filter(Boolean).join("\t");
@@ -5171,7 +5861,7 @@ async function vector(sub, args) {
5171
5861
  case "enable": {
5172
5862
  const p = need(project, "project name");
5173
5863
  const { linked } = await enableService(p, "vector", flags);
5174
- const settings = await api(base5(p));
5864
+ const settings = await api(base6(p));
5175
5865
  print({ enabled: "vector", project: p, linked, ...settings }, () => {
5176
5866
  console.log(`Vector search enabled for "${p}"
5177
5867
  `);
@@ -5189,7 +5879,7 @@ async function vector(sub, args) {
5189
5879
  }
5190
5880
  case "settings": {
5191
5881
  const p = need(project, "project name");
5192
- const settings = await api(base5(p));
5882
+ const settings = await api(base6(p));
5193
5883
  print(settings, () => {
5194
5884
  const e = settings.embedding;
5195
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)");
@@ -5208,13 +5898,13 @@ async function vector(sub, args) {
5208
5898
  const apiKey = flags.has("key") ? await readSecret("API key: ") : undefined;
5209
5899
  body = { embedding: { url: need(flagString(flags, "url"), "--url"), model: need(flagString(flags, "model"), "--model"), dimensions: dims, apiKey } };
5210
5900
  }
5211
- const settings = await api(base5(p), { method: "PUT", body: JSON.stringify(body) });
5901
+ const settings = await api(base6(p), { method: "PUT", body: JSON.stringify(body) });
5212
5902
  print(settings, () => console.log(settings.embedding ? `Embedding with ${settings.embedding.model} (${settings.embedding.dimensions}d)` : "Embedding provider removed"));
5213
5903
  return true;
5214
5904
  }
5215
5905
  case "list": {
5216
5906
  const p = need(project, "project name");
5217
- const indexes = await api(`${base5(p)}/indexes`);
5907
+ const indexes = await api(`${base6(p)}/indexes`);
5218
5908
  print(indexes, () => {
5219
5909
  if (indexes.length === 0)
5220
5910
  console.log("No indexes yet.");
@@ -5232,7 +5922,7 @@ async function vector(sub, args) {
5232
5922
  language: flagString(flags, "lang"),
5233
5923
  embed: flags.has("embed") || undefined
5234
5924
  };
5235
- const index = await api(`${base5(p)}/indexes`, { method: "POST", body: JSON.stringify(body) });
5925
+ const index = await api(`${base6(p)}/indexes`, { method: "POST", body: JSON.stringify(body) });
5236
5926
  print(index, () => console.log(`Created index "${index.name}" (${index.dimensions ? `${index.dimensions}d ${index.metric}` : "text only"}, ${index.language})`));
5237
5927
  return true;
5238
5928
  }
@@ -5315,7 +6005,7 @@ async function vector(sub, args) {
5315
6005
  async function sync3(file, dryRun, yes) {
5316
6006
  const config = await readConfigFile(file);
5317
6007
  const indexes = Object.entries(config.vector?.indexes ?? {}).map(([name, i]) => ({ ...i, name }));
5318
- const path = `${base5(config.project)}/indexes`;
6008
+ const path = `${base6(config.project)}/indexes`;
5319
6009
  const plan = await api(path, { method: "PUT", body: JSON.stringify({ indexes, dryRun: true }) });
5320
6010
  if (dryRun)
5321
6011
  return printSync(config.project, true, plan);
@@ -5340,7 +6030,7 @@ var ratelimitUsage = ` ratelimit enable <project> Turn on rate limits
5340
6030
  ratelimit delete <project> <name> Delete a rule
5341
6031
  ratelimit stats <project> [--range 24h|7d] Allowed and blocked requests per rule
5342
6032
  ratelimit sync [--config <file>] [--dry-run] Make code-defined rules match mybase.config.ts`;
5343
- var base6 = (project) => `/projects/${encodeURIComponent(project)}/ratelimit`;
6033
+ var base7 = (project) => `/projects/${encodeURIComponent(project)}/ratelimit`;
5344
6034
  function formatWindow(ms) {
5345
6035
  for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
5346
6036
  if (ms % size === 0)
@@ -5364,7 +6054,7 @@ async function ratelimit(sub, args) {
5364
6054
  case "enable": {
5365
6055
  const p = need(project, "project name");
5366
6056
  const { linked } = await enableService(p, "ratelimit", flags);
5367
- const settings = await api(base6(p));
6057
+ const settings = await api(base7(p));
5368
6058
  print({ enabled: "ratelimit", project: p, linked, ...settings }, () => {
5369
6059
  console.log(`Rate limits enabled for "${p}"
5370
6060
  `);
@@ -5382,7 +6072,7 @@ async function ratelimit(sub, args) {
5382
6072
  }
5383
6073
  case "rules": {
5384
6074
  const p = need(project, "project name");
5385
- const rules = await api(`${base6(p)}/rules`);
6075
+ const rules = await api(`${base7(p)}/rules`);
5386
6076
  print(rules, () => {
5387
6077
  if (rules.length === 0)
5388
6078
  console.log("No rules yet. Inline limits still work.");
@@ -5402,14 +6092,14 @@ async function ratelimit(sub, args) {
5402
6092
  algorithm: flagString(flags, "algorithm"),
5403
6093
  burst: flagInt(flags, "burst")
5404
6094
  };
5405
- const r = await api(`${base6(p)}/rules`, { method: "POST", body: JSON.stringify(body) });
6095
+ const r = await api(`${base7(p)}/rules`, { method: "POST", body: JSON.stringify(body) });
5406
6096
  print(r, () => console.log(`Created rule "${r.name}": ${r.limit} per ${formatWindow(r.windowMs)} (${r.algorithm})`));
5407
6097
  return true;
5408
6098
  }
5409
6099
  case "delete": {
5410
6100
  const p = need(project, "project name");
5411
6101
  const n = need(name, "rule name");
5412
- await api(`${base6(p)}/rules/${encodeURIComponent(n)}`, { method: "DELETE" });
6102
+ await api(`${base7(p)}/rules/${encodeURIComponent(n)}`, { method: "DELETE" });
5413
6103
  print({ deleted: n }, () => console.log(`Deleted rule "${n}"`));
5414
6104
  return true;
5415
6105
  }
@@ -5418,7 +6108,7 @@ async function ratelimit(sub, args) {
5418
6108
  const range = flagString(flags, "range") ?? "24h";
5419
6109
  if (!RATELIMIT_STATS_RANGES.includes(range))
5420
6110
  throw new Error(`--range must be one of ${RATELIMIT_STATS_RANGES.join(", ")}`);
5421
- const stats = await api(`${base6(p)}/stats?range=${range}`);
6111
+ const stats = await api(`${base7(p)}/stats?range=${range}`);
5422
6112
  print(stats, () => {
5423
6113
  const { rules } = stats;
5424
6114
  console.log(`Last ${range}:`);
@@ -5449,7 +6139,7 @@ async function sync4(file, dryRun) {
5449
6139
  burst: rule.burst
5450
6140
  }))
5451
6141
  };
5452
- printSync(config.project, dryRun, await api(`${base6(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
6142
+ printSync(config.project, dryRun, await api(`${base7(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
5453
6143
  }
5454
6144
 
5455
6145
  // src/realtime.ts
@@ -5466,7 +6156,7 @@ var realtimeUsage = ` realtime enable <project> Turn on realtime and print
5466
6156
  realtime channels <project> [--prefix p] Channels with subscribers right now
5467
6157
  realtime publish <project> <channel> <event> [json] Send a message to everyone subscribed
5468
6158
  realtime disable <project> [--yes] Turn it off (closes every connection)`;
5469
- var base7 = (project) => `/projects/${encodeURIComponent(project)}/realtime`;
6159
+ var base8 = (project) => `/projects/${encodeURIComponent(project)}/realtime`;
5470
6160
  function printSettings3(s) {
5471
6161
  console.log(` socket ${s.socketUrl} (connectRealtime in @mybase-dev/sdk/realtime)`);
5472
6162
  console.log(` publish POST ${s.publishUrl} (realtime:write key)`);
@@ -5491,7 +6181,7 @@ async function realtime(sub, args) {
5491
6181
  case "enable": {
5492
6182
  const p = need(project, "project name");
5493
6183
  const { linked } = await enableService(p, "realtime", flags);
5494
- const s = await api(base7(p));
6184
+ const s = await api(base8(p));
5495
6185
  print({ enabled: "realtime", project: p, linked, ...s }, () => {
5496
6186
  console.log(`Realtime enabled for "${p}"
5497
6187
  `);
@@ -5502,7 +6192,7 @@ async function realtime(sub, args) {
5502
6192
  }
5503
6193
  case "info": {
5504
6194
  const p = need(project, "project name");
5505
- const [s, stats] = await Promise.all([api(base7(p)), api(`${base7(p)}/stats`)]);
6195
+ const [s, stats] = await Promise.all([api(base8(p)), api(`${base8(p)}/stats`)]);
5506
6196
  const { secret: _secret, ...settings } = s;
5507
6197
  print({ ...settings, stats }, () => {
5508
6198
  printSettings3({ ...s, secret: "(mybase realtime key)" });
@@ -5519,7 +6209,7 @@ async function realtime(sub, args) {
5519
6209
  const p = need(project, "project name");
5520
6210
  if (flags.has("rotate"))
5521
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");
5522
- const s = flags.has("rotate") ? await api(`${base7(p)}/secret/rotate`, { method: "POST" }) : await api(base7(p));
6212
+ const s = flags.has("rotate") ? await api(`${base8(p)}/secret/rotate`, { method: "POST" }) : await api(base8(p));
5523
6213
  print({ secret: s.secret }, () => {
5524
6214
  if (flags.has("rotate"))
5525
6215
  console.log(`Rotated. Update MYBASE_REALTIME_SECRET where your server signs tokens.
@@ -5549,7 +6239,7 @@ async function realtime(sub, args) {
5549
6239
  if (n !== undefined)
5550
6240
  body[key] = n;
5551
6241
  }
5552
- const s = Object.keys(body).length ? await api(base7(p), { method: "PATCH", body: JSON.stringify(body) }) : await api(base7(p));
6242
+ const s = Object.keys(body).length ? await api(base8(p), { method: "PATCH", body: JSON.stringify(body) }) : await api(base8(p));
5553
6243
  const { secret: _secret, ...shown } = s;
5554
6244
  print(shown, () => printSettings3({ ...s, secret: "(mybase realtime key)" }));
5555
6245
  return true;
@@ -5557,7 +6247,7 @@ async function realtime(sub, args) {
5557
6247
  case "channels": {
5558
6248
  const p = need(project, "project name");
5559
6249
  const prefix = flagString(flags, "prefix");
5560
- const { channels } = await api(`${base7(p)}/channels${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`);
6250
+ const { channels } = await api(`${base8(p)}/channels${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`);
5561
6251
  print(channels, () => {
5562
6252
  if (!channels.length)
5563
6253
  return console.log("No channels have subscribers right now.");
@@ -5576,7 +6266,7 @@ async function realtime(sub, args) {
5576
6266
  payload = data;
5577
6267
  }
5578
6268
  }
5579
- const { ids } = await api(`${base7(p)}/publish`, {
6269
+ const { ids } = await api(`${base8(p)}/publish`, {
5580
6270
  method: "POST",
5581
6271
  body: JSON.stringify({ channel: need(channel, "channel"), event: need(event, "event"), data: payload })
5582
6272
  });
@@ -5635,8 +6325,8 @@ async function target(flags, needEnv = true) {
5635
6325
  }
5636
6326
  return { project, env: env ?? "" };
5637
6327
  }
5638
- var base8 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
5639
- var envPath = (project, env, rest = "") => `${base8(project)}/envs/${encodeURIComponent(env)}${rest}`;
6328
+ var base9 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
6329
+ var envPath = (project, env, rest = "") => `${base9(project)}/envs/${encodeURIComponent(env)}${rest}`;
5640
6330
  var fetchSecrets = (project, env) => api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}`);
5641
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");
5642
6332
  async function readStdin() {
@@ -5655,7 +6345,7 @@ async function secrets(sub, args) {
5655
6345
  case "enable": {
5656
6346
  const p = need(positional[0] ?? flagString(flags, "project"), "project name");
5657
6347
  await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "PUT" });
5658
- const overview = await api(base8(p));
6348
+ const overview = await api(base9(p));
5659
6349
  print({ enabled: "secrets", project: p, ...overview }, () => {
5660
6350
  console.log(`Secrets enabled for "${p}": ${overview.envs.map((e) => e.name).join(", ")}
5661
6351
  `);
@@ -5675,7 +6365,7 @@ async function secrets(sub, args) {
5675
6365
  }
5676
6366
  case "setup": {
5677
6367
  const { project, env } = await target(flags);
5678
- const overview = await api(base8(project));
6368
+ const overview = await api(base9(project));
5679
6369
  if (!overview.envs.some((e) => e.name === env)) {
5680
6370
  throw usageError(`"${project}" has no env "${env}" (envs: ${overview.envs.map((e) => e.name).join(", ")})`);
5681
6371
  }
@@ -5685,7 +6375,7 @@ async function secrets(sub, args) {
5685
6375
  }
5686
6376
  case "envs": {
5687
6377
  const { project } = await target(flags, false);
5688
- const overview = await api(base8(project));
6378
+ const overview = await api(base9(project));
5689
6379
  print(overview.envs, () => {
5690
6380
  for (const e of overview.envs)
5691
6381
  console.log(describeEnv(e));
@@ -5696,7 +6386,7 @@ async function secrets(sub, args) {
5696
6386
  const [action, name, next] = positional;
5697
6387
  const { project } = await target(flags, false);
5698
6388
  if (action === "create") {
5699
- const env = await api(`${base8(project)}/envs`, {
6389
+ const env = await api(`${base9(project)}/envs`, {
5700
6390
  method: "POST",
5701
6391
  body: JSON.stringify({ name: need(name, "env name"), parent: flagString(flags, "from") ?? null })
5702
6392
  });
@@ -5799,7 +6489,7 @@ async function secrets(sub, args) {
5799
6489
  const { project, env } = await target(flags);
5800
6490
  const envs = env.split(",").map((e) => e.trim()).filter(Boolean);
5801
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");
5802
- const result = await api(`${base8(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
6492
+ const result = await api(`${base9(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
5803
6493
  print(result, () => {
5804
6494
  console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
5805
6495
  console.log("Restart your apps with `mybase run` to pick them up.");
@@ -5809,7 +6499,7 @@ async function secrets(sub, args) {
5809
6499
  case "unlink": {
5810
6500
  const { project, env } = await target(flags);
5811
6501
  confirm(yes, `This removes the project credentials from "${env}". They stay valid; nothing is rotated.`, env, "the env name");
5812
- await api(`${base8(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
6502
+ await api(`${base9(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
5813
6503
  print({ unlinked: env, project }, () => console.log(`Removed the credentials from "${env}"`));
5814
6504
  return true;
5815
6505
  }
@@ -5985,8 +6675,8 @@ async function sandbox(sub, args) {
5985
6675
 
5986
6676
  // src/server.ts
5987
6677
  init_dist4();
5988
- import { join as join6 } from "path";
5989
- import { styleText as styleText4 } from "util";
6678
+ import { join as join9 } from "path";
6679
+ import { styleText as styleText5 } from "util";
5990
6680
 
5991
6681
  // ../../packages/shared/src/logger.ts
5992
6682
  var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
@@ -6029,6 +6719,8 @@ var SERVICE_COLORS = {
6029
6719
  realtime: color.magenta,
6030
6720
  sandbox: color.yellow,
6031
6721
  "sandbox-worker": color.yellow,
6722
+ hosting: color.green,
6723
+ "hosting-worker": color.green,
6032
6724
  "db-public": color.cyan,
6033
6725
  dev: color.gray
6034
6726
  };
@@ -6101,9 +6793,125 @@ init_server_env();
6101
6793
  init_server_sandbox_env();
6102
6794
  await __promiseAll([
6103
6795
  init_api(),
6104
- init_server_sandbox(),
6105
- init_server_setup()
6796
+ init_server_sandbox()
6106
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();
6107
6915
  var serverUsage = ` server setup [--dir <dir>] Install mybase on this machine: checks Docker, generates keys, sets up
6108
6916
  LAN/Tailscale access, a Cloudflare Tunnel, start on boot, and the admin
6109
6917
  [--yes] [--public-host <h>] [--tunnel-token <t> --domain <d>] [--port <n>] [--no-autostart]
@@ -6113,6 +6921,9 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
6113
6921
  Set up sandbox host support; optionally enable it for a project
6114
6922
  [--deny-hosts <host,ip,\u2026>] [--image <guest>] [--worker-image <worker>]
6115
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)
6116
6927
  server status [--dir <dir>] Containers, health, and whether mybase starts on boot
6117
6928
  server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
6118
6929
  server restart [--dir <dir>] Restart every container (API, Postgres, storage, cache, search, tunnel)
@@ -6125,19 +6936,19 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
6125
6936
  [--host <db.domain> --dns-token <t> [--email <e>] [--staging]] | --off
6126
6937
  server autostart on|off Start mybase when this machine boots (and restart it after crashes)
6127
6938
  Default dir: the last one setup used, else ~/mybase`;
6128
- var dim2 = (s) => styleText4("dim", s);
6129
- var green = (s) => styleText4("green", s);
6130
- var red = (s) => styleText4("red", s);
6939
+ var dim2 = (s) => styleText5("dim", s);
6940
+ var green = (s) => styleText5("green", s);
6941
+ var red = (s) => styleText5("red", s);
6131
6942
  async function loadEnv(dir) {
6132
- const file = Bun.file(join6(dir, ".env"));
6133
- if (!await file.exists() || !await Bun.file(join6(dir, "compose.yml")).exists()) {
6943
+ const file = Bun.file(join9(dir, ".env"));
6944
+ if (!await file.exists() || !await Bun.file(join9(dir, "compose.yml")).exists()) {
6134
6945
  throw new CliError(`no mybase server in ${dir}. Run \`mybase server setup\` (or pass --dir).`, 4);
6135
6946
  }
6136
6947
  const text = await file.text();
6137
6948
  return { text, values: parseEnv(text) };
6138
6949
  }
6139
6950
  async function saveEnv(dir, text) {
6140
- await Bun.write(join6(dir, ".env"), text);
6951
+ await Bun.write(join9(dir, ".env"), text);
6141
6952
  }
6142
6953
  async function status(args) {
6143
6954
  const dir = await serverDir(args);
@@ -6195,7 +7006,7 @@ async function update(args) {
6195
7006
  const { values } = await loadEnv(dir);
6196
7007
  const interactive = !jsonMode && process.stdin.isTTY === true && !args.includes("--yes");
6197
7008
  if (!jsonMode)
6198
- intro(`${styleText4("bold", " mybase ")} ${dim2("server update")}`);
7009
+ intro(`${styleText5("bold", " mybase ")} ${dim2("server update")}`);
6199
7010
  const replaced = await writeCompose(dir, interactive, true);
6200
7011
  if (replaced)
6201
7012
  ui.success(`compose.yml updated to this CLI's (${CLI_VERSION})`);
@@ -6306,7 +7117,7 @@ async function tunnel(args) {
6306
7117
  const { text, values } = await loadEnv(dir);
6307
7118
  const interactive = !jsonMode && process.stdin.isTTY === true;
6308
7119
  if (!jsonMode)
6309
- intro(`${styleText4("bold", " mybase ")} ${dim2("server tunnel")}`);
7120
+ intro(`${styleText5("bold", " mybase ")} ${dim2("server tunnel")}`);
6310
7121
  let updates;
6311
7122
  let host = null;
6312
7123
  if (args.includes("--off")) {
@@ -6341,7 +7152,7 @@ async function databasePublic(args) {
6341
7152
  const interactive = !jsonMode && process.stdin.isTTY === true;
6342
7153
  const off = args.includes("--off");
6343
7154
  if (!jsonMode)
6344
- intro(`${styleText4("bold", " mybase ")} ${dim2("server database-public")}`);
7155
+ intro(`${styleText5("bold", " mybase ")} ${dim2("server database-public")}`);
6345
7156
  let updates;
6346
7157
  if (off) {
6347
7158
  updates = Object.fromEntries(DB_PUBLIC_KEYS.map((k) => [k, null]));
@@ -6456,6 +7267,8 @@ async function server(command, args) {
6456
7267
  await setup(args);
6457
7268
  else if (command === "sandbox")
6458
7269
  await setupSandboxServer(args);
7270
+ else if (command === "hosting")
7271
+ await setupHostingServer(args);
6459
7272
  else if (command === "status")
6460
7273
  await status(args);
6461
7274
  else if (command === "update")
@@ -6479,8 +7292,8 @@ async function server(command, args) {
6479
7292
  init_config();
6480
7293
  init_output();
6481
7294
  await init_api();
6482
- import { chmod as chmod3, rename, rm as rm2 } from "fs/promises";
6483
- import { join as join7 } from "path";
7295
+ import { chmod as chmod3, rename, rm as rm4 } from "fs/promises";
7296
+ import { join as join10 } from "path";
6484
7297
  var updateUsage = ` update [--version <v>] [--cli] [--yes]
6485
7298
  Update this CLI to the latest release (or <v>), then the server on this
6486
7299
  machine, if there is one. --cli: only the CLI`;
@@ -6535,11 +7348,11 @@ async function replaceBinary(version) {
6535
7348
  }
6536
7349
  const check = Bun.spawnSync([temp, "help"], { stdout: "ignore", stderr: "ignore" });
6537
7350
  if (check.exitCode !== 0) {
6538
- await rm2(temp, { force: true });
7351
+ await rm4(temp, { force: true });
6539
7352
  throw new CliError(`the downloaded binary doesn't run on this machine; kept ${CLI_VERSION}`);
6540
7353
  }
6541
7354
  if (process.platform === "win32") {
6542
- await rm2(`${target}.old`, { force: true });
7355
+ await rm4(`${target}.old`, { force: true });
6543
7356
  await rename(target, `${target}.old`);
6544
7357
  }
6545
7358
  await rename(temp, target);
@@ -6578,7 +7391,7 @@ async function update2(args) {
6578
7391
  }
6579
7392
  }
6580
7393
  const dir = (await loadConfig()).server;
6581
- const installed = dir && await Bun.file(join7(dir, "compose.yml")).exists();
7394
+ const installed = dir && await Bun.file(join10(dir, "compose.yml")).exists();
6582
7395
  if (installed && !args.includes("--cli")) {
6583
7396
  let go = yes || jsonMode;
6584
7397
  if (!go && process.stdin.isTTY) {
@@ -6608,7 +7421,7 @@ var usersUsage = ` users enable <project> Turn on users: an auth schema
6608
7421
  users revoke <project> <user-id> [<session-id>] Sign a user out (one session, or everywhere)
6609
7422
  users delete <project> <user-id> [--yes] Delete a user and their sessions
6610
7423
  users disable <project> [--yes] Turn it off, deleting every user account`;
6611
- var base9 = (project) => `/projects/${encodeURIComponent(project)}/users`;
7424
+ var base10 = (project) => `/projects/${encodeURIComponent(project)}/users`;
6612
7425
  function authSnippet(project, apiUrl) {
6613
7426
  return `// auth.ts: npm i better-auth pg @mybase-dev/sdk
6614
7427
  import { betterAuth } from "better-auth";
@@ -6639,7 +7452,7 @@ async function users(sub, args) {
6639
7452
  case "enable": {
6640
7453
  const p = need(project, "project name");
6641
7454
  const { linked } = await enableService(p, "users", flags);
6642
- const o = await api(base9(p));
7455
+ const o = await api(base10(p));
6643
7456
  print({ enabled: "users", project: p, linked, ...o }, () => {
6644
7457
  console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
6645
7458
  `);
@@ -6653,7 +7466,7 @@ async function users(sub, args) {
6653
7466
  }
6654
7467
  case "init": {
6655
7468
  const p = need(project, "project name");
6656
- const o = await api(base9(p));
7469
+ const o = await api(base10(p));
6657
7470
  const env = flagString(flags, "env");
6658
7471
  let stored = null;
6659
7472
  if (env) {
@@ -6676,7 +7489,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
6676
7489
  case "list": {
6677
7490
  const p = need(project, "project name");
6678
7491
  const search = flagString(flags, "search");
6679
- const list = await api(`${base9(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
7492
+ const list = await api(`${base10(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
6680
7493
  print(list, () => {
6681
7494
  if (list.users.length === 0)
6682
7495
  console.log(search ? "No users match." : "No users yet.");
@@ -6689,7 +7502,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
6689
7502
  }
6690
7503
  case "show": {
6691
7504
  const p = need(project, "project name");
6692
- const u = await api(`${base9(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
7505
+ const u = await api(`${base10(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
6693
7506
  print(u, () => {
6694
7507
  console.log(`${u.email ?? u.id}${u.name ? ` (${u.name})` : ""}${u.banned ? " BANNED" : ""}`);
6695
7508
  console.log(` id ${u.id}`);
@@ -6707,10 +7520,10 @@ Active sessions (${u.sessions.length}):`);
6707
7520
  const p = need(project, "project name");
6708
7521
  const id = encodeURIComponent(need(userId, "user id"));
6709
7522
  if (sessionId) {
6710
- await api(`${base9(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
7523
+ await api(`${base10(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
6711
7524
  print({ revoked: 1 }, () => console.log("Signed out that session."));
6712
7525
  } else {
6713
- const r = await api(`${base9(p)}/accounts/${id}/sessions`, { method: "DELETE" });
7526
+ const r = await api(`${base10(p)}/accounts/${id}/sessions`, { method: "DELETE" });
6714
7527
  print(r, () => console.log(`Signed out ${r.revoked} session(s).`));
6715
7528
  }
6716
7529
  return true;
@@ -6719,7 +7532,7 @@ Active sessions (${u.sessions.length}):`);
6719
7532
  const p = need(project, "project name");
6720
7533
  const id = need(userId, "user id");
6721
7534
  confirm(args.includes("--yes"), `This deletes user ${id} of "${p}" and signs them out.`, id, "the user id");
6722
- await api(`${base9(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
7535
+ await api(`${base10(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
6723
7536
  print({ deleted: id }, () => console.log(`Deleted user ${id}`));
6724
7537
  return true;
6725
7538
  }
@@ -6763,6 +7576,7 @@ var USAGE = {
6763
7576
  users: usersUsage,
6764
7577
  captcha: captchaUsage,
6765
7578
  realtime: realtimeUsage,
7579
+ hosting: hostingUsage,
6766
7580
  run: runUsage
6767
7581
  };
6768
7582
  var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
@@ -6901,6 +7715,12 @@ async function main(args) {
6901
7715
  if (await realtime(sub, rest))
6902
7716
  return;
6903
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));
6904
7724
  if (command && command !== "help")
6905
7725
  throw usageError(`unknown command: mybase ${args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
6906
7726