@mybase-dev/cli 1.0.5 → 1.0.7

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 +268 -1
  2. package/package.json +1 -1
package/dist/mybase.js CHANGED
@@ -139,7 +139,7 @@ function parseDotenv(text) {
139
139
  // package.json
140
140
  var package_default = {
141
141
  name: "@mybase-dev/cli",
142
- version: "1.0.5",
142
+ version: "1.0.7",
143
143
  description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
144
144
  type: "module",
145
145
  repository: {
@@ -3717,6 +3717,109 @@ ${i}
3717
3717
  import { join as join4 } from "path";
3718
3718
  import { styleText as styleText4 } from "util";
3719
3719
 
3720
+ // ../../packages/shared/src/logger.ts
3721
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
3722
+ var isLogLevel = (value) => (value in LEVELS);
3723
+ var levelAtLeast = (level, min) => LEVELS[level] >= LEVELS[min];
3724
+ var ansi = (code) => (s) => useColor() ? `\x1B[${code}m${s}\x1B[0m` : s;
3725
+ var color = {
3726
+ dim: ansi(2),
3727
+ bold: ansi(1),
3728
+ red: ansi(31),
3729
+ green: ansi(32),
3730
+ yellow: ansi(33),
3731
+ blue: ansi(34),
3732
+ magenta: ansi(35),
3733
+ cyan: ansi(36),
3734
+ gray: ansi(90)
3735
+ };
3736
+ var LEVEL_STYLE = {
3737
+ debug: color.gray,
3738
+ info: color.green,
3739
+ warn: color.yellow,
3740
+ error: color.red
3741
+ };
3742
+ var SERVICE_COLORS = {
3743
+ api: color.cyan,
3744
+ dashboard: color.magenta,
3745
+ cli: color.blue,
3746
+ cron: color.yellow,
3747
+ analytics: color.green,
3748
+ cache: color.red,
3749
+ ratelimit: color.magenta,
3750
+ queue: color.blue,
3751
+ search: color.cyan,
3752
+ mcp: color.green,
3753
+ docs: color.gray,
3754
+ audit: color.yellow,
3755
+ alerts: color.red,
3756
+ users: color.green,
3757
+ captcha: color.blue,
3758
+ "db-public": color.cyan,
3759
+ dev: color.gray
3760
+ };
3761
+ var FALLBACK_COLORS = [color.cyan, color.magenta, color.blue, color.yellow, color.green];
3762
+ function serviceColor(service) {
3763
+ const known = SERVICE_COLORS[service];
3764
+ if (known)
3765
+ return known;
3766
+ let hash = 0;
3767
+ for (const ch of service)
3768
+ hash = hash * 31 + ch.charCodeAt(0) >>> 0;
3769
+ return FALLBACK_COLORS[hash % FALLBACK_COLORS.length];
3770
+ }
3771
+ function useColor() {
3772
+ if (process.env.NO_COLOR)
3773
+ return false;
3774
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
3775
+ return true;
3776
+ return Boolean(process.stdout.isTTY);
3777
+ }
3778
+ function formatValue(value) {
3779
+ if (typeof value === "string")
3780
+ return /\s|=|"/.test(value) ? JSON.stringify(value) : value;
3781
+ if (value instanceof Error)
3782
+ return JSON.stringify(value.message);
3783
+ return JSON.stringify(value);
3784
+ }
3785
+ function formatPretty(service, level, msg, fields, at = new Date) {
3786
+ const time = color.dim(at.toISOString().slice(11, 23));
3787
+ const lvl = LEVEL_STYLE[level](level.toUpperCase().padEnd(5));
3788
+ const tag = process.env.MYBASE_LOG_PREFIXED ? "" : `${serviceColor(service)(`[${service}]`)} `;
3789
+ let errorStack = "";
3790
+ const pairs = [];
3791
+ for (const [key, value] of Object.entries(fields)) {
3792
+ if (value === undefined)
3793
+ continue;
3794
+ if (value instanceof Error && value.stack)
3795
+ errorStack = `
3796
+ ${color.dim(value.stack)}`;
3797
+ pairs.push(`${color.gray(`${key}=`)}${formatValue(value)}`);
3798
+ }
3799
+ return `${time} ${lvl} ${tag}${msg}${pairs.length ? ` ${pairs.join(" ")}` : ""}${errorStack}`;
3800
+ }
3801
+ function prettyJsonLine(line) {
3802
+ if (!line.startsWith("{"))
3803
+ return null;
3804
+ let entry;
3805
+ try {
3806
+ entry = JSON.parse(line);
3807
+ } catch {
3808
+ return null;
3809
+ }
3810
+ const { time, level, service, msg, ...fields } = entry;
3811
+ if (typeof level !== "string" || !isLogLevel(level) || typeof service !== "string" || typeof msg !== "string")
3812
+ return null;
3813
+ for (const [key, value] of Object.entries(fields)) {
3814
+ const err = value;
3815
+ if (err && typeof err === "object" && typeof err.message === "string" && typeof err.stack === "string") {
3816
+ fields[key] = Object.assign(new Error(err.message), { name: String(err.name ?? "Error"), stack: err.stack });
3817
+ }
3818
+ }
3819
+ const at = typeof time === "string" && !Number.isNaN(Date.parse(time)) ? new Date(time) : new Date;
3820
+ return { level, entry, text: formatPretty(service, level, msg, fields, at) };
3821
+ }
3822
+
3720
3823
  // src/server-docker.ts
3721
3824
  import { existsSync, readFileSync } from "fs";
3722
3825
  import { statfs } from "fs/promises";
@@ -3759,6 +3862,7 @@ var compose_default = `# Runs mybase: the control plane (API + dashboard), the s
3759
3862
  # MYBASE_S3_PUBLIC_URL=https://s3.<domain>
3760
3863
  # MYBASE_TRUST_PROXY=cloudflare
3761
3864
  # Optionally POSTGRES_BIND=127.0.0.1, GARAGE_S3_BIND=127.0.0.1, and VALKEY_BIND=127.0.0.1 to stop exposing those ports on the LAN.
3865
+ # Optionally CLOUDFLARED_REGION=us if the tunnel lands on far-away data centers (see \`mybase server status\`).
3762
3866
  # 5. docker compose up -d
3763
3867
  # Apps on serverless hosts reach Postgres at wss://mybase.<domain>/db with @neondatabase/serverless,
3764
3868
  # or, with a router port forward, at a normal postgres:// URL (public Postgres, below).
@@ -3922,6 +4026,11 @@ services:
3922
4026
  command: tunnel --no-autoupdate run
3923
4027
  environment:
3924
4028
  TUNNEL_TOKEN: \${CLOUDFLARE_TUNNEL_TOKEN:-}
4029
+ # Which Cloudflare data centers the tunnel connects to: empty = nearest by anycast, \`us\` = US only
4030
+ # (if \`mybase server status\` shows far-away colos, every request takes the detour).
4031
+ TUNNEL_REGION: \${CLOUDFLARED_REGION:-}
4032
+ # auto (QUIC, falls back to HTTP/2), quic, or http2 (if your network blocks or throttles UDP).
4033
+ TUNNEL_TRANSPORT_PROTOCOL: \${CLOUDFLARED_PROTOCOL:-auto}
3925
4034
  depends_on:
3926
4035
  - mybase
3927
4036
 
@@ -4253,6 +4362,54 @@ async function enableDockerOnBoot() {
4253
4362
  return false;
4254
4363
  }
4255
4364
  }
4365
+ function tunnelConnections(logs) {
4366
+ const conns = new Map;
4367
+ for (const line of logs.split(`
4368
+ `)) {
4369
+ if (/Starting tunnel/.test(line))
4370
+ conns.clear();
4371
+ const index = Number(/connIndex=(\d+)/.exec(line)?.[1]);
4372
+ if (Number.isNaN(index))
4373
+ continue;
4374
+ if (/Registered tunnel connection/.test(line)) {
4375
+ const location = /location=(\S+)/.exec(line)?.[1];
4376
+ if (location)
4377
+ conns.set(index, { index, location, protocol: /protocol=(\S+)/.exec(line)?.[1] ?? null });
4378
+ } else if (/Unregistered tunnel connection|Connection terminated|Retrying connection|Serve tunnel error/.test(line)) {
4379
+ conns.delete(index);
4380
+ }
4381
+ }
4382
+ return [...conns.values()].sort((a, b) => a.index - b.index);
4383
+ }
4384
+ var LOG_SERVICES = ["mybase", "postgres", "garage", "valkey", "searxng", "cloudflared"];
4385
+ var LOG_ALIASES = {
4386
+ api: "mybase",
4387
+ dashboard: "mybase",
4388
+ db: "postgres",
4389
+ database: "postgres",
4390
+ storage: "garage",
4391
+ s3: "garage",
4392
+ cache: "valkey",
4393
+ search: "searxng",
4394
+ tunnel: "cloudflared"
4395
+ };
4396
+ function logService(name) {
4397
+ const lower = name.toLowerCase();
4398
+ return LOG_SERVICES.includes(lower) ? lower : LOG_ALIASES[lower] ?? null;
4399
+ }
4400
+ function splitComposeLine(line) {
4401
+ const m = /^([a-z0-9][\w.-]*?)(?:[-_]\d+)?\s+\| ?(.*)$/i.exec(line);
4402
+ return m ? { service: m[1], text: m[2] } : null;
4403
+ }
4404
+ function guessLevel(text) {
4405
+ if (/\b(ERR|ERROR|FATAL|PANIC|CRIT|CRITICAL)\b|level=(error|fatal)/.test(text))
4406
+ return "error";
4407
+ if (/\b(WRN|WARN|WARNING)\b|level=warn/.test(text))
4408
+ return "warn";
4409
+ if (/\b(DBG|DEBUG)\b|level=debug/.test(text))
4410
+ return "debug";
4411
+ return "info";
4412
+ }
4256
4413
 
4257
4414
  // src/server-setup.ts
4258
4415
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
@@ -5044,6 +5201,10 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
5044
5201
  [--admin-user <u> --admin-password-stdin] Without a terminal: defaults plus these flags
5045
5202
  server status [--dir <dir>] Containers, health, and whether mybase starts on boot
5046
5203
  server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
5204
+ server restart [--dir <dir>] Restart every container (API, Postgres, storage, cache, search, tunnel)
5205
+ server logs [<service>\u2026] [--dir <dir>] Live logs (all services, or api|postgres|storage|cache|search|tunnel)
5206
+ [-f | --no-follow] [--tail <n>|all] [--since <10m|timestamp>] [--level warn] [--raw]
5207
+ Follows in a terminal; without one prints the tail and exits
5047
5208
  server tunnel [--dir <dir>] Add or replace the Cloudflare Tunnel [--tunnel-token <t> --domain <d>] | --off
5048
5209
  server database-public [--dir <dir>] A normal postgres:// URL from anywhere: router port forward +
5049
5210
  Let's Encrypt TLS + DNS kept on your IP (Cloudflare)
@@ -5076,6 +5237,10 @@ async function status(args) {
5076
5237
  const health = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) }).then(async (r) => r.ok ? (await r.json()).version ?? "unknown" : null).catch(() => null);
5077
5238
  const autostart = values.MYBASE_RESTART !== "no";
5078
5239
  const docker = dockerOnBoot();
5240
+ const tunnelOn = !!values.CLOUDFLARE_TUNNEL_TOKEN && containers.some((c) => c.Service === "cloudflared");
5241
+ const tunnelLogs = tunnelOn ? await compose(dir, ["logs", "--no-log-prefix", "--tail", "500", "cloudflared"]) : null;
5242
+ const tunnel = tunnelLogs?.ok ? tunnelConnections(`${tunnelLogs.stdout}
5243
+ ${tunnelLogs.stderr}`) : null;
5079
5244
  const data = {
5080
5245
  dir,
5081
5246
  url,
@@ -5084,6 +5249,7 @@ async function status(args) {
5084
5249
  autostart: autostart && docker !== "disabled",
5085
5250
  dockerOnBoot: docker,
5086
5251
  publicUrl: values.CLOUDFLARE_TUNNEL_TOKEN ? values.MYBASE_PUBLIC_URL ?? null : null,
5252
+ tunnel,
5087
5253
  containers: containers.map((c) => ({ service: c.Service, state: c.State, health: c.Health || null }))
5088
5254
  };
5089
5255
  print(data, () => {
@@ -5091,6 +5257,10 @@ async function status(args) {
5091
5257
  console.log(` ${health ? green("\u25CF") : red("\u25CF")} ${url} ${health ? `version ${health}` : "not answering"}`);
5092
5258
  if (data.publicUrl)
5093
5259
  console.log(` ${dim2("public")} ${data.publicUrl}`);
5260
+ if (tunnel) {
5261
+ const where = tunnel.map((c) => `${c.location}${c.protocol ? ` ${c.protocol}` : ""}`).join(", ");
5262
+ console.log(` ${tunnel.length > 0 ? green("\u25CF") : red("\u25CF")} ${dim2("tunnel")} ${tunnel.length > 0 ? `${tunnel.length} connection(s): ${where}` : "not connected to Cloudflare (public URL answers 530)"}`);
5263
+ }
5094
5264
  console.log(` ${dim2("starts on boot")} ${!autostart ? "no (mybase server autostart on)" : docker === "disabled" ? "no: Docker doesn't start at boot (sudo systemctl enable docker)" : "yes"}`);
5095
5265
  console.log("");
5096
5266
  if (containers.length === 0)
@@ -5121,6 +5291,99 @@ async function update(args) {
5121
5291
  return print({ dir, version, composeUpdated: replaced }, () => {});
5122
5292
  outro(`mybase ${version} is running at ${localUrl(values)}`);
5123
5293
  }
5294
+ async function restart(args) {
5295
+ const dir = await serverDir(args);
5296
+ const { values } = await loadEnv(dir);
5297
+ if (!await stackExists(dir))
5298
+ throw new CliError("no containers to restart. Start them: mybase server update", 4);
5299
+ const s = ui.spinner();
5300
+ s.start("Restarting containers");
5301
+ const res = await compose(dir, ["restart"]);
5302
+ if (!res.ok) {
5303
+ s.stop("Restart failed");
5304
+ throw new CliError(`docker compose restart failed:
5305
+ ${res.stderr}`);
5306
+ }
5307
+ s.message("Waiting for mybase to answer");
5308
+ const version = await waitHealthy(localUrl(values), 120000);
5309
+ s.stop(version ? "Restarted" : "Containers restarted");
5310
+ if (!version) {
5311
+ ui.warn("mybase didn't answer yet; check `mybase server status`.");
5312
+ process.exitCode = 1;
5313
+ }
5314
+ print({ dir, healthy: version !== null, version }, () => {
5315
+ if (version)
5316
+ console.log(`mybase ${version} is running at ${localUrl(values)}`);
5317
+ });
5318
+ }
5319
+ var LOGS_VALUE_FLAGS = new Set(["--dir", "--tail", "--since", "--level"]);
5320
+ async function logs(args) {
5321
+ const dir = await serverDir(args);
5322
+ await loadEnv(dir);
5323
+ if (!await stackExists(dir))
5324
+ throw new CliError("no containers yet. Start them: mybase server update", 4);
5325
+ const services = [];
5326
+ for (let i = 0;i < args.length; i++) {
5327
+ const arg = args[i];
5328
+ if (LOGS_VALUE_FLAGS.has(arg))
5329
+ i++;
5330
+ if (arg.startsWith("-"))
5331
+ continue;
5332
+ const service = logService(arg);
5333
+ if (!service)
5334
+ throw usageError(`unknown service "${arg}": one of ${LOG_SERVICES.join(", ")} (or api, db, storage, cache, search, tunnel)`);
5335
+ if (!services.includes(service))
5336
+ services.push(service);
5337
+ }
5338
+ const min = flag(args, "--level") ?? "debug";
5339
+ if (!isLogLevel(min))
5340
+ throw usageError("--level is one of debug, info, warn, error");
5341
+ const tail = flag(args, "--tail") ?? "100";
5342
+ if (tail !== "all" && !/^\d+$/.test(tail))
5343
+ throw usageError("--tail is a number of lines, or all");
5344
+ const since = flag(args, "--since");
5345
+ const follow = args.includes("-f") || args.includes("--follow") || !args.includes("--no-follow") && !jsonMode && process.stdout.isTTY === true;
5346
+ const raw = args.includes("--raw");
5347
+ const cmd = ["docker", "compose", "logs", "--no-color", "--tail", tail, ...since ? ["--since", since] : [], ...follow ? ["--follow"] : [], ...services];
5348
+ const proc = Bun.spawn(cmd, { cwd: dir, stdout: "pipe", stderr: "pipe", env: composeEnv() });
5349
+ const colored = !jsonMode && color.red("x") !== "x";
5350
+ const width = Math.max(...(services.length > 0 ? services : LOG_SERVICES).map((s) => s.length));
5351
+ const show = (line) => {
5352
+ if (!colored)
5353
+ line = line.replace(/\x1b\[[0-9;]*m/g, "");
5354
+ const parts = splitComposeLine(line);
5355
+ if (!parts)
5356
+ return line && console.error(line);
5357
+ const ours = prettyJsonLine(parts.text);
5358
+ const level = ours?.level ?? guessLevel(parts.text);
5359
+ if (!levelAtLeast(level, min))
5360
+ return;
5361
+ if (jsonMode)
5362
+ console.log(JSON.stringify(ours ? { container: parts.service, ...ours.entry } : { container: parts.service, level, line: parts.text }));
5363
+ else if (raw)
5364
+ console.log(line);
5365
+ else
5366
+ console.log(`${serviceColor(parts.service)(parts.service.padEnd(width))} ${color.dim("|")} ${ours?.text ?? parts.text}`);
5367
+ };
5368
+ await Promise.all([eachLine(proc.stdout, show), eachLine(proc.stderr, show)]);
5369
+ if (await proc.exited !== 0)
5370
+ process.exitCode = 1;
5371
+ }
5372
+ async function eachLine(stream, fn) {
5373
+ const decoder = new TextDecoder;
5374
+ let buffer = "";
5375
+ for await (const chunk of stream) {
5376
+ buffer += decoder.decode(chunk, { stream: true });
5377
+ for (let i = buffer.indexOf(`
5378
+ `);i !== -1; i = buffer.indexOf(`
5379
+ `)) {
5380
+ fn(buffer.slice(0, i).replace(/\r$/, ""));
5381
+ buffer = buffer.slice(i + 1);
5382
+ }
5383
+ }
5384
+ if (buffer)
5385
+ fn(buffer);
5386
+ }
5124
5387
  async function tunnel(args) {
5125
5388
  const dir = await serverDir(args);
5126
5389
  const { text, values } = await loadEnv(dir);
@@ -5277,6 +5540,10 @@ async function server(command, args) {
5277
5540
  await status(args);
5278
5541
  else if (command === "update")
5279
5542
  await update(args);
5543
+ else if (command === "restart")
5544
+ await restart(args);
5545
+ else if (command === "logs")
5546
+ await logs(args);
5280
5547
  else if (command === "tunnel")
5281
5548
  await tunnel(args);
5282
5549
  else if (command === "database-public")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mybase-dev/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
5
5
  "type": "module",
6
6
  "repository": {