@mybase-dev/cli 1.0.6 → 1.0.8

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 +371 -17
  2. package/package.json +1 -1
package/dist/mybase.js CHANGED
@@ -116,6 +116,7 @@ var KEY_RESOURCES = [
116
116
  { id: "vector", name: "Vector search", description: "Indexes, documents, and queries (read), and changing them (write)" },
117
117
  { id: "users", name: "Users", description: "Your app's users and sessions (read), revoking and deleting them, and sending sign-in events (write)" },
118
118
  { id: "captcha", name: "Captcha", description: "Captcha settings (read), and verifying tokens (write)" },
119
+ { id: "realtime", name: "Realtime", description: "Channels, members, and settings (read), and publishing messages (write)" },
119
120
  { id: "audit", name: "Audit log", description: "Who did what: sign-ins, key and project changes, secret reads (read only)" }
120
121
  ];
121
122
  function parseDotenv(text) {
@@ -139,7 +140,7 @@ function parseDotenv(text) {
139
140
  // package.json
140
141
  var package_default = {
141
142
  name: "@mybase-dev/cli",
142
- version: "1.0.6",
143
+ version: "1.0.8",
143
144
  description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
144
145
  type: "module",
145
146
  repository: {
@@ -652,6 +653,9 @@ function contextMarkdown(c) {
652
653
  }
653
654
  if (c.captcha)
654
655
  out.push("## Captcha", "", `Script: ${c.captcha.scriptUrl} (data-project="${c.project}"). Check tokens with \`createCaptchaClient\` (\`captcha:write\` key) or Better Auth's captcha plugin via \`mybaseCaptcha()\`.`, "");
656
+ if (c.realtime) {
657
+ out.push("## Realtime", "", `Browsers: \`connectRealtime({ apiUrl, project: "${c.project}", token })\` (socket ${c.realtime.socketUrl}) with tokens your server signs via \`createRealtimeToken(MYBASE_REALTIME_SECRET, \u2026)\`. Publish from the server: POST ${c.realtime.publishUrl} (\`realtime:write\` key, \`createRealtimeClient\`).`, "");
658
+ }
655
659
  if (c.analytics)
656
660
  out.push("## Analytics", "", `Public write key \`${c.analytics.writeKey}\`, script ${c.analytics.scriptUrl}`, "");
657
661
  out.push("## Env vars", "", ...c.env.map((e) => `- \`${e.name}\`: ${e.description}`), "");
@@ -2077,6 +2081,145 @@ async function sync4(file, dryRun) {
2077
2081
  printSync(config.project, dryRun, await api(`${base6(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
2078
2082
  }
2079
2083
 
2084
+ // src/realtime.ts
2085
+ var realtimeUsage = ` realtime enable <project> Turn on realtime and print the socket URL and secret
2086
+ realtime info <project> Settings, connections, and messages in the last 24 hours
2087
+ realtime key <project> [--rotate] Show (or replace) the secret that signs browser tokens and webhooks
2088
+ (rotating closes every open connection)
2089
+ realtime settings <project> [--origins a,b|none] [--public on|off] [--webhook <url>|none]
2090
+ [--events message,presence,channel] [--max-connections n]
2091
+ [--max-channels n] [--max-message-bytes n] [--max-rate n]
2092
+ realtime channels <project> [--prefix p] Channels with subscribers right now
2093
+ realtime publish <project> <channel> <event> [json] Send a message to everyone subscribed
2094
+ realtime disable <project> [--yes] Turn it off (closes every connection)`;
2095
+ var base7 = (project) => `/projects/${encodeURIComponent(project)}/realtime`;
2096
+ function printSettings3(s) {
2097
+ console.log(` socket ${s.socketUrl} (connectRealtime in @mybase-dev/sdk/realtime)`);
2098
+ console.log(` publish POST ${s.publishUrl} (realtime:write key)`);
2099
+ console.log(` secret ${s.secret} (server-side only: MYBASE_REALTIME_SECRET)`);
2100
+ console.log(` public ${s.publicChannels ? "on: public:\u2026 channels need no token" : "off: every connection needs a token"}`);
2101
+ console.log(` origins ${s.allowedOrigins.length ? s.allowedOrigins.join(", ") : "any"}`);
2102
+ console.log(` webhook ${s.webhookUrl ? `${s.webhookUrl} (${s.webhookEvents.join(", ") || "no events"})` : "none"}`);
2103
+ console.log(` limits ${s.maxConnections} connections, ${s.maxChannels} channels each, ${s.maxMessageBytes} B frames, ${s.maxRate}/s`);
2104
+ }
2105
+ var list2 = (value) => value === "none" ? [] : value.split(",").map((v) => v.trim()).filter(Boolean);
2106
+ function onOff(value, name) {
2107
+ if (value === undefined)
2108
+ return;
2109
+ if (value === "on" || value === "off")
2110
+ return value === "on";
2111
+ throw usageError(`--${name} takes on or off`);
2112
+ }
2113
+ async function realtime(sub, args) {
2114
+ const { positional, flags } = parseArgs(args);
2115
+ const [project, channel, event, data] = positional;
2116
+ switch (sub) {
2117
+ case "enable": {
2118
+ const p = need(project, "project name");
2119
+ const { linked } = await enableService(p, "realtime", flags);
2120
+ const s = await api(base7(p));
2121
+ print({ enabled: "realtime", project: p, linked, ...s }, () => {
2122
+ console.log(`Realtime enabled for "${p}"
2123
+ `);
2124
+ printSettings3(s);
2125
+ printLinked(p, linked, flags);
2126
+ });
2127
+ return true;
2128
+ }
2129
+ case "info": {
2130
+ const p = need(project, "project name");
2131
+ const [s, stats] = await Promise.all([api(base7(p)), api(`${base7(p)}/stats`)]);
2132
+ const { secret: _secret, ...settings } = s;
2133
+ print({ ...settings, stats }, () => {
2134
+ printSettings3({ ...s, secret: "(mybase realtime key)" });
2135
+ const out = stats.hours.reduce((n, h) => n + h.messagesOut, 0);
2136
+ console.log(`
2137
+ now ${stats.connections} connections (peak ${stats.peakConnections}), ${stats.channels} channels`);
2138
+ console.log(` 24 hours ${out} messages delivered`);
2139
+ if (stats.webhook.lastError)
2140
+ console.log(` webhook failing: ${stats.webhook.lastError} (${stats.webhook.queued} queued, ${stats.webhook.dropped} dropped)`);
2141
+ });
2142
+ return true;
2143
+ }
2144
+ case "key": {
2145
+ const p = need(project, "project name");
2146
+ if (flags.has("rotate"))
2147
+ confirm(args.includes("--yes"), `Every open realtime connection on "${p}" closes, and tokens signed with the old secret stop working.`, p, "the project name");
2148
+ const s = flags.has("rotate") ? await api(`${base7(p)}/secret/rotate`, { method: "POST" }) : await api(base7(p));
2149
+ print({ secret: s.secret }, () => {
2150
+ if (flags.has("rotate"))
2151
+ console.log(`Rotated. Update MYBASE_REALTIME_SECRET where your server signs tokens.
2152
+ `);
2153
+ console.log(s.secret);
2154
+ });
2155
+ return true;
2156
+ }
2157
+ case "settings": {
2158
+ const p = need(project, "project name");
2159
+ const body = {};
2160
+ const origins = flagString(flags, "origins");
2161
+ if (origins !== undefined)
2162
+ body.allowedOrigins = list2(origins);
2163
+ const pub = onOff(flagString(flags, "public"), "public");
2164
+ if (pub !== undefined)
2165
+ body.publicChannels = pub;
2166
+ const webhook = flagString(flags, "webhook");
2167
+ if (webhook !== undefined)
2168
+ body.webhookUrl = webhook === "none" ? null : webhook;
2169
+ const events = flagString(flags, "events");
2170
+ if (events !== undefined)
2171
+ body.webhookEvents = list2(events);
2172
+ const ints = { maxConnections: "max-connections", maxChannels: "max-channels", maxMessageBytes: "max-message-bytes", maxRate: "max-rate" };
2173
+ for (const [key, flag] of Object.entries(ints)) {
2174
+ const n = flagInt(flags, flag);
2175
+ if (n !== undefined)
2176
+ body[key] = n;
2177
+ }
2178
+ const s = Object.keys(body).length ? await api(base7(p), { method: "PATCH", body: JSON.stringify(body) }) : await api(base7(p));
2179
+ const { secret: _secret, ...shown } = s;
2180
+ print(shown, () => printSettings3({ ...s, secret: "(mybase realtime key)" }));
2181
+ return true;
2182
+ }
2183
+ case "channels": {
2184
+ const p = need(project, "project name");
2185
+ const prefix = flagString(flags, "prefix");
2186
+ const { channels } = await api(`${base7(p)}/channels${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`);
2187
+ print(channels, () => {
2188
+ if (!channels.length)
2189
+ return console.log("No channels have subscribers right now.");
2190
+ for (const c of channels)
2191
+ console.log(`${c.channel} ${c.subscribers} subscribers${c.members === undefined ? "" : `, ${c.members} members`}`);
2192
+ });
2193
+ return true;
2194
+ }
2195
+ case "publish": {
2196
+ const p = need(project, "project name");
2197
+ let payload = null;
2198
+ if (data !== undefined) {
2199
+ try {
2200
+ payload = JSON.parse(data);
2201
+ } catch {
2202
+ payload = data;
2203
+ }
2204
+ }
2205
+ const { ids } = await api(`${base7(p)}/publish`, {
2206
+ method: "POST",
2207
+ body: JSON.stringify({ channel: need(channel, "channel"), event: need(event, "event"), data: payload })
2208
+ });
2209
+ print({ id: ids[0] }, () => console.log(`Published ${event} to ${channel}`));
2210
+ return true;
2211
+ }
2212
+ case "disable": {
2213
+ const p = need(project, "project name");
2214
+ confirm(args.includes("--yes"), `Every open realtime connection on "${p}" closes, and pages listening for updates stop getting them.`, p, "the project name");
2215
+ await api(`/projects/${encodeURIComponent(p)}/services/realtime`, { method: "DELETE" });
2216
+ print({ disabled: "realtime", project: p }, () => console.log(`Realtime disabled for "${p}"`));
2217
+ return true;
2218
+ }
2219
+ }
2220
+ return false;
2221
+ }
2222
+
2080
2223
  // src/run.ts
2081
2224
  import { constants } from "os";
2082
2225
 
@@ -2113,8 +2256,8 @@ async function target(flags, needEnv = true) {
2113
2256
  }
2114
2257
  return { project, env: env ?? "" };
2115
2258
  }
2116
- var base7 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
2117
- var envPath = (project, env, rest = "") => `${base7(project)}/envs/${encodeURIComponent(env)}${rest}`;
2259
+ var base8 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
2260
+ var envPath = (project, env, rest = "") => `${base8(project)}/envs/${encodeURIComponent(env)}${rest}`;
2118
2261
  var fetchSecrets = (project, env) => api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}`);
2119
2262
  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");
2120
2263
  async function readStdin() {
@@ -2133,7 +2276,7 @@ async function secrets(sub, args) {
2133
2276
  case "enable": {
2134
2277
  const p = need(positional[0] ?? flagString(flags, "project"), "project name");
2135
2278
  await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "PUT" });
2136
- const overview = await api(base7(p));
2279
+ const overview = await api(base8(p));
2137
2280
  print({ enabled: "secrets", project: p, ...overview }, () => {
2138
2281
  console.log(`Secrets enabled for "${p}": ${overview.envs.map((e) => e.name).join(", ")}
2139
2282
  `);
@@ -2153,7 +2296,7 @@ async function secrets(sub, args) {
2153
2296
  }
2154
2297
  case "setup": {
2155
2298
  const { project, env } = await target(flags);
2156
- const overview = await api(base7(project));
2299
+ const overview = await api(base8(project));
2157
2300
  if (!overview.envs.some((e) => e.name === env)) {
2158
2301
  throw usageError(`"${project}" has no env "${env}" (envs: ${overview.envs.map((e) => e.name).join(", ")})`);
2159
2302
  }
@@ -2163,7 +2306,7 @@ async function secrets(sub, args) {
2163
2306
  }
2164
2307
  case "envs": {
2165
2308
  const { project } = await target(flags, false);
2166
- const overview = await api(base7(project));
2309
+ const overview = await api(base8(project));
2167
2310
  print(overview.envs, () => {
2168
2311
  for (const e of overview.envs)
2169
2312
  console.log(describeEnv(e));
@@ -2174,7 +2317,7 @@ async function secrets(sub, args) {
2174
2317
  const [action, name, next] = positional;
2175
2318
  const { project } = await target(flags, false);
2176
2319
  if (action === "create") {
2177
- const env = await api(`${base7(project)}/envs`, {
2320
+ const env = await api(`${base8(project)}/envs`, {
2178
2321
  method: "POST",
2179
2322
  body: JSON.stringify({ name: need(name, "env name"), parent: flagString(flags, "from") ?? null })
2180
2323
  });
@@ -2277,7 +2420,7 @@ async function secrets(sub, args) {
2277
2420
  const { project, env } = await target(flags);
2278
2421
  const envs = env.split(",").map((e) => e.trim()).filter(Boolean);
2279
2422
  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");
2280
- const result = await api(`${base7(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
2423
+ const result = await api(`${base8(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
2281
2424
  print(result, () => {
2282
2425
  console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
2283
2426
  console.log("Restart your apps with `mybase run` to pick them up.");
@@ -2287,7 +2430,7 @@ async function secrets(sub, args) {
2287
2430
  case "unlink": {
2288
2431
  const { project, env } = await target(flags);
2289
2432
  confirm(yes, `This removes the project credentials from "${env}". They stay valid; nothing is rotated.`, env, "the env name");
2290
- await api(`${base7(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
2433
+ await api(`${base8(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
2291
2434
  print({ unlinked: env, project }, () => console.log(`Removed the credentials from "${env}"`));
2292
2435
  return true;
2293
2436
  }
@@ -3717,6 +3860,110 @@ ${i}
3717
3860
  import { join as join4 } from "path";
3718
3861
  import { styleText as styleText4 } from "util";
3719
3862
 
3863
+ // ../../packages/shared/src/logger.ts
3864
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
3865
+ var isLogLevel = (value) => (value in LEVELS);
3866
+ var levelAtLeast = (level, min) => LEVELS[level] >= LEVELS[min];
3867
+ var ansi = (code) => (s) => useColor() ? `\x1B[${code}m${s}\x1B[0m` : s;
3868
+ var color = {
3869
+ dim: ansi(2),
3870
+ bold: ansi(1),
3871
+ red: ansi(31),
3872
+ green: ansi(32),
3873
+ yellow: ansi(33),
3874
+ blue: ansi(34),
3875
+ magenta: ansi(35),
3876
+ cyan: ansi(36),
3877
+ gray: ansi(90)
3878
+ };
3879
+ var LEVEL_STYLE = {
3880
+ debug: color.gray,
3881
+ info: color.green,
3882
+ warn: color.yellow,
3883
+ error: color.red
3884
+ };
3885
+ var SERVICE_COLORS = {
3886
+ api: color.cyan,
3887
+ dashboard: color.magenta,
3888
+ cli: color.blue,
3889
+ cron: color.yellow,
3890
+ analytics: color.green,
3891
+ cache: color.red,
3892
+ ratelimit: color.magenta,
3893
+ queue: color.blue,
3894
+ search: color.cyan,
3895
+ mcp: color.green,
3896
+ docs: color.gray,
3897
+ audit: color.yellow,
3898
+ alerts: color.red,
3899
+ users: color.green,
3900
+ captcha: color.blue,
3901
+ realtime: color.magenta,
3902
+ "db-public": color.cyan,
3903
+ dev: color.gray
3904
+ };
3905
+ var FALLBACK_COLORS = [color.cyan, color.magenta, color.blue, color.yellow, color.green];
3906
+ function serviceColor(service) {
3907
+ const known = SERVICE_COLORS[service];
3908
+ if (known)
3909
+ return known;
3910
+ let hash = 0;
3911
+ for (const ch of service)
3912
+ hash = hash * 31 + ch.charCodeAt(0) >>> 0;
3913
+ return FALLBACK_COLORS[hash % FALLBACK_COLORS.length];
3914
+ }
3915
+ function useColor() {
3916
+ if (process.env.NO_COLOR)
3917
+ return false;
3918
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
3919
+ return true;
3920
+ return Boolean(process.stdout.isTTY);
3921
+ }
3922
+ function formatValue(value) {
3923
+ if (typeof value === "string")
3924
+ return /\s|=|"/.test(value) ? JSON.stringify(value) : value;
3925
+ if (value instanceof Error)
3926
+ return JSON.stringify(value.message);
3927
+ return JSON.stringify(value);
3928
+ }
3929
+ function formatPretty(service, level, msg, fields, at = new Date) {
3930
+ const time = color.dim(at.toISOString().slice(11, 23));
3931
+ const lvl = LEVEL_STYLE[level](level.toUpperCase().padEnd(5));
3932
+ const tag = process.env.MYBASE_LOG_PREFIXED ? "" : `${serviceColor(service)(`[${service}]`)} `;
3933
+ let errorStack = "";
3934
+ const pairs = [];
3935
+ for (const [key, value] of Object.entries(fields)) {
3936
+ if (value === undefined)
3937
+ continue;
3938
+ if (value instanceof Error && value.stack)
3939
+ errorStack = `
3940
+ ${color.dim(value.stack)}`;
3941
+ pairs.push(`${color.gray(`${key}=`)}${formatValue(value)}`);
3942
+ }
3943
+ return `${time} ${lvl} ${tag}${msg}${pairs.length ? ` ${pairs.join(" ")}` : ""}${errorStack}`;
3944
+ }
3945
+ function prettyJsonLine(line) {
3946
+ if (!line.startsWith("{"))
3947
+ return null;
3948
+ let entry;
3949
+ try {
3950
+ entry = JSON.parse(line);
3951
+ } catch {
3952
+ return null;
3953
+ }
3954
+ const { time, level, service, msg, ...fields } = entry;
3955
+ if (typeof level !== "string" || !isLogLevel(level) || typeof service !== "string" || typeof msg !== "string")
3956
+ return null;
3957
+ for (const [key, value] of Object.entries(fields)) {
3958
+ const err = value;
3959
+ if (err && typeof err === "object" && typeof err.message === "string" && typeof err.stack === "string") {
3960
+ fields[key] = Object.assign(new Error(err.message), { name: String(err.name ?? "Error"), stack: err.stack });
3961
+ }
3962
+ }
3963
+ const at = typeof time === "string" && !Number.isNaN(Date.parse(time)) ? new Date(time) : new Date;
3964
+ return { level, entry, text: formatPretty(service, level, msg, fields, at) };
3965
+ }
3966
+
3720
3967
  // src/server-docker.ts
3721
3968
  import { existsSync, readFileSync } from "fs";
3722
3969
  import { statfs } from "fs/promises";
@@ -4278,6 +4525,35 @@ function tunnelConnections(logs) {
4278
4525
  }
4279
4526
  return [...conns.values()].sort((a, b) => a.index - b.index);
4280
4527
  }
4528
+ var LOG_SERVICES = ["mybase", "postgres", "garage", "valkey", "searxng", "cloudflared"];
4529
+ var LOG_ALIASES = {
4530
+ api: "mybase",
4531
+ dashboard: "mybase",
4532
+ db: "postgres",
4533
+ database: "postgres",
4534
+ storage: "garage",
4535
+ s3: "garage",
4536
+ cache: "valkey",
4537
+ search: "searxng",
4538
+ tunnel: "cloudflared"
4539
+ };
4540
+ function logService(name) {
4541
+ const lower = name.toLowerCase();
4542
+ return LOG_SERVICES.includes(lower) ? lower : LOG_ALIASES[lower] ?? null;
4543
+ }
4544
+ function splitComposeLine(line) {
4545
+ const m = /^([a-z0-9][\w.-]*?)(?:[-_]\d+)?\s+\| ?(.*)$/i.exec(line);
4546
+ return m ? { service: m[1], text: m[2] } : null;
4547
+ }
4548
+ function guessLevel(text) {
4549
+ if (/\b(ERR|ERROR|FATAL|PANIC|CRIT|CRITICAL)\b|level=(error|fatal)/.test(text))
4550
+ return "error";
4551
+ if (/\b(WRN|WARN|WARNING)\b|level=warn/.test(text))
4552
+ return "warn";
4553
+ if (/\b(DBG|DEBUG)\b|level=debug/.test(text))
4554
+ return "debug";
4555
+ return "info";
4556
+ }
4281
4557
 
4282
4558
  // src/server-setup.ts
4283
4559
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
@@ -5070,6 +5346,9 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
5070
5346
  server status [--dir <dir>] Containers, health, and whether mybase starts on boot
5071
5347
  server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
5072
5348
  server restart [--dir <dir>] Restart every container (API, Postgres, storage, cache, search, tunnel)
5349
+ server logs [<service>\u2026] [--dir <dir>] Live logs (all services, or api|postgres|storage|cache|search|tunnel)
5350
+ [-f | --no-follow] [--tail <n>|all] [--since <10m|timestamp>] [--level warn] [--raw]
5351
+ Follows in a terminal; without one prints the tail and exits
5073
5352
  server tunnel [--dir <dir>] Add or replace the Cloudflare Tunnel [--tunnel-token <t> --domain <d>] | --off
5074
5353
  server database-public [--dir <dir>] A normal postgres:// URL from anywhere: router port forward +
5075
5354
  Let's Encrypt TLS + DNS kept on your IP (Cloudflare)
@@ -5181,6 +5460,74 @@ ${res.stderr}`);
5181
5460
  console.log(`mybase ${version} is running at ${localUrl(values)}`);
5182
5461
  });
5183
5462
  }
5463
+ var LOGS_VALUE_FLAGS = new Set(["--dir", "--tail", "--since", "--level"]);
5464
+ async function logs(args) {
5465
+ const dir = await serverDir(args);
5466
+ await loadEnv(dir);
5467
+ if (!await stackExists(dir))
5468
+ throw new CliError("no containers yet. Start them: mybase server update", 4);
5469
+ const services = [];
5470
+ for (let i = 0;i < args.length; i++) {
5471
+ const arg = args[i];
5472
+ if (LOGS_VALUE_FLAGS.has(arg))
5473
+ i++;
5474
+ if (arg.startsWith("-"))
5475
+ continue;
5476
+ const service = logService(arg);
5477
+ if (!service)
5478
+ throw usageError(`unknown service "${arg}": one of ${LOG_SERVICES.join(", ")} (or api, db, storage, cache, search, tunnel)`);
5479
+ if (!services.includes(service))
5480
+ services.push(service);
5481
+ }
5482
+ const min = flag(args, "--level") ?? "debug";
5483
+ if (!isLogLevel(min))
5484
+ throw usageError("--level is one of debug, info, warn, error");
5485
+ const tail = flag(args, "--tail") ?? "100";
5486
+ if (tail !== "all" && !/^\d+$/.test(tail))
5487
+ throw usageError("--tail is a number of lines, or all");
5488
+ const since = flag(args, "--since");
5489
+ const follow = args.includes("-f") || args.includes("--follow") || !args.includes("--no-follow") && !jsonMode && process.stdout.isTTY === true;
5490
+ const raw = args.includes("--raw");
5491
+ const cmd = ["docker", "compose", "logs", "--no-color", "--tail", tail, ...since ? ["--since", since] : [], ...follow ? ["--follow"] : [], ...services];
5492
+ const proc = Bun.spawn(cmd, { cwd: dir, stdout: "pipe", stderr: "pipe", env: composeEnv() });
5493
+ const colored = !jsonMode && color.red("x") !== "x";
5494
+ const width = Math.max(...(services.length > 0 ? services : LOG_SERVICES).map((s) => s.length));
5495
+ const show = (line) => {
5496
+ if (!colored)
5497
+ line = line.replace(/\x1b\[[0-9;]*m/g, "");
5498
+ const parts = splitComposeLine(line);
5499
+ if (!parts)
5500
+ return line && console.error(line);
5501
+ const ours = prettyJsonLine(parts.text);
5502
+ const level = ours?.level ?? guessLevel(parts.text);
5503
+ if (!levelAtLeast(level, min))
5504
+ return;
5505
+ if (jsonMode)
5506
+ console.log(JSON.stringify(ours ? { container: parts.service, ...ours.entry } : { container: parts.service, level, line: parts.text }));
5507
+ else if (raw)
5508
+ console.log(line);
5509
+ else
5510
+ console.log(`${serviceColor(parts.service)(parts.service.padEnd(width))} ${color.dim("|")} ${ours?.text ?? parts.text}`);
5511
+ };
5512
+ await Promise.all([eachLine(proc.stdout, show), eachLine(proc.stderr, show)]);
5513
+ if (await proc.exited !== 0)
5514
+ process.exitCode = 1;
5515
+ }
5516
+ async function eachLine(stream, fn) {
5517
+ const decoder = new TextDecoder;
5518
+ let buffer = "";
5519
+ for await (const chunk of stream) {
5520
+ buffer += decoder.decode(chunk, { stream: true });
5521
+ for (let i = buffer.indexOf(`
5522
+ `);i !== -1; i = buffer.indexOf(`
5523
+ `)) {
5524
+ fn(buffer.slice(0, i).replace(/\r$/, ""));
5525
+ buffer = buffer.slice(i + 1);
5526
+ }
5527
+ }
5528
+ if (buffer)
5529
+ fn(buffer);
5530
+ }
5184
5531
  async function tunnel(args) {
5185
5532
  const dir = await serverDir(args);
5186
5533
  const { text, values } = await loadEnv(dir);
@@ -5339,6 +5686,8 @@ async function server(command, args) {
5339
5686
  await update(args);
5340
5687
  else if (command === "restart")
5341
5688
  await restart(args);
5689
+ else if (command === "logs")
5690
+ await logs(args);
5342
5691
  else if (command === "tunnel")
5343
5692
  await tunnel(args);
5344
5693
  else if (command === "database-public")
@@ -5477,7 +5826,7 @@ var usersUsage = ` users enable <project> Turn on users: an auth schema
5477
5826
  users revoke <project> <user-id> [<session-id>] Sign a user out (one session, or everywhere)
5478
5827
  users delete <project> <user-id> [--yes] Delete a user and their sessions
5479
5828
  users disable <project> [--yes] Turn it off, deleting every user account`;
5480
- var base8 = (project) => `/projects/${encodeURIComponent(project)}/users`;
5829
+ var base9 = (project) => `/projects/${encodeURIComponent(project)}/users`;
5481
5830
  function authSnippet(project, apiUrl) {
5482
5831
  return `// auth.ts: npm i better-auth pg @mybase-dev/sdk
5483
5832
  import { betterAuth } from "better-auth";
@@ -5508,7 +5857,7 @@ async function users(sub, args) {
5508
5857
  case "enable": {
5509
5858
  const p = need(project, "project name");
5510
5859
  const { linked } = await enableService(p, "users", flags);
5511
- const o = await api(base8(p));
5860
+ const o = await api(base9(p));
5512
5861
  print({ enabled: "users", project: p, linked, ...o }, () => {
5513
5862
  console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
5514
5863
  `);
@@ -5522,7 +5871,7 @@ async function users(sub, args) {
5522
5871
  }
5523
5872
  case "init": {
5524
5873
  const p = need(project, "project name");
5525
- const o = await api(base8(p));
5874
+ const o = await api(base9(p));
5526
5875
  const env = flagString(flags, "env");
5527
5876
  let stored = null;
5528
5877
  if (env) {
@@ -5545,7 +5894,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
5545
5894
  case "list": {
5546
5895
  const p = need(project, "project name");
5547
5896
  const search = flagString(flags, "search");
5548
- const list = await api(`${base8(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
5897
+ const list = await api(`${base9(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
5549
5898
  print(list, () => {
5550
5899
  if (list.users.length === 0)
5551
5900
  console.log(search ? "No users match." : "No users yet.");
@@ -5558,7 +5907,7 @@ Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun w
5558
5907
  }
5559
5908
  case "show": {
5560
5909
  const p = need(project, "project name");
5561
- const u = await api(`${base8(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
5910
+ const u = await api(`${base9(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
5562
5911
  print(u, () => {
5563
5912
  console.log(`${u.email ?? u.id}${u.name ? ` (${u.name})` : ""}${u.banned ? " BANNED" : ""}`);
5564
5913
  console.log(` id ${u.id}`);
@@ -5576,10 +5925,10 @@ Active sessions (${u.sessions.length}):`);
5576
5925
  const p = need(project, "project name");
5577
5926
  const id = encodeURIComponent(need(userId, "user id"));
5578
5927
  if (sessionId) {
5579
- await api(`${base8(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
5928
+ await api(`${base9(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
5580
5929
  print({ revoked: 1 }, () => console.log("Signed out that session."));
5581
5930
  } else {
5582
- const r = await api(`${base8(p)}/accounts/${id}/sessions`, { method: "DELETE" });
5931
+ const r = await api(`${base9(p)}/accounts/${id}/sessions`, { method: "DELETE" });
5583
5932
  print(r, () => console.log(`Signed out ${r.revoked} session(s).`));
5584
5933
  }
5585
5934
  return true;
@@ -5588,7 +5937,7 @@ Active sessions (${u.sessions.length}):`);
5588
5937
  const p = need(project, "project name");
5589
5938
  const id = need(userId, "user id");
5590
5939
  confirm(args.includes("--yes"), `This deletes user ${id} of "${p}" and signs them out.`, id, "the user id");
5591
- await api(`${base8(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
5940
+ await api(`${base9(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
5592
5941
  print({ deleted: id }, () => console.log(`Deleted user ${id}`));
5593
5942
  return true;
5594
5943
  }
@@ -5630,6 +5979,7 @@ var USAGE = {
5630
5979
  vector: vectorUsage,
5631
5980
  users: usersUsage,
5632
5981
  captcha: captchaUsage,
5982
+ realtime: realtimeUsage,
5633
5983
  run: runUsage
5634
5984
  };
5635
5985
  var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
@@ -5762,6 +6112,10 @@ async function main(args) {
5762
6112
  if (await captcha(sub, rest))
5763
6113
  return;
5764
6114
  }
6115
+ if (command === "realtime" && sub) {
6116
+ if (await realtime(sub, rest))
6117
+ return;
6118
+ }
5765
6119
  if (command && command !== "help")
5766
6120
  throw usageError(`unknown command: mybase ${args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
5767
6121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mybase-dev/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": {