@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.
- package/dist/mybase.js +268 -1
- package/package.json +1 -1
package/dist/mybase.js
CHANGED
|
@@ -139,7 +139,7 @@ function parseDotenv(text) {
|
|
|
139
139
|
// package.json
|
|
140
140
|
var package_default = {
|
|
141
141
|
name: "@mybase-dev/cli",
|
|
142
|
-
version: "1.0.
|
|
142
|
+
version: "1.0.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