@mybase-dev/cli 1.0.4 → 1.0.6

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 +122 -22
  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.4",
142
+ version: "1.0.6",
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: {
@@ -305,6 +305,31 @@ function parseDelay(value) {
305
305
  return Math.round(Number(m[1]) * UNITS[m[2]]);
306
306
  }
307
307
 
308
+ // src/service-enable.ts
309
+ async function linkedEnvs(project) {
310
+ const overview = await api(`/projects/${encodeURIComponent(project)}/secrets`).catch(() => null);
311
+ return overview?.envs.filter((e) => e.linked).map((e) => e.name) ?? [];
312
+ }
313
+ async function enableService(project, id, flags) {
314
+ const envs = flags.has("no-link") ? [] : await linkedEnvs(project);
315
+ let link = envs.length > 0 && flags.has("link");
316
+ if (envs.length > 0 && !link && !jsonMode && process.stdin.isTTY) {
317
+ const answer = prompt(`Add the ${id} keys to the linked secrets envs (${envs.join(", ")})? [Y/n]`);
318
+ link = !answer?.trim() || /^y/i.test(answer.trim());
319
+ }
320
+ const path = `/projects/${encodeURIComponent(project)}/services/${id}`;
321
+ const result = await api(path, link ? { method: "PUT", body: JSON.stringify({ link: true }) } : { method: "PUT" });
322
+ return { result, linked: link ? envs : [] };
323
+ }
324
+ function printLinked(project, linked, flags) {
325
+ if (linked.length > 0)
326
+ console.log(`
327
+ Keys added to the linked secrets envs: ${linked.join(", ")} (mybase run -p ${project} -e ${linked[0]} -- \u2026)`);
328
+ else if (flags.has("link"))
329
+ console.log(`
330
+ No linked secrets envs yet: mybase secrets link -p ${project} -e prod`);
331
+ }
332
+
308
333
  // src/cron.ts
309
334
  var cronUsage = ` cron enable <project> Turn on cron for a project and print its signing secret
310
335
  cron disable <project> [--yes] Turn it off, deleting every job and its history
@@ -388,12 +413,13 @@ async function cron(sub, args) {
388
413
  switch (sub) {
389
414
  case "enable": {
390
415
  const p = need(project, "project name");
391
- await api(`/projects/${encodeURIComponent(p)}/services/cron`, { method: "PUT" });
416
+ const { linked } = await enableService(p, "cron", flags);
392
417
  const secret = await api(`/projects/${encodeURIComponent(p)}/cron/secret`);
393
- print({ enabled: "cron", project: p, ...secret }, () => {
418
+ print({ enabled: "cron", project: p, linked, ...secret }, () => {
394
419
  console.log(`Cron enabled for "${p}"
395
420
  `);
396
421
  printSecret(secret);
422
+ printLinked(p, linked, flags);
397
423
  });
398
424
  return true;
399
425
  }
@@ -881,12 +907,14 @@ async function analytics(sub, args) {
881
907
  switch (sub) {
882
908
  case "enable": {
883
909
  const p = need2(project, "project name");
884
- await api(`/projects/${encodeURIComponent(p)}/services/analytics`, { method: "PUT" });
910
+ const flags = parseArgs(args).flags;
911
+ const { linked } = await enableService(p, "analytics", flags);
885
912
  const settings = await api(`${base(p)}/settings`);
886
- print({ enabled: "analytics", project: p, ...settings }, () => {
913
+ print({ enabled: "analytics", project: p, linked, ...settings }, () => {
887
914
  console.log(`Analytics enabled for "${p}"
888
915
  `);
889
916
  printSettings(p, settings);
917
+ printLinked(p, linked, flags);
890
918
  });
891
919
  return true;
892
920
  }
@@ -1351,13 +1379,14 @@ async function cache(sub, args) {
1351
1379
  switch (sub) {
1352
1380
  case "enable": {
1353
1381
  const p = need(project, "project name");
1354
- const res = await api(`/projects/${encodeURIComponent(p)}/services/cache`, { method: "PUT" });
1355
- if (res.service !== "cache")
1382
+ const { result: res, linked } = await enableService(p, "cache", flags);
1383
+ if (res?.service !== "cache")
1356
1384
  throw new Error("unexpected answer from the API");
1357
- print({ enabled: "cache", project: p, ...res.credentials }, () => {
1385
+ print({ enabled: "cache", project: p, linked, ...res.credentials }, () => {
1358
1386
  console.log(`Cache enabled for "${p}"
1359
1387
  `);
1360
1388
  printCredentials(p, res.credentials);
1389
+ printLinked(p, linked, flags);
1361
1390
  });
1362
1391
  return true;
1363
1392
  }
@@ -1419,12 +1448,13 @@ async function captcha(sub, args) {
1419
1448
  switch (sub) {
1420
1449
  case "enable": {
1421
1450
  const p = need(project, "project name");
1422
- await api(`/projects/${encodeURIComponent(p)}/services/captcha`, { method: "PUT" });
1451
+ const { linked } = await enableService(p, "captcha", flags);
1423
1452
  const s = await api(base3(p));
1424
- print({ enabled: "captcha", project: p, ...s }, () => {
1453
+ print({ enabled: "captcha", project: p, linked, ...s }, () => {
1425
1454
  console.log(`Captcha enabled for "${p}"
1426
1455
  `);
1427
1456
  printSettings2(p, s);
1457
+ printLinked(p, linked, flags);
1428
1458
  });
1429
1459
  return true;
1430
1460
  }
@@ -1570,12 +1600,13 @@ async function queue(sub, args) {
1570
1600
  switch (sub) {
1571
1601
  case "enable": {
1572
1602
  const p = need(project, "project name");
1573
- await api(`/projects/${encodeURIComponent(p)}/services/queue`, { method: "PUT" });
1603
+ const { linked } = await enableService(p, "queue", flags);
1574
1604
  const settings = await api(base4(p));
1575
- print({ enabled: "queue", project: p, ...settings }, () => {
1605
+ print({ enabled: "queue", project: p, linked, ...settings }, () => {
1576
1606
  console.log(`Queues enabled for "${p}"
1577
1607
  `);
1578
1608
  printUsage(p, settings);
1609
+ printLinked(p, linked, flags);
1579
1610
  });
1580
1611
  return true;
1581
1612
  }
@@ -1770,12 +1801,13 @@ async function vector(sub, args) {
1770
1801
  switch (sub) {
1771
1802
  case "enable": {
1772
1803
  const p = need(project, "project name");
1773
- await api(`/projects/${encodeURIComponent(p)}/services/vector`, { method: "PUT" });
1804
+ const { linked } = await enableService(p, "vector", flags);
1774
1805
  const settings = await api(base5(p));
1775
- print({ enabled: "vector", project: p, ...settings }, () => {
1806
+ print({ enabled: "vector", project: p, linked, ...settings }, () => {
1776
1807
  console.log(`Vector search enabled for "${p}"
1777
1808
  `);
1778
1809
  printUsage2(p, settings);
1810
+ printLinked(p, linked, flags);
1779
1811
  });
1780
1812
  return true;
1781
1813
  }
@@ -1956,12 +1988,13 @@ async function ratelimit(sub, args) {
1956
1988
  switch (sub) {
1957
1989
  case "enable": {
1958
1990
  const p = need(project, "project name");
1959
- await api(`/projects/${encodeURIComponent(p)}/services/ratelimit`, { method: "PUT" });
1991
+ const { linked } = await enableService(p, "ratelimit", flags);
1960
1992
  const settings = await api(base6(p));
1961
- print({ enabled: "ratelimit", project: p, ...settings }, () => {
1993
+ print({ enabled: "ratelimit", project: p, linked, ...settings }, () => {
1962
1994
  console.log(`Rate limits enabled for "${p}"
1963
1995
  `);
1964
1996
  printUsage3(p, settings);
1997
+ printLinked(p, linked, flags);
1965
1998
  });
1966
1999
  return true;
1967
2000
  }
@@ -2063,8 +2096,11 @@ var secretsUsage = ` secrets enable <project> Turn on secrets (dev, stagi
2063
2096
  secrets history [--limit 20] Revisions, newest first
2064
2097
  secrets restore <revision> [--yes] Put values back as they were after <revision>
2065
2098
  secrets link --env <env,\u2026> [--yes] Rotate the project's DB/S3/cache credentials and keep them
2066
- in these envs (DATABASE_URL, S3_*, REDIS_URL, MYBASE_URL)
2099
+ in these envs (DATABASE_URL, S3_*, REDIS_URL, MYBASE_URL), with
2100
+ every service's keys (MYBASE_API_KEY, MYBASE_CRON_SECRET, \u2026)
2067
2101
  secrets unlink [--yes] Remove the credentials from the env (they stay valid)
2102
+ \`<service> enable\` asks to add that service's keys to linked
2103
+ envs (--link / --no-link to skip the question)
2068
2104
  Project/env: -p/--project, -e/--env, else MYBASE_PROJECT/MYBASE_ENV,
2069
2105
  else \`secrets setup\` for this directory`;
2070
2106
  var expandShortFlags = (args) => args.map((a) => a === "-p" ? "--project" : a === "-e" ? "--env" : a);
@@ -2240,7 +2276,7 @@ async function secrets(sub, args) {
2240
2276
  case "link": {
2241
2277
  const { project, env } = await target(flags);
2242
2278
  const envs = env.split(",").map((e) => e.trim()).filter(Boolean);
2243
- confirm(yes, `This rotates the credentials of "${project}" (database password, storage key, and cache password if the cache is on) and stores the new ones in ${envs.join(", ")} plus any env linked before. Apps still using the old credentials will stop working.`, project, "the project name");
2279
+ 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");
2244
2280
  const result = await api(`${base7(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
2245
2281
  print(result, () => {
2246
2282
  console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
@@ -2300,9 +2336,9 @@ async function search(sub, args) {
2300
2336
  switch (sub) {
2301
2337
  case "enable": {
2302
2338
  const p = need(project, "project name");
2303
- await api(servicePath(p), { method: "PUT" });
2339
+ const { linked } = await enableService(p, "search", flags);
2304
2340
  const searchUrl = `${API_URL}/search/${p}`;
2305
- print({ enabled: "search", project: p, searchUrl, fetchUrl: `${searchUrl}/fetch` }, () => {
2341
+ print({ enabled: "search", project: p, linked, searchUrl, fetchUrl: `${searchUrl}/fetch` }, () => {
2306
2342
  console.log(`Web search enabled for "${p}"
2307
2343
  `);
2308
2344
  console.log(` search URL ${searchUrl}?q=\u2026`);
@@ -2313,6 +2349,7 @@ async function search(sub, args) {
2313
2349
  `);
2314
2350
  console.log(` const web = createSearchClient({ apiUrl: "${API_URL}", project: "${p}", apiKey: process.env.MYBASE_API_KEY! })`);
2315
2351
  console.log(` const { results } = await web.search("\u2026") // or searchTools(web) for an LLM agent`);
2352
+ printLinked(p, linked, flags);
2316
2353
  });
2317
2354
  return true;
2318
2355
  }
@@ -3722,6 +3759,7 @@ var compose_default = `# Runs mybase: the control plane (API + dashboard), the s
3722
3759
  # MYBASE_S3_PUBLIC_URL=https://s3.<domain>
3723
3760
  # MYBASE_TRUST_PROXY=cloudflare
3724
3761
  # 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.
3762
+ # Optionally CLOUDFLARED_REGION=us if the tunnel lands on far-away data centers (see \`mybase server status\`).
3725
3763
  # 5. docker compose up -d
3726
3764
  # Apps on serverless hosts reach Postgres at wss://mybase.<domain>/db with @neondatabase/serverless,
3727
3765
  # or, with a router port forward, at a normal postgres:// URL (public Postgres, below).
@@ -3885,6 +3923,11 @@ services:
3885
3923
  command: tunnel --no-autoupdate run
3886
3924
  environment:
3887
3925
  TUNNEL_TOKEN: \${CLOUDFLARE_TUNNEL_TOKEN:-}
3926
+ # Which Cloudflare data centers the tunnel connects to: empty = nearest by anycast, \`us\` = US only
3927
+ # (if \`mybase server status\` shows far-away colos, every request takes the detour).
3928
+ TUNNEL_REGION: \${CLOUDFLARED_REGION:-}
3929
+ # auto (QUIC, falls back to HTTP/2), quic, or http2 (if your network blocks or throttles UDP).
3930
+ TUNNEL_TRANSPORT_PROTOCOL: \${CLOUDFLARED_PROTOCOL:-auto}
3888
3931
  depends_on:
3889
3932
  - mybase
3890
3933
 
@@ -4216,6 +4259,25 @@ async function enableDockerOnBoot() {
4216
4259
  return false;
4217
4260
  }
4218
4261
  }
4262
+ function tunnelConnections(logs) {
4263
+ const conns = new Map;
4264
+ for (const line of logs.split(`
4265
+ `)) {
4266
+ if (/Starting tunnel/.test(line))
4267
+ conns.clear();
4268
+ const index = Number(/connIndex=(\d+)/.exec(line)?.[1]);
4269
+ if (Number.isNaN(index))
4270
+ continue;
4271
+ if (/Registered tunnel connection/.test(line)) {
4272
+ const location = /location=(\S+)/.exec(line)?.[1];
4273
+ if (location)
4274
+ conns.set(index, { index, location, protocol: /protocol=(\S+)/.exec(line)?.[1] ?? null });
4275
+ } else if (/Unregistered tunnel connection|Connection terminated|Retrying connection|Serve tunnel error/.test(line)) {
4276
+ conns.delete(index);
4277
+ }
4278
+ }
4279
+ return [...conns.values()].sort((a, b) => a.index - b.index);
4280
+ }
4219
4281
 
4220
4282
  // src/server-setup.ts
4221
4283
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
@@ -5007,6 +5069,7 @@ var serverUsage = ` server setup [--dir <dir>] Install mybase on this machi
5007
5069
  [--admin-user <u> --admin-password-stdin] Without a terminal: defaults plus these flags
5008
5070
  server status [--dir <dir>] Containers, health, and whether mybase starts on boot
5009
5071
  server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
5072
+ server restart [--dir <dir>] Restart every container (API, Postgres, storage, cache, search, tunnel)
5010
5073
  server tunnel [--dir <dir>] Add or replace the Cloudflare Tunnel [--tunnel-token <t> --domain <d>] | --off
5011
5074
  server database-public [--dir <dir>] A normal postgres:// URL from anywhere: router port forward +
5012
5075
  Let's Encrypt TLS + DNS kept on your IP (Cloudflare)
@@ -5039,6 +5102,10 @@ async function status(args) {
5039
5102
  const health = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) }).then(async (r) => r.ok ? (await r.json()).version ?? "unknown" : null).catch(() => null);
5040
5103
  const autostart = values.MYBASE_RESTART !== "no";
5041
5104
  const docker = dockerOnBoot();
5105
+ const tunnelOn = !!values.CLOUDFLARE_TUNNEL_TOKEN && containers.some((c) => c.Service === "cloudflared");
5106
+ const tunnelLogs = tunnelOn ? await compose(dir, ["logs", "--no-log-prefix", "--tail", "500", "cloudflared"]) : null;
5107
+ const tunnel = tunnelLogs?.ok ? tunnelConnections(`${tunnelLogs.stdout}
5108
+ ${tunnelLogs.stderr}`) : null;
5042
5109
  const data = {
5043
5110
  dir,
5044
5111
  url,
@@ -5047,6 +5114,7 @@ async function status(args) {
5047
5114
  autostart: autostart && docker !== "disabled",
5048
5115
  dockerOnBoot: docker,
5049
5116
  publicUrl: values.CLOUDFLARE_TUNNEL_TOKEN ? values.MYBASE_PUBLIC_URL ?? null : null,
5117
+ tunnel,
5050
5118
  containers: containers.map((c) => ({ service: c.Service, state: c.State, health: c.Health || null }))
5051
5119
  };
5052
5120
  print(data, () => {
@@ -5054,6 +5122,10 @@ async function status(args) {
5054
5122
  console.log(` ${health ? green("\u25CF") : red("\u25CF")} ${url} ${health ? `version ${health}` : "not answering"}`);
5055
5123
  if (data.publicUrl)
5056
5124
  console.log(` ${dim2("public")} ${data.publicUrl}`);
5125
+ if (tunnel) {
5126
+ const where = tunnel.map((c) => `${c.location}${c.protocol ? ` ${c.protocol}` : ""}`).join(", ");
5127
+ 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)"}`);
5128
+ }
5057
5129
  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"}`);
5058
5130
  console.log("");
5059
5131
  if (containers.length === 0)
@@ -5084,6 +5156,31 @@ async function update(args) {
5084
5156
  return print({ dir, version, composeUpdated: replaced }, () => {});
5085
5157
  outro(`mybase ${version} is running at ${localUrl(values)}`);
5086
5158
  }
5159
+ async function restart(args) {
5160
+ const dir = await serverDir(args);
5161
+ const { values } = await loadEnv(dir);
5162
+ if (!await stackExists(dir))
5163
+ throw new CliError("no containers to restart. Start them: mybase server update", 4);
5164
+ const s = ui.spinner();
5165
+ s.start("Restarting containers");
5166
+ const res = await compose(dir, ["restart"]);
5167
+ if (!res.ok) {
5168
+ s.stop("Restart failed");
5169
+ throw new CliError(`docker compose restart failed:
5170
+ ${res.stderr}`);
5171
+ }
5172
+ s.message("Waiting for mybase to answer");
5173
+ const version = await waitHealthy(localUrl(values), 120000);
5174
+ s.stop(version ? "Restarted" : "Containers restarted");
5175
+ if (!version) {
5176
+ ui.warn("mybase didn't answer yet; check `mybase server status`.");
5177
+ process.exitCode = 1;
5178
+ }
5179
+ print({ dir, healthy: version !== null, version }, () => {
5180
+ if (version)
5181
+ console.log(`mybase ${version} is running at ${localUrl(values)}`);
5182
+ });
5183
+ }
5087
5184
  async function tunnel(args) {
5088
5185
  const dir = await serverDir(args);
5089
5186
  const { text, values } = await loadEnv(dir);
@@ -5240,6 +5337,8 @@ async function server(command, args) {
5240
5337
  await status(args);
5241
5338
  else if (command === "update")
5242
5339
  await update(args);
5340
+ else if (command === "restart")
5341
+ await restart(args);
5243
5342
  else if (command === "tunnel")
5244
5343
  await tunnel(args);
5245
5344
  else if (command === "database-public")
@@ -5408,15 +5507,16 @@ async function users(sub, args) {
5408
5507
  switch (sub) {
5409
5508
  case "enable": {
5410
5509
  const p = need(project, "project name");
5411
- await api(`/projects/${encodeURIComponent(p)}/services/users`, { method: "PUT" });
5510
+ const { linked } = await enableService(p, "users", flags);
5412
5511
  const o = await api(base8(p));
5413
- print({ enabled: "users", project: p, ...o }, () => {
5512
+ print({ enabled: "users", project: p, linked, ...o }, () => {
5414
5513
  console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
5415
5514
  `);
5416
5515
  console.log("Next:");
5417
5516
  console.log(` 1. An API key for the app: mybase token create ${p}-auth --project ${p} --perm users:write,cache:write`);
5418
5517
  console.log(` 2. The cache, for sessions: mybase cache enable ${p}`);
5419
5518
  console.log(` 3. An auth.ts to start from: mybase users init ${p}`);
5519
+ printLinked(p, linked, flags);
5420
5520
  });
5421
5521
  return true;
5422
5522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mybase-dev/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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": {