@mybase-dev/cli 1.0.0 → 1.0.1
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 +3864 -113
- package/package.json +1 -1
package/dist/mybase.js
CHANGED
|
@@ -1,15 +1,107 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
13
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
|
+
const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
|
+
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
24
|
+
for (let key of __getOwnPropNames(mod))
|
|
25
|
+
if (!__hasOwnProp.call(to, key))
|
|
26
|
+
__defProp(to, key, {
|
|
27
|
+
get: __accessProp.bind(mod, key),
|
|
28
|
+
enumerable: true
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (canCache)
|
|
32
|
+
cache.set(mod, to);
|
|
33
|
+
return to;
|
|
34
|
+
};
|
|
35
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
36
|
+
|
|
37
|
+
// ../../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
|
|
38
|
+
var require_src = __commonJS(function(exports, module) {
|
|
39
|
+
var ESC = "\x1B";
|
|
40
|
+
var CSI = `${ESC}[`;
|
|
41
|
+
var beep = "\x07";
|
|
42
|
+
var cursor = {
|
|
43
|
+
to(x, y) {
|
|
44
|
+
if (!y)
|
|
45
|
+
return `${CSI}${x + 1}G`;
|
|
46
|
+
return `${CSI}${y + 1};${x + 1}H`;
|
|
47
|
+
},
|
|
48
|
+
move(x, y) {
|
|
49
|
+
let ret = "";
|
|
50
|
+
if (x < 0)
|
|
51
|
+
ret += `${CSI}${-x}D`;
|
|
52
|
+
else if (x > 0)
|
|
53
|
+
ret += `${CSI}${x}C`;
|
|
54
|
+
if (y < 0)
|
|
55
|
+
ret += `${CSI}${-y}A`;
|
|
56
|
+
else if (y > 0)
|
|
57
|
+
ret += `${CSI}${y}B`;
|
|
58
|
+
return ret;
|
|
59
|
+
},
|
|
60
|
+
up: (count = 1) => `${CSI}${count}A`,
|
|
61
|
+
down: (count = 1) => `${CSI}${count}B`,
|
|
62
|
+
forward: (count = 1) => `${CSI}${count}C`,
|
|
63
|
+
backward: (count = 1) => `${CSI}${count}D`,
|
|
64
|
+
nextLine: (count = 1) => `${CSI}E`.repeat(count),
|
|
65
|
+
prevLine: (count = 1) => `${CSI}F`.repeat(count),
|
|
66
|
+
left: `${CSI}G`,
|
|
67
|
+
hide: `${CSI}?25l`,
|
|
68
|
+
show: `${CSI}?25h`,
|
|
69
|
+
save: `${ESC}7`,
|
|
70
|
+
restore: `${ESC}8`
|
|
71
|
+
};
|
|
72
|
+
var scroll = {
|
|
73
|
+
up: (count = 1) => `${CSI}S`.repeat(count),
|
|
74
|
+
down: (count = 1) => `${CSI}T`.repeat(count)
|
|
75
|
+
};
|
|
76
|
+
var erase = {
|
|
77
|
+
screen: `${CSI}2J`,
|
|
78
|
+
up: (count = 1) => `${CSI}1J`.repeat(count),
|
|
79
|
+
down: (count = 1) => `${CSI}J`.repeat(count),
|
|
80
|
+
line: `${CSI}2K`,
|
|
81
|
+
lineEnd: `${CSI}K`,
|
|
82
|
+
lineStart: `${CSI}1K`,
|
|
83
|
+
lines(count) {
|
|
84
|
+
let clear = "";
|
|
85
|
+
for (let i = 0;i < count; i++)
|
|
86
|
+
clear += this.line + (i < count - 1 ? cursor.up() : "");
|
|
87
|
+
if (count)
|
|
88
|
+
clear += cursor.left;
|
|
89
|
+
return clear;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
module.exports = { cursor, scroll, erase, beep };
|
|
93
|
+
});
|
|
3
94
|
|
|
4
95
|
// src/agent.ts
|
|
5
96
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
6
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
97
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
|
|
7
98
|
|
|
8
99
|
// ../../packages/shared/src/index.ts
|
|
9
100
|
var DEFAULT_API_URL = "http://localhost:4000";
|
|
10
101
|
var ANALYTICS_RANGES = ["24h", "7d", "30d", "90d"];
|
|
11
102
|
var RATELIMIT_STATS_RANGES = ["24h", "7d"];
|
|
12
103
|
var QUEUE_STATS_RANGES = ["24h", "7d"];
|
|
104
|
+
var ADMIN_PASSWORD_MIN = 10;
|
|
13
105
|
var KEY_RESOURCES = [
|
|
14
106
|
{ id: "projects", name: "Projects", description: "List projects; create, delete, and turn services on or off" },
|
|
15
107
|
{ id: "database", name: "Database", description: "Database details, tables, and rows (the SQL editor needs admin)" },
|
|
@@ -18,12 +110,36 @@ var KEY_RESOURCES = [
|
|
|
18
110
|
{ id: "analytics", name: "Analytics", description: "Reports, events, and settings" },
|
|
19
111
|
{ id: "cache", name: "Cache", description: "Keys, and HTTP commands (write)" },
|
|
20
112
|
{ id: "ratelimit", name: "Rate limits", description: "Rules and stats, and limit checks (write)" },
|
|
21
|
-
{ id: "queue", name: "Queues", description: "Queues and messages (receiving is a write)" }
|
|
113
|
+
{ id: "queue", name: "Queues", description: "Queues and messages (receiving is a write)" },
|
|
114
|
+
{ id: "search", name: "Web search", description: "Web searches and page reads (read)" },
|
|
115
|
+
{ id: "secrets", name: "Secrets", description: "Secret values per environment (read), and changing them (write)" },
|
|
116
|
+
{ id: "vector", name: "Vector search", description: "Indexes, documents, and queries (read), and changing them (write)" },
|
|
117
|
+
{ id: "users", name: "Users", description: "Your app's users and sessions (read), revoking and deleting them, and sending sign-in events (write)" },
|
|
118
|
+
{ id: "captcha", name: "Captcha", description: "Captcha settings (read), and verifying tokens (write)" },
|
|
119
|
+
{ id: "audit", name: "Audit log", description: "Who did what: sign-ins, key and project changes, secret reads (read only)" }
|
|
22
120
|
];
|
|
121
|
+
function parseDotenv(text) {
|
|
122
|
+
const out = {};
|
|
123
|
+
const re = /^[ \t]*(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*=[ \t]*('[^']*'|"(?:\\.|[^"\\])*"|[^\n]*?)[ \t]*$/gm;
|
|
124
|
+
for (const m of text.replace(/\r\n?/g, `
|
|
125
|
+
`).matchAll(re)) {
|
|
126
|
+
const key = m[1];
|
|
127
|
+
let value = m[2];
|
|
128
|
+
if (value.startsWith("'"))
|
|
129
|
+
value = value.slice(1, -1);
|
|
130
|
+
else if (value.startsWith('"'))
|
|
131
|
+
value = value.slice(1, -1).replace(/\\([nrt"\\])/g, (_, c) => ({ n: `
|
|
132
|
+
`, r: "\r", t: "\t" })[c] ?? c);
|
|
133
|
+
else
|
|
134
|
+
value = value.replace(/\s+#.*$/, "").trim();
|
|
135
|
+
out[key] = value;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
23
139
|
// package.json
|
|
24
140
|
var package_default = {
|
|
25
141
|
name: "@mybase-dev/cli",
|
|
26
|
-
version: "1.0.
|
|
142
|
+
version: "1.0.1",
|
|
27
143
|
description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
|
|
28
144
|
type: "module",
|
|
29
145
|
repository: {
|
|
@@ -50,9 +166,9 @@ var package_default = {
|
|
|
50
166
|
};
|
|
51
167
|
|
|
52
168
|
// src/config.ts
|
|
53
|
-
import { chmod, mkdir
|
|
169
|
+
import { chmod, mkdir } from "fs/promises";
|
|
54
170
|
import { homedir } from "os";
|
|
55
|
-
import { dirname, join } from "path";
|
|
171
|
+
import { dirname, join, resolve, sep } from "path";
|
|
56
172
|
var configPath = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "mybase", "config.json");
|
|
57
173
|
async function loadConfig() {
|
|
58
174
|
const file = Bun.file(configPath);
|
|
@@ -64,18 +180,56 @@ async function loadConfig() {
|
|
|
64
180
|
throw new Error(`${configPath} isn't valid JSON. Fix it, or delete it and run \`mybase login\` again.`);
|
|
65
181
|
}
|
|
66
182
|
}
|
|
67
|
-
async function
|
|
183
|
+
async function writeConfig(config) {
|
|
68
184
|
await mkdir(dirname(configPath), { recursive: true, mode: 448 });
|
|
69
185
|
await Bun.write(configPath, `${JSON.stringify(config, null, 2)}
|
|
70
186
|
`);
|
|
71
187
|
await chmod(configPath, 384);
|
|
72
188
|
}
|
|
189
|
+
async function saveConfig(config) {
|
|
190
|
+
const { url: _url, token: _token, ...rest } = await loadConfig();
|
|
191
|
+
await writeConfig({ ...config, ...rest });
|
|
192
|
+
}
|
|
73
193
|
async function clearConfig() {
|
|
74
|
-
|
|
194
|
+
const { url: _url, token: _token, ...rest } = await loadConfig();
|
|
195
|
+
await writeConfig(rest);
|
|
196
|
+
}
|
|
197
|
+
async function saveServerDir(dir) {
|
|
198
|
+
await writeConfig({ ...await loadConfig(), server: resolve(dir) });
|
|
199
|
+
}
|
|
200
|
+
async function dirSetup(dir = process.cwd()) {
|
|
201
|
+
const { dirs = {} } = await loadConfig();
|
|
202
|
+
for (let d = resolve(dir);; d = dirname(d)) {
|
|
203
|
+
const setup = dirs[d];
|
|
204
|
+
if (setup)
|
|
205
|
+
return { ...setup, dir: d };
|
|
206
|
+
if (d === dirname(d))
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function saveDirSetup(dir, setup) {
|
|
211
|
+
const config = await loadConfig();
|
|
212
|
+
await writeConfig({ ...config, dirs: { ...config.dirs, [resolve(dir).replace(new RegExp(`\\${sep}$`), "") || sep]: setup } });
|
|
75
213
|
}
|
|
76
214
|
|
|
77
215
|
// src/output.ts
|
|
78
|
-
var
|
|
216
|
+
var RUN_VALUE_FLAGS = new Set(["-p", "-e", "--project", "--env"]);
|
|
217
|
+
function splitRunArgs(args) {
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i < args.length && args[i].startsWith("-")) {
|
|
220
|
+
if (args[i] === "--")
|
|
221
|
+
return { options: args.slice(0, i), command: args.slice(i + 1) };
|
|
222
|
+
i += RUN_VALUE_FLAGS.has(args[i]) ? 2 : 1;
|
|
223
|
+
}
|
|
224
|
+
return { options: args.slice(0, i), command: args.slice(i) };
|
|
225
|
+
}
|
|
226
|
+
function ownArgs(args) {
|
|
227
|
+
if (args[0] === "run")
|
|
228
|
+
return ["run", ...splitRunArgs(args.slice(1)).options];
|
|
229
|
+
const dash = args.indexOf("--");
|
|
230
|
+
return dash === -1 ? args : args.slice(0, dash);
|
|
231
|
+
}
|
|
232
|
+
var jsonMode = ownArgs(Bun.argv.slice(2)).includes("--json");
|
|
79
233
|
|
|
80
234
|
class CliError extends Error {
|
|
81
235
|
exitCode;
|
|
@@ -134,7 +288,7 @@ async function api(path, init) {
|
|
|
134
288
|
}
|
|
135
289
|
|
|
136
290
|
// src/cron.ts
|
|
137
|
-
import { resolve } from "path";
|
|
291
|
+
import { resolve as resolve2 } from "path";
|
|
138
292
|
|
|
139
293
|
// ../../packages/sdk/src/cron.ts
|
|
140
294
|
var encoder = new TextEncoder;
|
|
@@ -371,7 +525,7 @@ async function fetchTask(project, body) {
|
|
|
371
525
|
return { task, created };
|
|
372
526
|
}
|
|
373
527
|
async function readConfigFile(file) {
|
|
374
|
-
const path =
|
|
528
|
+
const path = resolve2(file);
|
|
375
529
|
if (!await Bun.file(path).exists())
|
|
376
530
|
throw new Error(`no config at ${path}`);
|
|
377
531
|
const config = (await import(path)).default;
|
|
@@ -465,6 +619,13 @@ function contextMarkdown(c) {
|
|
|
465
619
|
}
|
|
466
620
|
if (c.cache)
|
|
467
621
|
out.push("## Cache", "", `TCP ${c.cache.url} (password shown once at enable), HTTP ${c.cache.httpUrl}. Every key starts with \`${c.cache.keyPrefix}\`.`, "");
|
|
622
|
+
if (c.search)
|
|
623
|
+
out.push("## Web search", "", `GET ${c.search.searchUrl}?q=\u2026 and ${c.search.fetchUrl}?url=\u2026 with a \`search:read\` key.`, "");
|
|
624
|
+
if (c.users) {
|
|
625
|
+
out.push("## Users (Better Auth)", "", c.users.ready ? `${c.users.users} user(s). Better Auth's tables are in schema \`${c.users.schema}\`; the app connects with \`authDatabaseUrl(DATABASE_URL)\` from @mybase-dev/sdk/better-auth.` : `On, but Better Auth's tables aren't there yet: run \`npx @better-auth/cli migrate\` in the app (schema \`${c.users.schema}\`). Start from \`mybase users init ${c.project}\`.`, "");
|
|
626
|
+
}
|
|
627
|
+
if (c.captcha)
|
|
628
|
+
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()\`.`, "");
|
|
468
629
|
if (c.analytics)
|
|
469
630
|
out.push("## Analytics", "", `Public write key \`${c.analytics.writeKey}\`, script ${c.analytics.scriptUrl}`, "");
|
|
470
631
|
out.push("## Env vars", "", ...c.env.map((e) => `- \`${e.name}\`: ${e.description}`), "");
|
|
@@ -533,7 +694,9 @@ var SDK_ENTRY = {
|
|
|
533
694
|
analytics: "`@mybase-dev/sdk/analytics` (page views, events, errors)",
|
|
534
695
|
cache: "`@mybase-dev/sdk/cache` (HTTP cache client)",
|
|
535
696
|
ratelimit: "`@mybase-dev/sdk/ratelimit` (check limits)",
|
|
536
|
-
queue: "`@mybase-dev/sdk/queue` (send, receive, consume)"
|
|
697
|
+
queue: "`@mybase-dev/sdk/queue` (send, receive, consume)",
|
|
698
|
+
search: "`@mybase-dev/sdk/search` (web search + page reads, `searchTools` for LLM agents)",
|
|
699
|
+
vector: "`@mybase-dev/sdk/vector` (vector, full-text, and hybrid search over documents; `retrievalTool` for RAG)"
|
|
537
700
|
};
|
|
538
701
|
function agentsSection(c) {
|
|
539
702
|
const sdk = c.services.filter((s) => SDK_ENTRY[s]).map((s) => ` - ${SDK_ENTRY[s]}`);
|
|
@@ -548,13 +711,13 @@ This app's backend is the mybase project \`${c.project}\` at ${c.apiUrl} (self-h
|
|
|
548
711
|
${sdk.length ? `- **SDK** (\`npm i @mybase-dev/sdk\`):
|
|
549
712
|
${sdk.join(`
|
|
550
713
|
`)}
|
|
551
|
-
` : ""}- **Config in code:** cron jobs, rate limit rules, and
|
|
714
|
+
` : ""}- **Config in code:** cron jobs, rate limit rules, queues, and vector indexes the app depends on go in \`mybase.config.ts\` (\`defineConfig\` from \`@mybase-dev/sdk/config\`) and are applied with \`mybase cron|ratelimit|queue|vector sync\` (\`--dry-run\` first).
|
|
552
715
|
- \`MYBASE_API_KEY\` is server-side only. Browser code may use only the analytics write key.`;
|
|
553
716
|
}
|
|
554
717
|
function skill(c) {
|
|
555
718
|
return `---
|
|
556
719
|
name: mybase
|
|
557
|
-
description: Use for anything touching this app's backend on mybase (self-hosted Postgres, S3 storage, cron, queues, cache, rate limits, analytics), project "${c.project}". Covers the mybase CLI, MCP tools, @mybase-dev/sdk, and mybase.config.ts.
|
|
720
|
+
description: Use for anything touching this app's backend on mybase (self-hosted Postgres, S3 storage, cron, queues, cache, rate limits, analytics, vector search), project "${c.project}". Covers the mybase CLI, MCP tools, @mybase-dev/sdk, and mybase.config.ts.
|
|
558
721
|
---
|
|
559
722
|
|
|
560
723
|
# mybase
|
|
@@ -565,7 +728,7 @@ Project \`${c.project}\` on ${c.apiUrl}. Services: database, storage${c.services
|
|
|
565
728
|
|
|
566
729
|
1. Ground yourself: \`mybase context ${c.project} --json\` or MCP \`get_project_context\` (project "${c.project}").
|
|
567
730
|
2. Look up the exact API before writing code: \`mybase docs --search "<words>"\`, then \`mybase docs <slug>\`. Index: ${c.docsUrl}
|
|
568
|
-
3. Declare cron jobs, queues,
|
|
731
|
+
3. Declare cron jobs, queues, rate limit rules, and vector indexes in \`mybase.config.ts\`; preview with \`mybase <service> sync --dry-run\`, then sync.
|
|
569
732
|
4. Verify against the live project: \`mybase queue peek\`, \`mybase cron runs\`, MCP \`list_cron_runs\`, \`peek_messages\`, \`analytics_stats\`.
|
|
570
733
|
|
|
571
734
|
## Rules
|
|
@@ -616,7 +779,7 @@ async function agent(sub, args) {
|
|
|
616
779
|
if (sub !== "init")
|
|
617
780
|
return false;
|
|
618
781
|
const { flags } = parseArgs(args);
|
|
619
|
-
const dir =
|
|
782
|
+
const dir = resolve3(flagString(flags, "dir") ?? ".");
|
|
620
783
|
const c = await getContext(await chooseProject(flagString(flags, "project"), dir));
|
|
621
784
|
const written = [];
|
|
622
785
|
const notes = [];
|
|
@@ -770,29 +933,194 @@ async function analytics(sub, args) {
|
|
|
770
933
|
return false;
|
|
771
934
|
}
|
|
772
935
|
|
|
936
|
+
// src/alerts.ts
|
|
937
|
+
var alertsUsage = ` alerts Alert channels and rules (admin key)
|
|
938
|
+
alerts add webhook|ntfy <url> [--name n] [--token t] Send alerts to a webhook (signed) or an ntfy topic URL
|
|
939
|
+
alerts test <channel-id> Send a test alert and wait for the result
|
|
940
|
+
alerts remove <channel-id> [--yes] Stop sending alerts there
|
|
941
|
+
alerts rotate <channel-id> New webhook signing secret
|
|
942
|
+
alerts rule <rule> on|off Turn one rule on or off (rule ids: see \`mybase alerts\`)`;
|
|
943
|
+
function printChannel(c) {
|
|
944
|
+
const state = c.enabled ? "" : " (paused)";
|
|
945
|
+
console.log(` ${c.id} ${c.kind.padEnd(7)} ${c.name}${state}`);
|
|
946
|
+
console.log(` ${c.url}${c.lastError ? ` last error: ${c.lastError}` : c.lastSentAt ? ` last sent ${c.lastSentAt}` : ""}`);
|
|
947
|
+
if (c.secret)
|
|
948
|
+
console.log(` signing secret ${c.secret} (verify with verifyCronSignature from @mybase-dev/sdk/cron)`);
|
|
949
|
+
}
|
|
950
|
+
async function alerts(args) {
|
|
951
|
+
const { positional, flags } = parseArgs(args);
|
|
952
|
+
const [sub, a, b] = positional;
|
|
953
|
+
switch (sub) {
|
|
954
|
+
case undefined: {
|
|
955
|
+
const o = await api("/alerts");
|
|
956
|
+
print(o, () => {
|
|
957
|
+
console.log("Channels:");
|
|
958
|
+
if (o.channels.length === 0)
|
|
959
|
+
console.log(" None. Add one with: mybase alerts add ntfy https://ntfy.sh/<topic>");
|
|
960
|
+
for (const c of o.channels)
|
|
961
|
+
printChannel(c);
|
|
962
|
+
console.log(`
|
|
963
|
+
Rules:`);
|
|
964
|
+
for (const r of o.rules)
|
|
965
|
+
console.log(` ${r.enabled ? "on " : "off"} ${r.id.padEnd(24)} ${r.description}`);
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
case "add": {
|
|
970
|
+
if (a !== "webhook" && a !== "ntfy" || !b)
|
|
971
|
+
throw usageError(`usage: mybase alerts add webhook|ntfy <url>
|
|
972
|
+
|
|
973
|
+
${alertsUsage}`);
|
|
974
|
+
const c = await api("/alerts/channels", {
|
|
975
|
+
method: "POST",
|
|
976
|
+
body: JSON.stringify({ kind: a, url: b, name: flagString(flags, "name"), token: flagString(flags, "token") })
|
|
977
|
+
});
|
|
978
|
+
print(c, () => {
|
|
979
|
+
console.log(`Added ${c.kind} channel "${c.name}"`);
|
|
980
|
+
printChannel(c);
|
|
981
|
+
console.log(`
|
|
982
|
+
Try it: mybase alerts test ${c.id}`);
|
|
983
|
+
});
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
case "test": {
|
|
987
|
+
if (!a)
|
|
988
|
+
throw usageError("missing channel id");
|
|
989
|
+
const r = await api(`/alerts/channels/${encodeURIComponent(a)}/test`, { method: "POST" });
|
|
990
|
+
print(r, () => console.log(r.ok ? "Sent. Check that it arrived." : `Not delivered: ${r.error}`));
|
|
991
|
+
if (!r.ok)
|
|
992
|
+
process.exitCode = 1;
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
case "remove": {
|
|
996
|
+
if (!a)
|
|
997
|
+
throw usageError("missing channel id");
|
|
998
|
+
confirm(args.includes("--yes"), `Alerts will no longer be sent to channel ${a}.`, a, "the channel id");
|
|
999
|
+
await api(`/alerts/channels/${encodeURIComponent(a)}`, { method: "DELETE" });
|
|
1000
|
+
print({ removed: a }, () => console.log(`Removed channel ${a}`));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
case "rotate": {
|
|
1004
|
+
if (!a)
|
|
1005
|
+
throw usageError("missing channel id");
|
|
1006
|
+
const c = await api(`/alerts/channels/${encodeURIComponent(a)}/secret/rotate`, { method: "POST" });
|
|
1007
|
+
print(c, () => {
|
|
1008
|
+
console.log("Rotated. The old secret stops working now.");
|
|
1009
|
+
printChannel(c);
|
|
1010
|
+
});
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
case "rule": {
|
|
1014
|
+
if (!a || b !== "on" && b !== "off")
|
|
1015
|
+
throw usageError("usage: mybase alerts rule <rule> on|off");
|
|
1016
|
+
const o = await api(`/alerts/rules/${encodeURIComponent(a)}`, { method: "PUT", body: JSON.stringify({ enabled: b === "on" }) });
|
|
1017
|
+
print(o.rules.find((r) => r.id === a), () => console.log(`Rule ${a} is ${b}.`));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
throw usageError(`unknown: mybase alerts ${sub}
|
|
1022
|
+
|
|
1023
|
+
${alertsUsage}`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/audit.ts
|
|
1027
|
+
var auditUsage = ` audit [--project p] [--action auth|auth.login] [--outcome ok|denied|failed] [--since 24h|7d|<ISO date>] [--limit n] [--follow]
|
|
1028
|
+
Recent audit log entries, newest first (--follow: keep printing new ones)
|
|
1029
|
+
audit retention [<days>] Show (or set, 7\u2013365, admin key) how long entries are kept`;
|
|
1030
|
+
function sinceDate(value, now = Date.now()) {
|
|
1031
|
+
const m = /^(\d+)(m|h|d)$/.exec(value);
|
|
1032
|
+
if (m)
|
|
1033
|
+
return new Date(now - Number(m[1]) * { m: 60000, h: 3600000, d: 86400000 }[m[2]]).toISOString();
|
|
1034
|
+
if (Number.isNaN(Date.parse(value)))
|
|
1035
|
+
throw usageError(`--since must be like 24h, 7d, or a date (got "${value}")`);
|
|
1036
|
+
return new Date(value).toISOString();
|
|
1037
|
+
}
|
|
1038
|
+
var actorLabel = (e) => e.actor.kind === "token" ? `key "${e.actor.name}"` : e.actor.kind === "session" ? e.actor.name ?? "admin" : e.actor.kind === "system" ? "mybase" : e.actor.name ? `${e.actor.name} (not signed in)` : "anonymous";
|
|
1039
|
+
function formatEvent(e) {
|
|
1040
|
+
const where = [e.project && `[${e.project}]`, e.target].filter(Boolean).join(" ");
|
|
1041
|
+
const meta = e.meta ? ` ${Object.entries(e.meta).map(([k, v]) => `${k}=${v}`).join(" ")}` : "";
|
|
1042
|
+
return `${e.at.replace("T", " ").slice(0, 19)} ${e.outcome.padEnd(6)} ${e.action.padEnd(24)} ${actorLabel(e)}${e.ip ? ` from ${e.ip}` : ""}${where ? ` ${where}` : ""}${meta}`;
|
|
1043
|
+
}
|
|
1044
|
+
async function audit(args) {
|
|
1045
|
+
const { positional, flags } = parseArgs(args);
|
|
1046
|
+
if (positional[0] === "retention") {
|
|
1047
|
+
const days = positional[1];
|
|
1048
|
+
const settings = days ? await api("/audit/settings", { method: "PUT", body: JSON.stringify({ retentionDays: Number(days) }) }) : await api("/audit/settings");
|
|
1049
|
+
print(settings, () => console.log(`Audit entries are kept for ${settings.retentionDays} days.`));
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (positional.length)
|
|
1053
|
+
throw usageError(`unknown: mybase audit ${positional[0]}
|
|
1054
|
+
|
|
1055
|
+
${auditUsage}`);
|
|
1056
|
+
const query = new URLSearchParams;
|
|
1057
|
+
for (const name of ["project", "action", "outcome", "actor"]) {
|
|
1058
|
+
const value = flagString(flags, name);
|
|
1059
|
+
if (value)
|
|
1060
|
+
query.set(name, value);
|
|
1061
|
+
}
|
|
1062
|
+
const since = flagString(flags, "since");
|
|
1063
|
+
if (since)
|
|
1064
|
+
query.set("since", sinceDate(since));
|
|
1065
|
+
query.set("limit", String(flagInt(flags, "limit") ?? 50));
|
|
1066
|
+
const page = await api(`/audit?${query}`);
|
|
1067
|
+
if (!flags.has("follow")) {
|
|
1068
|
+
print(page, () => {
|
|
1069
|
+
if (page.events.length === 0)
|
|
1070
|
+
console.log("No audit entries match.");
|
|
1071
|
+
for (const e of [...page.events].reverse())
|
|
1072
|
+
console.log(formatEvent(e));
|
|
1073
|
+
});
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
let last = page.events[0]?.id ?? "0";
|
|
1077
|
+
const show = (events) => {
|
|
1078
|
+
for (const e of [...events].reverse())
|
|
1079
|
+
console.log(jsonMode ? JSON.stringify(e) : formatEvent(e));
|
|
1080
|
+
};
|
|
1081
|
+
show(page.events);
|
|
1082
|
+
query.delete("since");
|
|
1083
|
+
for (;; ) {
|
|
1084
|
+
await Bun.sleep(5000);
|
|
1085
|
+
const fresh = await api(`/audit?${query}`);
|
|
1086
|
+
const newer = fresh.events.filter((e) => BigInt(e.id) > BigInt(last));
|
|
1087
|
+
if (newer.length) {
|
|
1088
|
+
show(newer);
|
|
1089
|
+
last = newer[0].id;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
773
1094
|
// src/auth.ts
|
|
774
1095
|
import { hostname } from "os";
|
|
775
1096
|
|
|
776
1097
|
// src/token.ts
|
|
777
1098
|
var tokenUsage = ` token list List API keys and what they may do
|
|
778
|
-
token create <name> (--perm <p,\u2026> [--project <name,\u2026>] | --admin)
|
|
1099
|
+
token create <name> (--perm <p,\u2026> [--project <name,\u2026>] [--env <env,\u2026>] | --admin)
|
|
779
1100
|
Make a key and print it once. <p>: <resource>:read|write,
|
|
780
1101
|
resources: ${KEY_RESOURCES.map((r) => r.id).join(", ")}
|
|
781
|
-
|
|
1102
|
+
--env limits secrets to those environments
|
|
1103
|
+
token update <id> [--name <n>] [--perm <p,\u2026>] [--project <name,\u2026>|--all-projects]
|
|
1104
|
+
[--env <env,\u2026>|--all-envs] [--admin]
|
|
782
1105
|
token delete <id> Revoke a key (it stops working now)`;
|
|
783
1106
|
var list = (value) => value?.split(",").map((v) => v.trim()).filter(Boolean);
|
|
784
|
-
var describeGrant = (t) => `${t.permissions.join(",")} on ${t.projects ? t.projects.join(",") : "all projects"}`;
|
|
1107
|
+
var describeGrant = (t) => `${t.permissions.join(",")} on ${t.projects ? t.projects.join(",") : "all projects"}${t.environments ? ` (secrets envs ${t.environments.join(",")})` : ""}`;
|
|
785
1108
|
function grantFlags(flags) {
|
|
786
1109
|
const body = {};
|
|
787
1110
|
if (flags.has("admin")) {
|
|
788
1111
|
body.permissions = ["admin"];
|
|
789
1112
|
body.projects = null;
|
|
1113
|
+
body.environments = null;
|
|
790
1114
|
} else if (flags.has("perm"))
|
|
791
1115
|
body.permissions = list(flagString(flags, "perm"));
|
|
792
1116
|
if (flags.has("all-projects"))
|
|
793
1117
|
body.projects = null;
|
|
794
1118
|
else if (flags.has("project"))
|
|
795
1119
|
body.projects = list(flagString(flags, "project")) ?? null;
|
|
1120
|
+
if (flags.has("all-envs"))
|
|
1121
|
+
body.environments = null;
|
|
1122
|
+
else if (flags.has("env"))
|
|
1123
|
+
body.environments = list(flagString(flags, "env")) ?? null;
|
|
796
1124
|
return body;
|
|
797
1125
|
}
|
|
798
1126
|
async function token(sub, args) {
|
|
@@ -828,7 +1156,7 @@ async function token(sub, args) {
|
|
|
828
1156
|
const id = need(first, "key id");
|
|
829
1157
|
const body = { ...grantFlags(flags), ...flags.has("name") ? { name: flagString(flags, "name") } : {} };
|
|
830
1158
|
if (Object.keys(body).length === 0)
|
|
831
|
-
throw new Error("nothing to change: pass --name, --perm, --project, --all-projects, or --admin");
|
|
1159
|
+
throw new Error("nothing to change: pass --name, --perm, --project, --all-projects, --env, --all-envs, or --admin");
|
|
832
1160
|
const updated = await api(`/auth/tokens/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(body) });
|
|
833
1161
|
print(updated, () => console.log(`Updated "${updated.name}" (${describeGrant(updated)})`));
|
|
834
1162
|
return true;
|
|
@@ -902,16 +1230,28 @@ async function login(args) {
|
|
|
902
1230
|
throw new Error("missing username");
|
|
903
1231
|
const password = await readSecret("Password: ");
|
|
904
1232
|
const tokenName = `cli on ${hostname()}`;
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1233
|
+
const post = async (path, input) => {
|
|
1234
|
+
const res = await fetch(`${url}${path}`, {
|
|
1235
|
+
method: "POST",
|
|
1236
|
+
headers: { "Content-Type": "application/json" },
|
|
1237
|
+
body: JSON.stringify(input)
|
|
1238
|
+
}).catch(() => {
|
|
1239
|
+
throw new Error(`can't reach the API at ${url}`);
|
|
1240
|
+
});
|
|
1241
|
+
const body = await res.json().catch(() => ({}));
|
|
1242
|
+
if (!res.ok)
|
|
1243
|
+
throw new Error(body.error ?? res.statusText);
|
|
1244
|
+
return body;
|
|
1245
|
+
};
|
|
1246
|
+
let body = await post("/auth/login", { username, password, tokenName });
|
|
1247
|
+
if (body.mfaRequired && body.challenge) {
|
|
1248
|
+
const code = (await readLine("Code (or recovery code):")).trim();
|
|
1249
|
+
if (!code)
|
|
1250
|
+
throw new Error("missing code");
|
|
1251
|
+
body = await post("/auth/login/mfa", { challenge: body.challenge, code });
|
|
1252
|
+
}
|
|
1253
|
+
if (!body.token)
|
|
1254
|
+
throw new Error("the server didn't return a token");
|
|
915
1255
|
const previous = await loadConfig();
|
|
916
1256
|
await saveConfig({ url, token: body.token });
|
|
917
1257
|
if (previous.token && previous.url)
|
|
@@ -953,7 +1293,7 @@ async function whoami() {
|
|
|
953
1293
|
if (!status.authenticated)
|
|
954
1294
|
console.log(`not signed in to ${API_URL}. Run \`mybase login\`.`);
|
|
955
1295
|
else {
|
|
956
|
-
const grant = status.permissions ? ` (${describeGrant({ permissions: status.permissions, projects: status.projects })})` : "";
|
|
1296
|
+
const grant = status.permissions ? ` (${describeGrant({ permissions: status.permissions, projects: status.projects, environments: status.environments ?? null })})` : "";
|
|
957
1297
|
console.log(`${status.username} on ${API_URL}${grant}`);
|
|
958
1298
|
}
|
|
959
1299
|
if (warning)
|
|
@@ -1059,6 +1399,64 @@ async function cache(sub, args) {
|
|
|
1059
1399
|
return false;
|
|
1060
1400
|
}
|
|
1061
1401
|
|
|
1402
|
+
// src/captcha.ts
|
|
1403
|
+
var captchaUsage = ` captcha enable <project> Turn on the captcha and print the script tag and secret
|
|
1404
|
+
captcha key <project> [--rotate] Show (or replace) the secret your server checks tokens with
|
|
1405
|
+
captcha difficulty <project> <n> Numbers a browser may try per challenge (1000\u20131000000; default 50000)
|
|
1406
|
+
captcha disable <project> [--yes] Turn it off (forms that need it stop accepting submissions)`;
|
|
1407
|
+
var base3 = (project) => `/projects/${encodeURIComponent(project)}/captcha`;
|
|
1408
|
+
function printSettings2(project, s) {
|
|
1409
|
+
console.log("Script tag (forms marked data-mybase-captcha get a hidden mybase-captcha field):");
|
|
1410
|
+
console.log(` <script defer src="${s.scriptUrl}" data-project="${project}"></script>
|
|
1411
|
+
`);
|
|
1412
|
+
console.log(` secret ${s.secretKey} (server-side only: MYBASE_CAPTCHA_SECRET)`);
|
|
1413
|
+
console.log(` difficulty ${s.maxNumber.toLocaleString()} numbers per challenge`);
|
|
1414
|
+
console.log(` siteverify ${s.siteVerifyUrl} (Turnstile-compatible; Better Auth: mybaseCaptcha())`);
|
|
1415
|
+
}
|
|
1416
|
+
async function captcha(sub, args) {
|
|
1417
|
+
const { positional, flags } = parseArgs(args);
|
|
1418
|
+
const [project, value] = positional;
|
|
1419
|
+
switch (sub) {
|
|
1420
|
+
case "enable": {
|
|
1421
|
+
const p = need(project, "project name");
|
|
1422
|
+
await api(`/projects/${encodeURIComponent(p)}/services/captcha`, { method: "PUT" });
|
|
1423
|
+
const s = await api(base3(p));
|
|
1424
|
+
print({ enabled: "captcha", project: p, ...s }, () => {
|
|
1425
|
+
console.log(`Captcha enabled for "${p}"
|
|
1426
|
+
`);
|
|
1427
|
+
printSettings2(p, s);
|
|
1428
|
+
});
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
case "key": {
|
|
1432
|
+
const p = need(project, "project name");
|
|
1433
|
+
const s = flags.has("rotate") ? await api(`${base3(p)}/secret/rotate`, { method: "POST" }) : await api(base3(p));
|
|
1434
|
+
print(s, () => {
|
|
1435
|
+
if (flags.has("rotate"))
|
|
1436
|
+
console.log(`Rotated. The old secret stops working now.
|
|
1437
|
+
`);
|
|
1438
|
+
printSettings2(p, s);
|
|
1439
|
+
});
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
case "difficulty": {
|
|
1443
|
+
const p = need(project, "project name");
|
|
1444
|
+
const n = Number(need(value, "a number"));
|
|
1445
|
+
const s = await api(base3(p), { method: "PATCH", body: JSON.stringify({ maxNumber: n }) });
|
|
1446
|
+
print(s, () => console.log(`Challenges now take up to ${s.maxNumber.toLocaleString()} tries.`));
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
case "disable": {
|
|
1450
|
+
const p = need(project, "project name");
|
|
1451
|
+
confirm(args.includes("--yes"), `Forms on "${p}" that check the captcha will refuse every submission until you remove the check.`, p, "the project name");
|
|
1452
|
+
await api(`/projects/${encodeURIComponent(p)}/services/captcha`, { method: "DELETE" });
|
|
1453
|
+
print({ disabled: "captcha", project: p }, () => console.log(`Captcha disabled for "${p}"`));
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1062
1460
|
// src/docs.ts
|
|
1063
1461
|
var docsUsage = ` docs List docs pages
|
|
1064
1462
|
docs <slug> Print a page as markdown (e.g. quickstart, queues, sdk/queues)
|
|
@@ -1105,6 +1503,35 @@ Read one: mybase docs <slug>. For agents: ${API_URL}/llms.txt`);
|
|
|
1105
1503
|
return true;
|
|
1106
1504
|
}
|
|
1107
1505
|
|
|
1506
|
+
// src/doctor.ts
|
|
1507
|
+
var doctorUsage = ` doctor [<project>] Security checks: the whole instance (admin key), or one project. Exits 1 on critical findings`;
|
|
1508
|
+
var LABELS = { critical: "CRITICAL", warn: "warning", info: "info" };
|
|
1509
|
+
async function doctor(args) {
|
|
1510
|
+
const project = args.find((a) => !a.startsWith("-"));
|
|
1511
|
+
const report = await api(project ? `/projects/${encodeURIComponent(project)}/advisor` : "/advisor");
|
|
1512
|
+
print(report, () => {
|
|
1513
|
+
const scope = project ? `project "${project}"` : "this instance";
|
|
1514
|
+
if (report.findings.length === 0) {
|
|
1515
|
+
console.log(`No problems found in ${scope} (${report.checks.length} checks).`);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
console.log(`${report.findings.length} finding(s) in ${scope}:
|
|
1519
|
+
`);
|
|
1520
|
+
for (const f of report.findings) {
|
|
1521
|
+
console.log(`${LABELS[f.severity]} ${f.title}${f.project && !project ? ` [${f.project}]` : ""}`);
|
|
1522
|
+
console.log(` ${f.detail}`);
|
|
1523
|
+
console.log(` Fix: ${f.fix}
|
|
1524
|
+
`);
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
const critical = report.findings.filter((f) => f.severity === "critical").length;
|
|
1528
|
+
if (critical) {
|
|
1529
|
+
if (jsonMode)
|
|
1530
|
+
process.exit(1);
|
|
1531
|
+
throw new CliError(`${critical} critical finding(s)`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1108
1535
|
// src/queue.ts
|
|
1109
1536
|
var queueUsage = ` queue enable <project> Turn on queues
|
|
1110
1537
|
queue disable <project> [--yes] Turn them off, deleting every queue and message
|
|
@@ -1119,8 +1546,8 @@ var queueUsage = ` queue enable <project> Turn on queues
|
|
|
1119
1546
|
queue redrive <project> <dlq> [--to <queue>] Move dead letters back to their queues
|
|
1120
1547
|
queue stats <project> [--range 24h|7d] Sent, received, deleted, dead-lettered per queue
|
|
1121
1548
|
queue sync [--config <file>] [--dry-run] [--yes] Make code-defined queues match mybase.config.ts`;
|
|
1122
|
-
var
|
|
1123
|
-
var queuePath = (project, name, rest = "") => `${
|
|
1549
|
+
var base4 = (project) => `/projects/${encodeURIComponent(project)}/queue`;
|
|
1550
|
+
var queuePath = (project, name, rest = "") => `${base4(project)}/queues/${encodeURIComponent(name)}${rest}`;
|
|
1124
1551
|
function formatMs(ms) {
|
|
1125
1552
|
for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
|
|
1126
1553
|
if (ms >= size && ms % size === 0)
|
|
@@ -1144,7 +1571,7 @@ async function queue(sub, args) {
|
|
|
1144
1571
|
case "enable": {
|
|
1145
1572
|
const p = need(project, "project name");
|
|
1146
1573
|
await api(`/projects/${encodeURIComponent(p)}/services/queue`, { method: "PUT" });
|
|
1147
|
-
const settings = await api(
|
|
1574
|
+
const settings = await api(base4(p));
|
|
1148
1575
|
print({ enabled: "queue", project: p, ...settings }, () => {
|
|
1149
1576
|
console.log(`Queues enabled for "${p}"
|
|
1150
1577
|
`);
|
|
@@ -1161,7 +1588,7 @@ async function queue(sub, args) {
|
|
|
1161
1588
|
}
|
|
1162
1589
|
case "list": {
|
|
1163
1590
|
const p = need(project, "project name");
|
|
1164
|
-
const queues = await api(`${
|
|
1591
|
+
const queues = await api(`${base4(p)}/queues`);
|
|
1165
1592
|
print(queues, () => {
|
|
1166
1593
|
if (queues.length === 0)
|
|
1167
1594
|
console.log("No queues yet.");
|
|
@@ -1187,7 +1614,7 @@ async function queue(sub, args) {
|
|
|
1187
1614
|
contentDedup: flags.has("content-dedup") || undefined,
|
|
1188
1615
|
deadLetter: dlq ? { queue: dlq, maxReceives: flagInt(flags, "max-receives") ?? 5 } : undefined
|
|
1189
1616
|
};
|
|
1190
|
-
const q = await api(`${
|
|
1617
|
+
const q = await api(`${base4(p)}/queues`, { method: "POST", body: JSON.stringify(body) });
|
|
1191
1618
|
print(q, () => console.log(`Created ${q.fifo ? "FIFO" : "standard"} queue "${q.name}" (visibility ${formatMs(q.visibilityTimeoutMs)}, retention ${formatMs(q.retentionMs)})`));
|
|
1192
1619
|
return true;
|
|
1193
1620
|
}
|
|
@@ -1272,7 +1699,7 @@ async function queue(sub, args) {
|
|
|
1272
1699
|
const range = flagString(flags, "range") ?? "24h";
|
|
1273
1700
|
if (!QUEUE_STATS_RANGES.includes(range))
|
|
1274
1701
|
throw new Error(`--range must be one of ${QUEUE_STATS_RANGES.join(", ")}`);
|
|
1275
|
-
const stats = await api(`${
|
|
1702
|
+
const stats = await api(`${base4(p)}/stats?range=${range}`);
|
|
1276
1703
|
print(stats, () => {
|
|
1277
1704
|
const { queues } = stats;
|
|
1278
1705
|
console.log(`Last ${range}:`);
|
|
@@ -1301,7 +1728,7 @@ function parseDuration(value) {
|
|
|
1301
1728
|
async function sync2(file, dryRun, yes) {
|
|
1302
1729
|
const config = await readConfigFile(file);
|
|
1303
1730
|
const queues = Object.entries(config.queue?.queues ?? {}).map(([name, q]) => ({ ...q, name }));
|
|
1304
|
-
const path = `${
|
|
1731
|
+
const path = `${base4(config.project)}/queues`;
|
|
1305
1732
|
const plan = await api(path, { method: "PUT", body: JSON.stringify({ queues, dryRun: true }) });
|
|
1306
1733
|
if (dryRun)
|
|
1307
1734
|
return printSync(config.project, true, plan);
|
|
@@ -1311,6 +1738,192 @@ async function sync2(file, dryRun, yes) {
|
|
|
1311
1738
|
printSync(config.project, false, await api(path, { method: "PUT", body: JSON.stringify({ queues }) }));
|
|
1312
1739
|
}
|
|
1313
1740
|
|
|
1741
|
+
// src/vector.ts
|
|
1742
|
+
var vectorUsage = ` vector enable <project> Turn on vector + full-text search
|
|
1743
|
+
vector disable <project> [--yes] Turn it off, deleting every index and document
|
|
1744
|
+
vector settings <project> Show the embedding provider
|
|
1745
|
+
vector embedding <project> --url <url> --model <m> --dims <n> [--key] Set it (--key prompts for an API key)
|
|
1746
|
+
vector embedding <project> --off Remove it
|
|
1747
|
+
vector list <project> List indexes with document counts
|
|
1748
|
+
vector create <project> <index> [--dims <n>] [--metric cosine|l2|ip] [--lang english] [--embed]
|
|
1749
|
+
vector delete <project> <index> [--yes] Delete an index and its documents
|
|
1750
|
+
vector upsert <project> <index> <file.jsonl> Upsert documents, one JSON object per line
|
|
1751
|
+
vector documents <project> <index> [--limit 20] [--after <id>] List documents in id order
|
|
1752
|
+
vector query <project> <index> [<text>] [--mode text|vector|hybrid] [--top 10] [--filter <json>]
|
|
1753
|
+
vector sync [--config <file>] [--dry-run] [--yes] Make code-defined indexes match mybase.config.ts`;
|
|
1754
|
+
var base5 = (project) => `/projects/${encodeURIComponent(project)}/vector`;
|
|
1755
|
+
var indexPath = (project, name, rest = "") => `${base5(project)}/indexes/${encodeURIComponent(name)}${rest}`;
|
|
1756
|
+
function describe(i) {
|
|
1757
|
+
const vectors = i.dimensions ? `${i.dimensions}d ${i.metric}` : "text only";
|
|
1758
|
+
return [i.name, vectors, i.language, i.embed ? "embeds" : "", `${i.documents} docs`, i.source].filter(Boolean).join("\t");
|
|
1759
|
+
}
|
|
1760
|
+
function printUsage2(project, settings) {
|
|
1761
|
+
console.log("In your app (@mybase-dev/sdk), with an API key that has vector:write for this project:");
|
|
1762
|
+
console.log(` mybase token create ${project}-app --project ${project} --perm vector:write
|
|
1763
|
+
`);
|
|
1764
|
+
console.log(` const vector = createVectorClient({ apiUrl: "${settings.apiUrl}", project: "${project}", apiKey: process.env.MYBASE_API_KEY! })`);
|
|
1765
|
+
console.log(` const { matches } = await vector.index("<index>").query("search text")`);
|
|
1766
|
+
}
|
|
1767
|
+
async function vector(sub, args) {
|
|
1768
|
+
const { positional, flags } = parseArgs(args);
|
|
1769
|
+
const [project, name, third] = positional;
|
|
1770
|
+
switch (sub) {
|
|
1771
|
+
case "enable": {
|
|
1772
|
+
const p = need(project, "project name");
|
|
1773
|
+
await api(`/projects/${encodeURIComponent(p)}/services/vector`, { method: "PUT" });
|
|
1774
|
+
const settings = await api(base5(p));
|
|
1775
|
+
print({ enabled: "vector", project: p, ...settings }, () => {
|
|
1776
|
+
console.log(`Vector search enabled for "${p}"
|
|
1777
|
+
`);
|
|
1778
|
+
printUsage2(p, settings);
|
|
1779
|
+
});
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
case "disable": {
|
|
1783
|
+
const p = need(project, "project name");
|
|
1784
|
+
confirm(flags.has("yes"), `This deletes every vector index and document of "${p}".`, p, "the project name");
|
|
1785
|
+
await api(`/projects/${encodeURIComponent(p)}/services/vector`, { method: "DELETE" });
|
|
1786
|
+
print({ disabled: "vector", project: p }, () => console.log(`Vector search disabled for "${p}"`));
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
case "settings": {
|
|
1790
|
+
const p = need(project, "project name");
|
|
1791
|
+
const settings = await api(base5(p));
|
|
1792
|
+
print(settings, () => {
|
|
1793
|
+
const e = settings.embedding;
|
|
1794
|
+
console.log(e ? `embedding ${e.model} (${e.dimensions}d) at ${e.url}${e.hasKey ? ", API key stored" : ""}` : "embedding none (apps send their own vectors)");
|
|
1795
|
+
});
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
case "embedding": {
|
|
1799
|
+
const p = need(project, "project name");
|
|
1800
|
+
let body;
|
|
1801
|
+
if (flags.has("off"))
|
|
1802
|
+
body = { embedding: null };
|
|
1803
|
+
else {
|
|
1804
|
+
const dims = flagInt(flags, "dims");
|
|
1805
|
+
if (dims === undefined)
|
|
1806
|
+
throw new Error("--dims is required (the model's vector length, e.g. 1536)");
|
|
1807
|
+
const apiKey = flags.has("key") ? await readSecret("API key: ") : undefined;
|
|
1808
|
+
body = { embedding: { url: need(flagString(flags, "url"), "--url"), model: need(flagString(flags, "model"), "--model"), dimensions: dims, apiKey } };
|
|
1809
|
+
}
|
|
1810
|
+
const settings = await api(base5(p), { method: "PUT", body: JSON.stringify(body) });
|
|
1811
|
+
print(settings, () => console.log(settings.embedding ? `Embedding with ${settings.embedding.model} (${settings.embedding.dimensions}d)` : "Embedding provider removed"));
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
case "list": {
|
|
1815
|
+
const p = need(project, "project name");
|
|
1816
|
+
const indexes = await api(`${base5(p)}/indexes`);
|
|
1817
|
+
print(indexes, () => {
|
|
1818
|
+
if (indexes.length === 0)
|
|
1819
|
+
console.log("No indexes yet.");
|
|
1820
|
+
for (const i of indexes)
|
|
1821
|
+
console.log(describe(i));
|
|
1822
|
+
});
|
|
1823
|
+
return true;
|
|
1824
|
+
}
|
|
1825
|
+
case "create": {
|
|
1826
|
+
const p = need(project, "project name");
|
|
1827
|
+
const body = {
|
|
1828
|
+
name: need(name, "index name"),
|
|
1829
|
+
dimensions: flagInt(flags, "dims"),
|
|
1830
|
+
metric: flagString(flags, "metric"),
|
|
1831
|
+
language: flagString(flags, "lang"),
|
|
1832
|
+
embed: flags.has("embed") || undefined
|
|
1833
|
+
};
|
|
1834
|
+
const index = await api(`${base5(p)}/indexes`, { method: "POST", body: JSON.stringify(body) });
|
|
1835
|
+
print(index, () => console.log(`Created index "${index.name}" (${index.dimensions ? `${index.dimensions}d ${index.metric}` : "text only"}, ${index.language})`));
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
case "delete": {
|
|
1839
|
+
const p = need(project, "project name");
|
|
1840
|
+
const n = need(name, "index name");
|
|
1841
|
+
confirm(flags.has("yes"), `This deletes index "${n}" and every document in it.`, n, "the index name");
|
|
1842
|
+
await api(indexPath(p, n), { method: "DELETE" });
|
|
1843
|
+
print({ deleted: n }, () => console.log(`Deleted index "${n}"`));
|
|
1844
|
+
return true;
|
|
1845
|
+
}
|
|
1846
|
+
case "upsert": {
|
|
1847
|
+
const p = need(project, "project name");
|
|
1848
|
+
const n = need(name, "index name");
|
|
1849
|
+
const file = need(third, "JSONL file");
|
|
1850
|
+
const lines = (await Bun.file(file).text()).split(/\r?\n/).filter((l) => l.trim());
|
|
1851
|
+
const documents = lines.map((line, i) => {
|
|
1852
|
+
try {
|
|
1853
|
+
return JSON.parse(line);
|
|
1854
|
+
} catch {
|
|
1855
|
+
throw new Error(`${file}:${i + 1} is not JSON`);
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
let upserted = 0;
|
|
1859
|
+
let embedded = 0;
|
|
1860
|
+
for (let i = 0;i < documents.length; i += 100) {
|
|
1861
|
+
const res = await api(indexPath(p, n, "/documents"), { method: "POST", body: JSON.stringify({ documents: documents.slice(i, i + 100) }) });
|
|
1862
|
+
upserted += res.upserted;
|
|
1863
|
+
embedded += res.embedded;
|
|
1864
|
+
}
|
|
1865
|
+
print({ upserted, embedded }, () => console.log(`Upserted ${upserted} documents into "${n}"${embedded ? ` (${embedded} embedded)` : ""}`));
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
case "query": {
|
|
1869
|
+
const p = need(project, "project name");
|
|
1870
|
+
const n = need(name, "index name");
|
|
1871
|
+
const rawFilter = flagString(flags, "filter");
|
|
1872
|
+
let filter;
|
|
1873
|
+
try {
|
|
1874
|
+
filter = rawFilter === undefined ? undefined : JSON.parse(rawFilter);
|
|
1875
|
+
} catch {
|
|
1876
|
+
throw new Error(`--filter must be JSON, e.g. '{"lang":"en"}'`);
|
|
1877
|
+
}
|
|
1878
|
+
const body = { text: third, mode: flagString(flags, "mode"), topK: flagInt(flags, "top"), filter, highlight: true };
|
|
1879
|
+
const res = await api(indexPath(p, n, "/query"), { method: "POST", body: JSON.stringify(body) });
|
|
1880
|
+
print(res, () => {
|
|
1881
|
+
if (res.matches.length === 0)
|
|
1882
|
+
console.log(`No matches (${res.mode}).`);
|
|
1883
|
+
for (const m of res.matches) {
|
|
1884
|
+
console.log(`${m.score.toFixed(4)} ${m.id}${m.title ? ` ${m.title}` : ""}`);
|
|
1885
|
+
const snippet = m.highlight ?? m.text?.slice(0, 160);
|
|
1886
|
+
if (snippet)
|
|
1887
|
+
console.log(` ${snippet.replace(/\s+/g, " ")}`);
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
case "documents": {
|
|
1893
|
+
const p = need(project, "project name");
|
|
1894
|
+
const n = need(name, "index name");
|
|
1895
|
+
const params = new URLSearchParams({ limit: String(flagInt(flags, "limit") ?? 20) });
|
|
1896
|
+
const after = flagString(flags, "after");
|
|
1897
|
+
if (after)
|
|
1898
|
+
params.set("after", after);
|
|
1899
|
+
const res = await api(indexPath(p, n, `/documents?${params}`));
|
|
1900
|
+
print(res, () => {
|
|
1901
|
+
for (const d of res.documents)
|
|
1902
|
+
console.log(`${d.id} ${d.title ?? d.text.slice(0, 80).replace(/\s+/g, " ")}`);
|
|
1903
|
+
if (res.next)
|
|
1904
|
+
console.log(`more: --after ${res.next}`);
|
|
1905
|
+
});
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
case "sync":
|
|
1909
|
+
await sync3(flagString(flags, "config") ?? "mybase.config.ts", flags.has("dry-run"), flags.has("yes"));
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
async function sync3(file, dryRun, yes) {
|
|
1915
|
+
const config = await readConfigFile(file);
|
|
1916
|
+
const indexes = Object.entries(config.vector?.indexes ?? {}).map(([name, i]) => ({ ...i, name }));
|
|
1917
|
+
const path = `${base5(config.project)}/indexes`;
|
|
1918
|
+
const plan = await api(path, { method: "PUT", body: JSON.stringify({ indexes, dryRun: true }) });
|
|
1919
|
+
if (dryRun)
|
|
1920
|
+
return printSync(config.project, true, plan);
|
|
1921
|
+
if (plan.deleted.length > 0) {
|
|
1922
|
+
confirm(yes, `This deletes ${plan.deleted.map((n) => `"${n}"`).join(", ")} and every document in ${plan.deleted.length === 1 ? "it" : "them"}.`, "yes");
|
|
1923
|
+
}
|
|
1924
|
+
printSync(config.project, false, await api(path, { method: "PUT", body: JSON.stringify({ indexes }) }));
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1314
1927
|
// src/ratelimit.ts
|
|
1315
1928
|
var ratelimitUsage = ` ratelimit enable <project> Turn on rate limits
|
|
1316
1929
|
ratelimit disable <project> [--yes] Turn them off, deleting every rule and counter
|
|
@@ -1320,7 +1933,7 @@ var ratelimitUsage = ` ratelimit enable <project> Turn on rate limits
|
|
|
1320
1933
|
ratelimit delete <project> <name> Delete a rule
|
|
1321
1934
|
ratelimit stats <project> [--range 24h|7d] Allowed and blocked requests per rule
|
|
1322
1935
|
ratelimit sync [--config <file>] [--dry-run] Make code-defined rules match mybase.config.ts`;
|
|
1323
|
-
var
|
|
1936
|
+
var base6 = (project) => `/projects/${encodeURIComponent(project)}/ratelimit`;
|
|
1324
1937
|
function formatWindow(ms) {
|
|
1325
1938
|
for (const [unit, size] of [["d", 86400000], ["h", 3600000], ["m", 60000], ["s", 1000]]) {
|
|
1326
1939
|
if (ms % size === 0)
|
|
@@ -1328,7 +1941,7 @@ function formatWindow(ms) {
|
|
|
1328
1941
|
}
|
|
1329
1942
|
return `${ms}ms`;
|
|
1330
1943
|
}
|
|
1331
|
-
function
|
|
1944
|
+
function printUsage3(project, settings) {
|
|
1332
1945
|
console.log(` check URL ${settings.limitUrl}
|
|
1333
1946
|
`);
|
|
1334
1947
|
console.log("In your app (@mybase-dev/sdk), with an API key that has ratelimit:write for this project:");
|
|
@@ -1344,11 +1957,11 @@ async function ratelimit(sub, args) {
|
|
|
1344
1957
|
case "enable": {
|
|
1345
1958
|
const p = need(project, "project name");
|
|
1346
1959
|
await api(`/projects/${encodeURIComponent(p)}/services/ratelimit`, { method: "PUT" });
|
|
1347
|
-
const settings = await api(
|
|
1960
|
+
const settings = await api(base6(p));
|
|
1348
1961
|
print({ enabled: "ratelimit", project: p, ...settings }, () => {
|
|
1349
1962
|
console.log(`Rate limits enabled for "${p}"
|
|
1350
1963
|
`);
|
|
1351
|
-
|
|
1964
|
+
printUsage3(p, settings);
|
|
1352
1965
|
});
|
|
1353
1966
|
return true;
|
|
1354
1967
|
}
|
|
@@ -1361,7 +1974,7 @@ async function ratelimit(sub, args) {
|
|
|
1361
1974
|
}
|
|
1362
1975
|
case "rules": {
|
|
1363
1976
|
const p = need(project, "project name");
|
|
1364
|
-
const rules = await api(`${
|
|
1977
|
+
const rules = await api(`${base6(p)}/rules`);
|
|
1365
1978
|
print(rules, () => {
|
|
1366
1979
|
if (rules.length === 0)
|
|
1367
1980
|
console.log("No rules yet. Inline limits still work.");
|
|
@@ -1381,14 +1994,14 @@ async function ratelimit(sub, args) {
|
|
|
1381
1994
|
algorithm: flagString(flags, "algorithm"),
|
|
1382
1995
|
burst: flagInt(flags, "burst")
|
|
1383
1996
|
};
|
|
1384
|
-
const r = await api(`${
|
|
1997
|
+
const r = await api(`${base6(p)}/rules`, { method: "POST", body: JSON.stringify(body) });
|
|
1385
1998
|
print(r, () => console.log(`Created rule "${r.name}": ${r.limit} per ${formatWindow(r.windowMs)} (${r.algorithm})`));
|
|
1386
1999
|
return true;
|
|
1387
2000
|
}
|
|
1388
2001
|
case "delete": {
|
|
1389
2002
|
const p = need(project, "project name");
|
|
1390
2003
|
const n = need(name, "rule name");
|
|
1391
|
-
await api(`${
|
|
2004
|
+
await api(`${base6(p)}/rules/${encodeURIComponent(n)}`, { method: "DELETE" });
|
|
1392
2005
|
print({ deleted: n }, () => console.log(`Deleted rule "${n}"`));
|
|
1393
2006
|
return true;
|
|
1394
2007
|
}
|
|
@@ -1397,7 +2010,7 @@ async function ratelimit(sub, args) {
|
|
|
1397
2010
|
const range = flagString(flags, "range") ?? "24h";
|
|
1398
2011
|
if (!RATELIMIT_STATS_RANGES.includes(range))
|
|
1399
2012
|
throw new Error(`--range must be one of ${RATELIMIT_STATS_RANGES.join(", ")}`);
|
|
1400
|
-
const stats = await api(`${
|
|
2013
|
+
const stats = await api(`${base6(p)}/stats?range=${range}`);
|
|
1401
2014
|
print(stats, () => {
|
|
1402
2015
|
const { rules } = stats;
|
|
1403
2016
|
console.log(`Last ${range}:`);
|
|
@@ -1410,13 +2023,13 @@ async function ratelimit(sub, args) {
|
|
|
1410
2023
|
return true;
|
|
1411
2024
|
}
|
|
1412
2025
|
case "sync": {
|
|
1413
|
-
await
|
|
2026
|
+
await sync4(flagString(flags, "config") ?? "mybase.config.ts", flags.has("dry-run"));
|
|
1414
2027
|
return true;
|
|
1415
2028
|
}
|
|
1416
2029
|
}
|
|
1417
2030
|
return false;
|
|
1418
2031
|
}
|
|
1419
|
-
async function
|
|
2032
|
+
async function sync4(file, dryRun) {
|
|
1420
2033
|
const config = await readConfigFile(file);
|
|
1421
2034
|
const body = {
|
|
1422
2035
|
dryRun,
|
|
@@ -1428,76 +2041,3188 @@ async function sync3(file, dryRun) {
|
|
|
1428
2041
|
burst: rule.burst
|
|
1429
2042
|
}))
|
|
1430
2043
|
};
|
|
1431
|
-
printSync(config.project, dryRun, await api(`${
|
|
2044
|
+
printSync(config.project, dryRun, await api(`${base6(config.project)}/rules`, { method: "PUT", body: JSON.stringify(body) }));
|
|
1432
2045
|
}
|
|
1433
2046
|
|
|
1434
|
-
// src/
|
|
1435
|
-
|
|
1436
|
-
project list List projects
|
|
1437
|
-
project delete <name> [--yes] Delete a project, its database, and all its files`;
|
|
1438
|
-
var USAGE = {
|
|
1439
|
-
auth: authUsage,
|
|
1440
|
-
token: tokenUsage,
|
|
1441
|
-
project: projectUsage,
|
|
1442
|
-
context: contextUsage,
|
|
1443
|
-
docs: docsUsage,
|
|
1444
|
-
mcp: mcpUsage,
|
|
1445
|
-
agent: agentUsage,
|
|
1446
|
-
cron: cronUsage,
|
|
1447
|
-
analytics: analyticsUsage,
|
|
1448
|
-
cache: cacheUsage,
|
|
1449
|
-
ratelimit: ratelimitUsage,
|
|
1450
|
-
queue: queueUsage
|
|
1451
|
-
};
|
|
1452
|
-
var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
|
|
1453
|
-
--yes Skip confirmations (required without a terminal)
|
|
1454
|
-
MYBASE_API_URL, MYBASE_TOKEN Override the saved server and token
|
|
1455
|
-
Exit codes: 0 ok, 1 failure, 2 usage, 3 not signed in / bad token, 4 not found`;
|
|
1456
|
-
var usage = `Usage: mybase <command> [--json]
|
|
1457
|
-
|
|
1458
|
-
Commands:
|
|
1459
|
-
${Object.values(USAGE).join(`
|
|
1460
|
-
`)}
|
|
1461
|
-
help [<command>] Show this message, or one command's
|
|
2047
|
+
// src/run.ts
|
|
2048
|
+
import { constants } from "os";
|
|
1462
2049
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
2050
|
+
// src/secrets.ts
|
|
2051
|
+
var secretsUsage = ` secrets enable <project> Turn on secrets (dev, staging, prod to start)
|
|
2052
|
+
secrets disable <project> [--yes] Turn them off, deleting every env, value, and revision
|
|
2053
|
+
secrets setup -p <project> -e <env> Use this project + env in this directory (and below)
|
|
2054
|
+
secrets envs List environments
|
|
2055
|
+
secrets env create <name> [--from <root env>] Create an env (a branch env with --from)
|
|
2056
|
+
secrets env rename <name> <new name>
|
|
2057
|
+
secrets env delete <name> [--yes] Delete an env, its branch envs, and their history
|
|
2058
|
+
secrets list [--values] List keys (values hidden unless --values)
|
|
2059
|
+
secrets get <KEY> Print one value
|
|
2060
|
+
secrets set <KEY=value>\u2026 [--message <m>] Set keys (KEY=- reads the value from stdin)
|
|
2061
|
+
secrets unset <KEY>\u2026 [--message <m>]
|
|
2062
|
+
secrets upload <.env file> [--message <m>] Set every key in a .env file
|
|
2063
|
+
secrets history [--limit 20] Revisions, newest first
|
|
2064
|
+
secrets restore <revision> [--yes] Put values back as they were after <revision>
|
|
2065
|
+
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)
|
|
2067
|
+
secrets unlink [--yes] Remove the credentials from the env (they stay valid)
|
|
2068
|
+
Project/env: -p/--project, -e/--env, else MYBASE_PROJECT/MYBASE_ENV,
|
|
2069
|
+
else \`secrets setup\` for this directory`;
|
|
2070
|
+
var expandShortFlags = (args) => args.map((a) => a === "-p" ? "--project" : a === "-e" ? "--env" : a);
|
|
2071
|
+
async function target(flags, needEnv = true) {
|
|
2072
|
+
const setup = await dirSetup();
|
|
2073
|
+
const project = flagString(flags, "project") ?? process.env.MYBASE_PROJECT ?? setup?.project;
|
|
2074
|
+
const env = flagString(flags, "env") ?? process.env.MYBASE_ENV ?? setup?.env;
|
|
2075
|
+
if (!project || needEnv && !env) {
|
|
2076
|
+
throw usageError(`which ${project ? "env" : "project"}? Pass -p <project> -e <env>, or run \`mybase secrets setup -p <project> -e <env>\` here once`);
|
|
1471
2077
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2078
|
+
return { project, env: env ?? "" };
|
|
2079
|
+
}
|
|
2080
|
+
var base7 = (project) => `/projects/${encodeURIComponent(project)}/secrets`;
|
|
2081
|
+
var envPath = (project, env, rest = "") => `${base7(project)}/envs/${encodeURIComponent(env)}${rest}`;
|
|
2082
|
+
var fetchSecrets = (project, env) => api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}`);
|
|
2083
|
+
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");
|
|
2084
|
+
async function readStdin() {
|
|
2085
|
+
if (process.stdin.isTTY)
|
|
2086
|
+
return (prompt("Value:") ?? "").replace(/\n$/, "");
|
|
2087
|
+
return (await Bun.stdin.text()).replace(/\r?\n$/, "");
|
|
2088
|
+
}
|
|
2089
|
+
function printUpdate(result, env) {
|
|
2090
|
+
print(result, () => console.log(result.changed === 0 ? `No changes in "${env}".` : `Updated "${env}": ${result.changed} key(s), revision ${result.revision}`));
|
|
2091
|
+
}
|
|
2092
|
+
async function secrets(sub, args) {
|
|
2093
|
+
const { positional, flags } = parseArgs(expandShortFlags(args));
|
|
2094
|
+
const yes = flags.has("yes");
|
|
2095
|
+
const message = flagString(flags, "message");
|
|
2096
|
+
switch (sub) {
|
|
2097
|
+
case "enable": {
|
|
2098
|
+
const p = need(positional[0] ?? flagString(flags, "project"), "project name");
|
|
2099
|
+
await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "PUT" });
|
|
2100
|
+
const overview = await api(base7(p));
|
|
2101
|
+
print({ enabled: "secrets", project: p, ...overview }, () => {
|
|
2102
|
+
console.log(`Secrets enabled for "${p}": ${overview.envs.map((e) => e.name).join(", ")}
|
|
1495
2103
|
`);
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2104
|
+
console.log("Next:");
|
|
2105
|
+
console.log(` mybase secrets setup -p ${p} -e dev # once per repo`);
|
|
2106
|
+
console.log(` mybase secrets set API_URL=https://\u2026 # or: mybase secrets upload .env`);
|
|
2107
|
+
console.log(` mybase run -- bun start # starts your app with them`);
|
|
2108
|
+
});
|
|
2109
|
+
return true;
|
|
2110
|
+
}
|
|
2111
|
+
case "disable": {
|
|
2112
|
+
const p = need(positional[0] ?? flagString(flags, "project"), "project name");
|
|
2113
|
+
confirm(yes, `This deletes every secrets environment, value, and revision of "${p}". They can't be recovered.`, p, "the project name");
|
|
2114
|
+
await api(`/projects/${encodeURIComponent(p)}/services/secrets`, { method: "DELETE" });
|
|
2115
|
+
print({ disabled: "secrets", project: p }, () => console.log(`Secrets disabled for "${p}"`));
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
case "setup": {
|
|
2119
|
+
const { project, env } = await target(flags);
|
|
2120
|
+
const overview = await api(base7(project));
|
|
2121
|
+
if (!overview.envs.some((e) => e.name === env)) {
|
|
2122
|
+
throw usageError(`"${project}" has no env "${env}" (envs: ${overview.envs.map((e) => e.name).join(", ")})`);
|
|
2123
|
+
}
|
|
2124
|
+
await saveDirSetup(process.cwd(), { project, env });
|
|
2125
|
+
print({ dir: process.cwd(), project, env }, () => console.log(`This directory now uses "${project}" / "${env}". Try: mybase run -- <command>`));
|
|
2126
|
+
return true;
|
|
2127
|
+
}
|
|
2128
|
+
case "envs": {
|
|
2129
|
+
const { project } = await target(flags, false);
|
|
2130
|
+
const overview = await api(base7(project));
|
|
2131
|
+
print(overview.envs, () => {
|
|
2132
|
+
for (const e of overview.envs)
|
|
2133
|
+
console.log(describeEnv(e));
|
|
2134
|
+
});
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
case "env": {
|
|
2138
|
+
const [action, name, next] = positional;
|
|
2139
|
+
const { project } = await target(flags, false);
|
|
2140
|
+
if (action === "create") {
|
|
2141
|
+
const env = await api(`${base7(project)}/envs`, {
|
|
2142
|
+
method: "POST",
|
|
2143
|
+
body: JSON.stringify({ name: need(name, "env name"), parent: flagString(flags, "from") ?? null })
|
|
2144
|
+
});
|
|
2145
|
+
print(env, () => console.log(`Created "${env.name}"${env.parent ? ` (inherits "${env.parent}")` : ""}`));
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
if (action === "rename") {
|
|
2149
|
+
const env = await api(envPath(project, need(name, "env name")), { method: "PATCH", body: JSON.stringify({ name: need(next, "new name") }) });
|
|
2150
|
+
print(env, () => console.log(`Renamed "${name}" to "${env.name}"`));
|
|
2151
|
+
return true;
|
|
2152
|
+
}
|
|
2153
|
+
if (action === "delete") {
|
|
2154
|
+
const env = need(name, "env name");
|
|
2155
|
+
confirm(yes, `This deletes env "${env}" of "${project}", its branch envs, and all their values and history.`, env, "the env name");
|
|
2156
|
+
await api(envPath(project, env), { method: "DELETE" });
|
|
2157
|
+
print({ deleted: env, project }, () => console.log(`Deleted "${env}"`));
|
|
2158
|
+
return true;
|
|
2159
|
+
}
|
|
2160
|
+
throw usageError("mybase secrets env create|rename|delete <name>");
|
|
2161
|
+
}
|
|
2162
|
+
case "list": {
|
|
2163
|
+
const { project, env } = await target(flags);
|
|
2164
|
+
const values = flags.has("values");
|
|
2165
|
+
const detail = await api(`${envPath(project, env)}${values ? "?values=1" : ""}`);
|
|
2166
|
+
print(detail, () => {
|
|
2167
|
+
if (detail.secrets.length === 0)
|
|
2168
|
+
console.log(`No keys in "${env}" yet.`);
|
|
2169
|
+
for (const s of detail.secrets) {
|
|
2170
|
+
const tags = [s.inherited ? `from ${detail.env.parent}` : "", s.managed ? "managed" : ""].filter(Boolean).join(", ");
|
|
2171
|
+
console.log(`${values ? `${s.key}=${s.value}` : s.key}${tags ? ` (${tags})` : ""}`);
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
case "get": {
|
|
2177
|
+
const key = need(positional[0], "key");
|
|
2178
|
+
const { project, env } = await target(flags);
|
|
2179
|
+
const { value } = await api(`/secrets/${encodeURIComponent(project)}/${encodeURIComponent(env)}/${encodeURIComponent(key)}`);
|
|
2180
|
+
if (jsonMode)
|
|
2181
|
+
print({ key, value }, () => {});
|
|
2182
|
+
else
|
|
2183
|
+
process.stdout.write(`${value}
|
|
2184
|
+
`);
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
case "set": {
|
|
2188
|
+
if (positional.length === 0)
|
|
2189
|
+
throw usageError("mybase secrets set KEY=value \u2026");
|
|
2190
|
+
const { project, env } = await target(flags);
|
|
2191
|
+
const set = {};
|
|
2192
|
+
for (const pair of positional) {
|
|
2193
|
+
const eq = pair.indexOf("=");
|
|
2194
|
+
if (eq <= 0)
|
|
2195
|
+
throw usageError(`expected KEY=value, got "${pair}"`);
|
|
2196
|
+
const value = pair.slice(eq + 1);
|
|
2197
|
+
set[pair.slice(0, eq)] = value === "-" ? await readStdin() : value;
|
|
2198
|
+
}
|
|
2199
|
+
printUpdate(await api(envPath(project, env, "/secrets"), { method: "PATCH", body: JSON.stringify({ set, message }) }), env);
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
case "unset": {
|
|
2203
|
+
if (positional.length === 0)
|
|
2204
|
+
throw usageError("mybase secrets unset KEY \u2026");
|
|
2205
|
+
const { project, env } = await target(flags);
|
|
2206
|
+
printUpdate(await api(envPath(project, env, "/secrets"), { method: "PATCH", body: JSON.stringify({ unset: positional, message }) }), env);
|
|
2207
|
+
return true;
|
|
2208
|
+
}
|
|
2209
|
+
case "upload": {
|
|
2210
|
+
const file = Bun.file(need(positional[0], ".env file"));
|
|
2211
|
+
if (!await file.exists())
|
|
2212
|
+
throw usageError(`no file ${positional[0]}`);
|
|
2213
|
+
const set = parseDotenv(await file.text());
|
|
2214
|
+
if (Object.keys(set).length === 0)
|
|
2215
|
+
throw usageError(`no KEY=value lines in ${positional[0]}`);
|
|
2216
|
+
const { project, env } = await target(flags);
|
|
2217
|
+
printUpdate(await api(envPath(project, env, "/secrets"), { method: "PATCH", body: JSON.stringify({ set, message: message ?? `uploaded ${positional[0]}` }) }), env);
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
case "history": {
|
|
2221
|
+
const { project, env } = await target(flags);
|
|
2222
|
+
const revisions = await api(`${envPath(project, env, "/revisions")}?limit=${flagInt(flags, "limit") ?? 20}`);
|
|
2223
|
+
print(revisions, () => {
|
|
2224
|
+
if (revisions.length === 0)
|
|
2225
|
+
console.log(`No changes in "${env}" yet.`);
|
|
2226
|
+
for (const r of revisions) {
|
|
2227
|
+
const changes = r.changes.map((c) => `${c.action === "added" ? "+" : c.action === "removed" ? "-" : "~"}${c.key}`).join(" ");
|
|
2228
|
+
console.log([`#${r.revision}`, r.createdAt, r.actor, r.message ?? "", changes].join("\t"));
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
return true;
|
|
2232
|
+
}
|
|
2233
|
+
case "restore": {
|
|
2234
|
+
const revision = need(positional[0], "revision");
|
|
2235
|
+
const { project, env } = await target(flags);
|
|
2236
|
+
confirm(yes, `This puts the values of "${env}" back as they were after revision ${revision} (as a new revision; linked credentials stay).`, revision, "the revision");
|
|
2237
|
+
printUpdate(await api(envPath(project, env, `/revisions/${encodeURIComponent(revision)}/restore`), { method: "POST" }), env);
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
case "link": {
|
|
2241
|
+
const { project, env } = await target(flags);
|
|
2242
|
+
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");
|
|
2244
|
+
const result = await api(`${base7(project)}/link`, { method: "POST", body: JSON.stringify({ envs }) });
|
|
2245
|
+
print(result, () => {
|
|
2246
|
+
console.log(`Rotated and stored in ${result.envs.join(", ")}: ${result.keys.join(", ")}`);
|
|
2247
|
+
console.log("Restart your apps with `mybase run` to pick them up.");
|
|
2248
|
+
});
|
|
2249
|
+
return true;
|
|
2250
|
+
}
|
|
2251
|
+
case "unlink": {
|
|
2252
|
+
const { project, env } = await target(flags);
|
|
2253
|
+
confirm(yes, `This removes the project credentials from "${env}". They stay valid; nothing is rotated.`, env, "the env name");
|
|
2254
|
+
await api(`${base7(project)}/link/${encodeURIComponent(env)}`, { method: "DELETE" });
|
|
2255
|
+
print({ unlinked: env, project }, () => console.log(`Removed the credentials from "${env}"`));
|
|
2256
|
+
return true;
|
|
2257
|
+
}
|
|
2258
|
+
default:
|
|
2259
|
+
return false;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/run.ts
|
|
2264
|
+
var runUsage = ` run [-p <project>] [-e <env>] [--preserve-env] -- <command> [args\u2026]
|
|
2265
|
+
Start <command> with the env's secrets as env vars
|
|
2266
|
+
(--preserve-env: vars already set win over secrets)`;
|
|
2267
|
+
async function run(args) {
|
|
2268
|
+
const { options, command } = splitRunArgs(args);
|
|
2269
|
+
if (command.length === 0)
|
|
2270
|
+
throw usageError(`missing command
|
|
2271
|
+
|
|
2272
|
+
${runUsage}`);
|
|
2273
|
+
const { flags } = parseArgs(expandShortFlags(options));
|
|
2274
|
+
const { project, env } = await target(flags);
|
|
2275
|
+
const { secrets } = await fetchSecrets(project, env);
|
|
2276
|
+
const vars = flags.has("preserve-env") ? { ...secrets, ...process.env } : { ...process.env, ...secrets };
|
|
2277
|
+
vars.MYBASE_PROJECT ??= project;
|
|
2278
|
+
vars.MYBASE_ENV = env;
|
|
2279
|
+
let proc;
|
|
2280
|
+
try {
|
|
2281
|
+
proc = Bun.spawn(command, { env: vars, stdio: ["inherit", "inherit", "inherit"] });
|
|
2282
|
+
} catch {
|
|
2283
|
+
throw new CliError(`command not found: ${command[0]}`, 127);
|
|
2284
|
+
}
|
|
2285
|
+
process.on("SIGINT", () => {});
|
|
2286
|
+
for (const signal of ["SIGTERM", "SIGHUP"])
|
|
2287
|
+
process.on(signal, () => proc.kill(signal));
|
|
2288
|
+
const code = await proc.exited;
|
|
2289
|
+
const signal = proc.signalCode;
|
|
2290
|
+
process.exit(signal ? 128 + (constants.signals[signal] ?? 0) : code);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// src/search.ts
|
|
2294
|
+
var searchUsage = ` search enable <project> Turn on web search (SearXNG) for apps and agents
|
|
2295
|
+
search disable <project> [--yes] Turn it off, clearing cached searches and pages`;
|
|
2296
|
+
var servicePath = (project) => `/projects/${encodeURIComponent(project)}/services/search`;
|
|
2297
|
+
async function search(sub, args) {
|
|
2298
|
+
const { positional, flags } = parseArgs(args);
|
|
2299
|
+
const [project] = positional;
|
|
2300
|
+
switch (sub) {
|
|
2301
|
+
case "enable": {
|
|
2302
|
+
const p = need(project, "project name");
|
|
2303
|
+
await api(servicePath(p), { method: "PUT" });
|
|
2304
|
+
const searchUrl = `${API_URL}/search/${p}`;
|
|
2305
|
+
print({ enabled: "search", project: p, searchUrl, fetchUrl: `${searchUrl}/fetch` }, () => {
|
|
2306
|
+
console.log(`Web search enabled for "${p}"
|
|
2307
|
+
`);
|
|
2308
|
+
console.log(` search URL ${searchUrl}?q=\u2026`);
|
|
2309
|
+
console.log(` fetch URL ${searchUrl}/fetch?url=\u2026
|
|
2310
|
+
`);
|
|
2311
|
+
console.log("In your app (@mybase-dev/sdk), with an API key that has search:read for this project:");
|
|
2312
|
+
console.log(` mybase token create ${p}-agent --project ${p} --perm search:read
|
|
2313
|
+
`);
|
|
2314
|
+
console.log(` const web = createSearchClient({ apiUrl: "${API_URL}", project: "${p}", apiKey: process.env.MYBASE_API_KEY! })`);
|
|
2315
|
+
console.log(` const { results } = await web.search("\u2026") // or searchTools(web) for an LLM agent`);
|
|
2316
|
+
});
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
case "disable": {
|
|
2320
|
+
const p = need(project, "project name");
|
|
2321
|
+
confirm(flags.has("yes"), `This turns off web search for "${p}" and clears its cached searches and pages.`, p, "the project name");
|
|
2322
|
+
await api(servicePath(p), { method: "DELETE" });
|
|
2323
|
+
print({ disabled: "search", project: p }, () => console.log(`Web search disabled for "${p}"`));
|
|
2324
|
+
return true;
|
|
2325
|
+
}
|
|
2326
|
+
default:
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// ../../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
|
|
2332
|
+
import { styleText } from "util";
|
|
2333
|
+
import { stdout, stdin } from "process";
|
|
2334
|
+
import * as l from "readline";
|
|
2335
|
+
import l__default from "readline";
|
|
2336
|
+
|
|
2337
|
+
// ../../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
|
|
2338
|
+
var getCodePointsLength = (() => {
|
|
2339
|
+
const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
|
2340
|
+
return (input) => {
|
|
2341
|
+
let surrogatePairsNr = 0;
|
|
2342
|
+
SURROGATE_PAIR_RE.lastIndex = 0;
|
|
2343
|
+
while (SURROGATE_PAIR_RE.test(input)) {
|
|
2344
|
+
surrogatePairsNr += 1;
|
|
2345
|
+
}
|
|
2346
|
+
return input.length - surrogatePairsNr;
|
|
2347
|
+
};
|
|
2348
|
+
})();
|
|
2349
|
+
var isFullWidth = (x) => {
|
|
2350
|
+
return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510;
|
|
2351
|
+
};
|
|
2352
|
+
var isWideNotCJKTNotEmoji = (x) => {
|
|
2353
|
+
return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
// ../../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js
|
|
2357
|
+
var ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
|
|
2358
|
+
var CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
|
|
2359
|
+
var CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu;
|
|
2360
|
+
var TAB_RE = /\t{1,1000}/y;
|
|
2361
|
+
var EMOJI_RE = /[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[\u{E0061}-\u{E007A}]{2}[\u{E0030}-\u{E0039}\u{E0061}-\u{E007A}]{1,3}\u{E007F}|(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})(?:\u200D(?:\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F\u20E3?))*/yu;
|
|
2362
|
+
var LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
|
|
2363
|
+
var MODIFIER_RE = /\p{M}+/gu;
|
|
2364
|
+
var NO_TRUNCATION = { limit: Infinity, ellipsis: "" };
|
|
2365
|
+
var getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {}) => {
|
|
2366
|
+
const LIMIT = truncationOptions.limit ?? Infinity;
|
|
2367
|
+
const ELLIPSIS = truncationOptions.ellipsis ?? "";
|
|
2368
|
+
const ELLIPSIS_WIDTH = truncationOptions?.ellipsisWidth ?? (ELLIPSIS ? getStringTruncatedWidth(ELLIPSIS, NO_TRUNCATION, widthOptions).width : 0);
|
|
2369
|
+
const ANSI_WIDTH = 0;
|
|
2370
|
+
const CONTROL_WIDTH = widthOptions.controlWidth ?? 0;
|
|
2371
|
+
const TAB_WIDTH = widthOptions.tabWidth ?? 8;
|
|
2372
|
+
const EMOJI_WIDTH = widthOptions.emojiWidth ?? 2;
|
|
2373
|
+
const FULL_WIDTH_WIDTH = 2;
|
|
2374
|
+
const REGULAR_WIDTH = widthOptions.regularWidth ?? 1;
|
|
2375
|
+
const WIDE_WIDTH = widthOptions.wideWidth ?? FULL_WIDTH_WIDTH;
|
|
2376
|
+
const PARSE_BLOCKS = [
|
|
2377
|
+
[LATIN_RE, REGULAR_WIDTH],
|
|
2378
|
+
[ANSI_RE, ANSI_WIDTH],
|
|
2379
|
+
[CONTROL_RE, CONTROL_WIDTH],
|
|
2380
|
+
[TAB_RE, TAB_WIDTH],
|
|
2381
|
+
[EMOJI_RE, EMOJI_WIDTH],
|
|
2382
|
+
[CJKT_WIDE_RE, WIDE_WIDTH]
|
|
2383
|
+
];
|
|
2384
|
+
let indexPrev = 0;
|
|
2385
|
+
let index = 0;
|
|
2386
|
+
let length = input.length;
|
|
2387
|
+
let lengthExtra = 0;
|
|
2388
|
+
let truncationEnabled = false;
|
|
2389
|
+
let truncationIndex = length;
|
|
2390
|
+
let truncationLimit = Math.max(0, LIMIT - ELLIPSIS_WIDTH);
|
|
2391
|
+
let unmatchedStart = 0;
|
|
2392
|
+
let unmatchedEnd = 0;
|
|
2393
|
+
let width = 0;
|
|
2394
|
+
let widthExtra = 0;
|
|
2395
|
+
outer:
|
|
2396
|
+
while (true) {
|
|
2397
|
+
if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) {
|
|
2398
|
+
const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index);
|
|
2399
|
+
lengthExtra = 0;
|
|
2400
|
+
for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
|
|
2401
|
+
const codePoint = char.codePointAt(0) || 0;
|
|
2402
|
+
if (isFullWidth(codePoint)) {
|
|
2403
|
+
widthExtra = FULL_WIDTH_WIDTH;
|
|
2404
|
+
} else if (isWideNotCJKTNotEmoji(codePoint)) {
|
|
2405
|
+
widthExtra = WIDE_WIDTH;
|
|
2406
|
+
} else {
|
|
2407
|
+
widthExtra = REGULAR_WIDTH;
|
|
2408
|
+
}
|
|
2409
|
+
if (width + widthExtra > truncationLimit) {
|
|
2410
|
+
truncationIndex = Math.min(truncationIndex, Math.max(unmatchedStart, indexPrev) + lengthExtra);
|
|
2411
|
+
}
|
|
2412
|
+
if (width + widthExtra > LIMIT) {
|
|
2413
|
+
truncationEnabled = true;
|
|
2414
|
+
break outer;
|
|
2415
|
+
}
|
|
2416
|
+
lengthExtra += char.length;
|
|
2417
|
+
width += widthExtra;
|
|
2418
|
+
}
|
|
2419
|
+
unmatchedStart = unmatchedEnd = 0;
|
|
2420
|
+
}
|
|
2421
|
+
if (index >= length) {
|
|
2422
|
+
break outer;
|
|
2423
|
+
}
|
|
2424
|
+
for (let i = 0, l = PARSE_BLOCKS.length;i < l; i++) {
|
|
2425
|
+
const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i];
|
|
2426
|
+
BLOCK_RE.lastIndex = index;
|
|
2427
|
+
if (BLOCK_RE.test(input)) {
|
|
2428
|
+
lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index;
|
|
2429
|
+
widthExtra = lengthExtra * BLOCK_WIDTH;
|
|
2430
|
+
if (width + widthExtra > truncationLimit) {
|
|
2431
|
+
truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
|
|
2432
|
+
}
|
|
2433
|
+
if (width + widthExtra > LIMIT) {
|
|
2434
|
+
truncationEnabled = true;
|
|
2435
|
+
break outer;
|
|
2436
|
+
}
|
|
2437
|
+
width += widthExtra;
|
|
2438
|
+
unmatchedStart = indexPrev;
|
|
2439
|
+
unmatchedEnd = index;
|
|
2440
|
+
index = indexPrev = BLOCK_RE.lastIndex;
|
|
2441
|
+
continue outer;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
index += 1;
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
width: truncationEnabled ? truncationLimit : width,
|
|
2448
|
+
index: truncationEnabled ? truncationIndex : length,
|
|
2449
|
+
truncated: truncationEnabled,
|
|
2450
|
+
ellipsed: truncationEnabled && LIMIT >= ELLIPSIS_WIDTH
|
|
2451
|
+
};
|
|
2452
|
+
};
|
|
2453
|
+
var dist_default = getStringTruncatedWidth;
|
|
2454
|
+
|
|
2455
|
+
// ../../node_modules/.bun/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js
|
|
2456
|
+
var NO_TRUNCATION2 = {
|
|
2457
|
+
limit: Infinity,
|
|
2458
|
+
ellipsis: "",
|
|
2459
|
+
ellipsisWidth: 0
|
|
2460
|
+
};
|
|
2461
|
+
var fastStringWidth = (input, options = {}) => {
|
|
2462
|
+
return dist_default(input, NO_TRUNCATION2, options).width;
|
|
2463
|
+
};
|
|
2464
|
+
var dist_default2 = fastStringWidth;
|
|
2465
|
+
|
|
2466
|
+
// ../../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
|
|
2467
|
+
var ESC = "\x1B";
|
|
2468
|
+
var CSI = "\x9B";
|
|
2469
|
+
var END_CODE = 39;
|
|
2470
|
+
var ANSI_ESCAPE_BELL = "\x07";
|
|
2471
|
+
var ANSI_CSI = "[";
|
|
2472
|
+
var ANSI_OSC = "]";
|
|
2473
|
+
var ANSI_SGR_TERMINATOR = "m";
|
|
2474
|
+
var ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
|
|
2475
|
+
var GROUP_REGEX = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`, "y");
|
|
2476
|
+
var getClosingCode = (openingCode) => {
|
|
2477
|
+
if (openingCode >= 30 && openingCode <= 37)
|
|
2478
|
+
return 39;
|
|
2479
|
+
if (openingCode >= 90 && openingCode <= 97)
|
|
2480
|
+
return 39;
|
|
2481
|
+
if (openingCode >= 40 && openingCode <= 47)
|
|
2482
|
+
return 49;
|
|
2483
|
+
if (openingCode >= 100 && openingCode <= 107)
|
|
2484
|
+
return 49;
|
|
2485
|
+
if (openingCode === 1 || openingCode === 2)
|
|
2486
|
+
return 22;
|
|
2487
|
+
if (openingCode === 3)
|
|
2488
|
+
return 23;
|
|
2489
|
+
if (openingCode === 4)
|
|
2490
|
+
return 24;
|
|
2491
|
+
if (openingCode === 7)
|
|
2492
|
+
return 27;
|
|
2493
|
+
if (openingCode === 8)
|
|
2494
|
+
return 28;
|
|
2495
|
+
if (openingCode === 9)
|
|
2496
|
+
return 29;
|
|
2497
|
+
if (openingCode === 0)
|
|
2498
|
+
return 0;
|
|
2499
|
+
return;
|
|
2500
|
+
};
|
|
2501
|
+
var wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
2502
|
+
var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
|
|
2503
|
+
var wrapWord = (rows, word, columns) => {
|
|
2504
|
+
const characters = word[Symbol.iterator]();
|
|
2505
|
+
let isInsideEscape = false;
|
|
2506
|
+
let isInsideLinkEscape = false;
|
|
2507
|
+
let lastRow = rows.at(-1);
|
|
2508
|
+
let visible = lastRow === undefined ? 0 : dist_default2(lastRow);
|
|
2509
|
+
let currentCharacter = characters.next();
|
|
2510
|
+
let nextCharacter = characters.next();
|
|
2511
|
+
let rawCharacterIndex = 0;
|
|
2512
|
+
while (!currentCharacter.done) {
|
|
2513
|
+
const character = currentCharacter.value;
|
|
2514
|
+
const characterLength = dist_default2(character);
|
|
2515
|
+
if (visible + characterLength <= columns) {
|
|
2516
|
+
rows[rows.length - 1] += character;
|
|
2517
|
+
} else {
|
|
2518
|
+
rows.push(character);
|
|
2519
|
+
visible = 0;
|
|
2520
|
+
}
|
|
2521
|
+
if (character === ESC || character === CSI) {
|
|
2522
|
+
isInsideEscape = true;
|
|
2523
|
+
isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
|
|
2524
|
+
}
|
|
2525
|
+
if (isInsideEscape) {
|
|
2526
|
+
if (isInsideLinkEscape) {
|
|
2527
|
+
if (character === ANSI_ESCAPE_BELL) {
|
|
2528
|
+
isInsideEscape = false;
|
|
2529
|
+
isInsideLinkEscape = false;
|
|
2530
|
+
}
|
|
2531
|
+
} else if (character === ANSI_SGR_TERMINATOR) {
|
|
2532
|
+
isInsideEscape = false;
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
visible += characterLength;
|
|
2536
|
+
if (visible === columns && !nextCharacter.done) {
|
|
2537
|
+
rows.push("");
|
|
2538
|
+
visible = 0;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
currentCharacter = nextCharacter;
|
|
2542
|
+
nextCharacter = characters.next();
|
|
2543
|
+
rawCharacterIndex += character.length;
|
|
2544
|
+
}
|
|
2545
|
+
lastRow = rows.at(-1);
|
|
2546
|
+
if (!visible && lastRow !== undefined && lastRow.length && rows.length > 1) {
|
|
2547
|
+
rows[rows.length - 2] += rows.pop();
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
var stringVisibleTrimSpacesRight = (string) => {
|
|
2551
|
+
const words = string.split(" ");
|
|
2552
|
+
let last = words.length;
|
|
2553
|
+
while (last) {
|
|
2554
|
+
if (dist_default2(words[last - 1])) {
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
last--;
|
|
2558
|
+
}
|
|
2559
|
+
if (last === words.length) {
|
|
2560
|
+
return string;
|
|
2561
|
+
}
|
|
2562
|
+
return words.slice(0, last).join(" ") + words.slice(last).join("");
|
|
2563
|
+
};
|
|
2564
|
+
var exec = (string, columns, options = {}) => {
|
|
2565
|
+
if (options.trim !== false && string.trim() === "") {
|
|
2566
|
+
return "";
|
|
2567
|
+
}
|
|
2568
|
+
let returnValue = "";
|
|
2569
|
+
let escapeCode;
|
|
2570
|
+
let escapeUrl;
|
|
2571
|
+
const words = string.split(" ");
|
|
2572
|
+
let rows = [""];
|
|
2573
|
+
let rowLength = 0;
|
|
2574
|
+
for (let index = 0;index < words.length; index++) {
|
|
2575
|
+
const word = words[index];
|
|
2576
|
+
if (options.trim !== false) {
|
|
2577
|
+
const row = rows.at(-1) ?? "";
|
|
2578
|
+
const trimmed = row.trimStart();
|
|
2579
|
+
if (row.length !== trimmed.length) {
|
|
2580
|
+
rows[rows.length - 1] = trimmed;
|
|
2581
|
+
rowLength = dist_default2(trimmed);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (index !== 0) {
|
|
2585
|
+
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
2586
|
+
rows.push("");
|
|
2587
|
+
rowLength = 0;
|
|
2588
|
+
}
|
|
2589
|
+
if (rowLength || options.trim === false) {
|
|
2590
|
+
rows[rows.length - 1] += " ";
|
|
2591
|
+
rowLength++;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
const wordLength = dist_default2(word);
|
|
2595
|
+
if (options.hard && wordLength > columns) {
|
|
2596
|
+
const remainingColumns = columns - rowLength;
|
|
2597
|
+
const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns);
|
|
2598
|
+
const breaksStartingNextLine = Math.floor((wordLength - 1) / columns);
|
|
2599
|
+
if (breaksStartingNextLine < breaksStartingThisLine) {
|
|
2600
|
+
rows.push("");
|
|
2601
|
+
}
|
|
2602
|
+
wrapWord(rows, word, columns);
|
|
2603
|
+
rowLength = dist_default2(rows.at(-1) ?? "");
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
if (rowLength + wordLength > columns && rowLength && wordLength) {
|
|
2607
|
+
if (options.wordWrap === false && rowLength < columns) {
|
|
2608
|
+
wrapWord(rows, word, columns);
|
|
2609
|
+
rowLength = dist_default2(rows.at(-1) ?? "");
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
rows.push("");
|
|
2613
|
+
rowLength = 0;
|
|
2614
|
+
}
|
|
2615
|
+
if (rowLength + wordLength > columns && options.wordWrap === false) {
|
|
2616
|
+
wrapWord(rows, word, columns);
|
|
2617
|
+
rowLength = dist_default2(rows.at(-1) ?? "");
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
rows[rows.length - 1] += word;
|
|
2621
|
+
rowLength += wordLength;
|
|
2622
|
+
}
|
|
2623
|
+
if (options.trim !== false) {
|
|
2624
|
+
rows = rows.map((row) => stringVisibleTrimSpacesRight(row));
|
|
2625
|
+
}
|
|
2626
|
+
const preString = rows.join(`
|
|
2627
|
+
`);
|
|
2628
|
+
let inSurrogate = false;
|
|
2629
|
+
for (let i = 0;i < preString.length; i++) {
|
|
2630
|
+
const character = preString[i];
|
|
2631
|
+
returnValue += character;
|
|
2632
|
+
if (!inSurrogate) {
|
|
2633
|
+
inSurrogate = character >= "\uD800" && character <= "\uDBFF";
|
|
2634
|
+
if (inSurrogate) {
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
} else {
|
|
2638
|
+
inSurrogate = false;
|
|
2639
|
+
}
|
|
2640
|
+
if (character === ESC || character === CSI) {
|
|
2641
|
+
GROUP_REGEX.lastIndex = i + 1;
|
|
2642
|
+
const groupsResult = GROUP_REGEX.exec(preString);
|
|
2643
|
+
const groups = groupsResult?.groups;
|
|
2644
|
+
if (groups?.code !== undefined) {
|
|
2645
|
+
const code = Number.parseFloat(groups.code);
|
|
2646
|
+
escapeCode = code === END_CODE ? undefined : code;
|
|
2647
|
+
} else if (groups?.uri !== undefined) {
|
|
2648
|
+
escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (preString[i + 1] === `
|
|
2652
|
+
`) {
|
|
2653
|
+
if (escapeUrl) {
|
|
2654
|
+
returnValue += wrapAnsiHyperlink("");
|
|
2655
|
+
}
|
|
2656
|
+
const closingCode = escapeCode ? getClosingCode(escapeCode) : undefined;
|
|
2657
|
+
if (escapeCode && closingCode) {
|
|
2658
|
+
returnValue += wrapAnsiCode(closingCode);
|
|
2659
|
+
}
|
|
2660
|
+
} else if (character === `
|
|
2661
|
+
`) {
|
|
2662
|
+
if (escapeCode && getClosingCode(escapeCode)) {
|
|
2663
|
+
returnValue += wrapAnsiCode(escapeCode);
|
|
2664
|
+
}
|
|
2665
|
+
if (escapeUrl) {
|
|
2666
|
+
returnValue += wrapAnsiHyperlink(escapeUrl);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return returnValue;
|
|
2671
|
+
};
|
|
2672
|
+
var CRLF_OR_LF = /\r?\n/;
|
|
2673
|
+
function wrapAnsi(string, columns, options) {
|
|
2674
|
+
return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join(`
|
|
2675
|
+
`);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// ../../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
|
|
2679
|
+
var import_sisteransi = __toESM(require_src(), 1);
|
|
2680
|
+
import { ReadStream } from "tty";
|
|
2681
|
+
function findCursor(s, o, l) {
|
|
2682
|
+
if (!l.some((r) => !r.disabled))
|
|
2683
|
+
return s;
|
|
2684
|
+
const t = s + o, n = Math.max(l.length - 1, 0), e = t < 0 ? n : t > n ? 0 : t;
|
|
2685
|
+
return l[e]?.disabled ? findCursor(e, o < 0 ? -1 : 1, l) : e;
|
|
2686
|
+
}
|
|
2687
|
+
var a$1 = ["up", "down", "left", "right", "space", "enter", "cancel"];
|
|
2688
|
+
var t = [
|
|
2689
|
+
"January",
|
|
2690
|
+
"February",
|
|
2691
|
+
"March",
|
|
2692
|
+
"April",
|
|
2693
|
+
"May",
|
|
2694
|
+
"June",
|
|
2695
|
+
"July",
|
|
2696
|
+
"August",
|
|
2697
|
+
"September",
|
|
2698
|
+
"October",
|
|
2699
|
+
"November",
|
|
2700
|
+
"December"
|
|
2701
|
+
];
|
|
2702
|
+
var settings = {
|
|
2703
|
+
actions: new Set(a$1),
|
|
2704
|
+
aliases: /* @__PURE__ */ new Map([
|
|
2705
|
+
["k", "up"],
|
|
2706
|
+
["j", "down"],
|
|
2707
|
+
["h", "left"],
|
|
2708
|
+
["l", "right"],
|
|
2709
|
+
["\x03", "cancel"],
|
|
2710
|
+
["escape", "cancel"]
|
|
2711
|
+
]),
|
|
2712
|
+
messages: {
|
|
2713
|
+
cancel: "Canceled",
|
|
2714
|
+
error: "Something went wrong"
|
|
2715
|
+
},
|
|
2716
|
+
withGuide: true,
|
|
2717
|
+
accessible: undefined,
|
|
2718
|
+
date: {
|
|
2719
|
+
monthNames: [...t],
|
|
2720
|
+
messages: {
|
|
2721
|
+
required: "Please enter a valid date",
|
|
2722
|
+
invalidMonth: "There are only 12 months in a year",
|
|
2723
|
+
invalidDay: (n, e) => `There are only ${n} days in ${e}`,
|
|
2724
|
+
afterMin: (n) => `Date must be on or after ${n.toISOString().slice(0, 10)}`,
|
|
2725
|
+
beforeMax: (n) => `Date must be on or before ${n.toISOString().slice(0, 10)}`
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
function isAccessible(n) {
|
|
2730
|
+
if (n !== undefined)
|
|
2731
|
+
return n;
|
|
2732
|
+
if (settings.accessible !== undefined)
|
|
2733
|
+
return settings.accessible;
|
|
2734
|
+
const e = process.env.ACCESSIBLE;
|
|
2735
|
+
return e !== undefined && e !== "" && e !== "0" && e !== "false";
|
|
2736
|
+
}
|
|
2737
|
+
function isActionKey(n, e) {
|
|
2738
|
+
if (typeof n == "string")
|
|
2739
|
+
return settings.aliases.get(n) === e;
|
|
2740
|
+
for (const s of n)
|
|
2741
|
+
if (s !== undefined && isActionKey(s, e))
|
|
2742
|
+
return true;
|
|
2743
|
+
return false;
|
|
2744
|
+
}
|
|
2745
|
+
function diffLines(i, s) {
|
|
2746
|
+
if (i === s)
|
|
2747
|
+
return;
|
|
2748
|
+
const e = i.split(`
|
|
2749
|
+
`), t = s.split(`
|
|
2750
|
+
`), r = Math.max(e.length, t.length), f = [];
|
|
2751
|
+
for (let n = 0;n < r; n++)
|
|
2752
|
+
e[n] !== t[n] && f.push(n);
|
|
2753
|
+
return {
|
|
2754
|
+
lines: f,
|
|
2755
|
+
numLinesBefore: e.length,
|
|
2756
|
+
numLinesAfter: t.length,
|
|
2757
|
+
numLines: r
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
var R = globalThis.process.platform.startsWith("win");
|
|
2761
|
+
var CANCEL_SYMBOL = Symbol("clack:cancel");
|
|
2762
|
+
function isCancel(e) {
|
|
2763
|
+
return e === CANCEL_SYMBOL;
|
|
2764
|
+
}
|
|
2765
|
+
function setRawMode(e, r) {
|
|
2766
|
+
const o = e;
|
|
2767
|
+
o.isTTY && o.setRawMode(r);
|
|
2768
|
+
}
|
|
2769
|
+
function block({
|
|
2770
|
+
input: e = stdin,
|
|
2771
|
+
output: r = stdout,
|
|
2772
|
+
overwrite: o = true,
|
|
2773
|
+
hideCursor: n = true
|
|
2774
|
+
} = {}) {
|
|
2775
|
+
const s = l.createInterface({
|
|
2776
|
+
input: e,
|
|
2777
|
+
output: r,
|
|
2778
|
+
prompt: "",
|
|
2779
|
+
tabSize: 1
|
|
2780
|
+
});
|
|
2781
|
+
l.emitKeypressEvents(e, s), e instanceof ReadStream && e.isTTY && e.setRawMode(true);
|
|
2782
|
+
const t = (f, { name: a, sequence: w }) => {
|
|
2783
|
+
const c = String(f);
|
|
2784
|
+
if (isActionKey([c, a, w], "cancel")) {
|
|
2785
|
+
n && r.write(import_sisteransi.cursor.show), process.exit(0);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
if (!o)
|
|
2789
|
+
return;
|
|
2790
|
+
const i = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0;
|
|
2791
|
+
l.moveCursor(r, i, m, () => {
|
|
2792
|
+
l.clearLine(r, 1, () => {
|
|
2793
|
+
e.once("keypress", t);
|
|
2794
|
+
});
|
|
2795
|
+
});
|
|
2796
|
+
};
|
|
2797
|
+
return n && r.write(import_sisteransi.cursor.hide), e.once("keypress", t), () => {
|
|
2798
|
+
e.off("keypress", t), n && r.write(import_sisteransi.cursor.show), e instanceof ReadStream && e.isTTY && !R && e.setRawMode(false), s.terminal = false, s.close();
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
var getColumns = (e) => ("columns" in e) && typeof e.columns == "number" ? e.columns : 80;
|
|
2802
|
+
var getRows = (e) => ("rows" in e) && typeof e.rows == "number" ? e.rows : 20;
|
|
2803
|
+
function wrapTextWithPrefix(e, r, o, n = o, s = o, t) {
|
|
2804
|
+
const f = getColumns(e ?? stdout);
|
|
2805
|
+
return wrapAnsi(r, f - o.length, {
|
|
2806
|
+
hard: true,
|
|
2807
|
+
trim: false
|
|
2808
|
+
}).split(`
|
|
2809
|
+
`).map((c, i, m) => {
|
|
2810
|
+
const d = t ? t(c, i) : c;
|
|
2811
|
+
return i === 0 ? `${n}${d}` : i === m.length - 1 ? `${s}${d}` : `${o}${d}`;
|
|
2812
|
+
}).join(`
|
|
2813
|
+
`);
|
|
2814
|
+
}
|
|
2815
|
+
function runValidation(e, a) {
|
|
2816
|
+
if ("~standard" in e) {
|
|
2817
|
+
const n = e["~standard"].validate(a);
|
|
2818
|
+
return n instanceof Promise ? n.then((r) => r.issues?.at(0)?.message) : n.issues?.at(0)?.message;
|
|
2819
|
+
}
|
|
2820
|
+
return e(a);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
class y {
|
|
2824
|
+
input;
|
|
2825
|
+
output;
|
|
2826
|
+
_abortSignal;
|
|
2827
|
+
rl;
|
|
2828
|
+
opts;
|
|
2829
|
+
_render;
|
|
2830
|
+
_track = false;
|
|
2831
|
+
_prevFrame = "";
|
|
2832
|
+
_subscribers = /* @__PURE__ */ new Map;
|
|
2833
|
+
_cursor = 0;
|
|
2834
|
+
state = "initial";
|
|
2835
|
+
error = "";
|
|
2836
|
+
value;
|
|
2837
|
+
userInput = "";
|
|
2838
|
+
get accessible() {
|
|
2839
|
+
return isAccessible(this.opts.accessible);
|
|
2840
|
+
}
|
|
2841
|
+
constructor(t, e = true) {
|
|
2842
|
+
const { input: i = stdin, output: s = stdout, render: r, signal: n, ...o } = t;
|
|
2843
|
+
this.opts = o, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = r.bind(this), this._track = e, this._abortSignal = n, this.input = i, this.output = s;
|
|
2844
|
+
}
|
|
2845
|
+
unsubscribe() {
|
|
2846
|
+
this._subscribers.clear();
|
|
2847
|
+
}
|
|
2848
|
+
setSubscriber(t, e) {
|
|
2849
|
+
const i = this._subscribers.get(t) ?? [];
|
|
2850
|
+
i.push(e), this._subscribers.set(t, i);
|
|
2851
|
+
}
|
|
2852
|
+
on(t, e) {
|
|
2853
|
+
this.setSubscriber(t, { cb: e });
|
|
2854
|
+
}
|
|
2855
|
+
once(t, e) {
|
|
2856
|
+
this.setSubscriber(t, { cb: e, once: true });
|
|
2857
|
+
}
|
|
2858
|
+
emit(t, ...e) {
|
|
2859
|
+
const i = this._subscribers.get(t) ?? [], s = [];
|
|
2860
|
+
for (const r of i)
|
|
2861
|
+
r.cb(...e), r.once && s.push(() => i.splice(i.indexOf(r), 1));
|
|
2862
|
+
for (const r of s)
|
|
2863
|
+
r();
|
|
2864
|
+
}
|
|
2865
|
+
prompt() {
|
|
2866
|
+
return new Promise((t) => {
|
|
2867
|
+
if (this._abortSignal) {
|
|
2868
|
+
if (this._abortSignal.aborted)
|
|
2869
|
+
return this.state = "cancel", this.close(), t(CANCEL_SYMBOL);
|
|
2870
|
+
this._abortSignal.addEventListener("abort", () => {
|
|
2871
|
+
this.state = "cancel", this.close();
|
|
2872
|
+
}, { once: true });
|
|
2873
|
+
}
|
|
2874
|
+
this.rl = l__default.createInterface({
|
|
2875
|
+
input: this.input,
|
|
2876
|
+
tabSize: 2,
|
|
2877
|
+
prompt: "",
|
|
2878
|
+
escapeCodeTimeout: 50,
|
|
2879
|
+
terminal: true
|
|
2880
|
+
}), this.rl.prompt(), this.opts.initialUserInput !== undefined && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
|
|
2881
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
|
|
2882
|
+
}), this.once("cancel", () => {
|
|
2883
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
|
|
2884
|
+
});
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
_isActionKey(t, e) {
|
|
2888
|
+
return t === "\t";
|
|
2889
|
+
}
|
|
2890
|
+
_shouldSubmit(t, e) {
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
2893
|
+
_setValue(t) {
|
|
2894
|
+
this.value = t, this.emit("value", this.value);
|
|
2895
|
+
}
|
|
2896
|
+
_setUserInput(t, e) {
|
|
2897
|
+
this.userInput = t ?? "", this.emit("userInput", this.userInput), e && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor);
|
|
2898
|
+
}
|
|
2899
|
+
_clearUserInput() {
|
|
2900
|
+
this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput("");
|
|
2901
|
+
}
|
|
2902
|
+
async onKeypress(t, e) {
|
|
2903
|
+
if (this.state !== "validating") {
|
|
2904
|
+
if (this._track && e.name !== "return" && (e.name && this._isActionKey(t, e) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), e?.name && (!this._track && settings.aliases.has(e.name) && this.emit("cursor", settings.aliases.get(e.name)), settings.actions.has(e.name) && this.emit("cursor", e.name)), t && (t.toLowerCase() === "y" || t.toLowerCase() === "n") && this.emit("confirm", t.toLowerCase() === "y"), this.emit("key", t, e), e?.name === "return" && this._shouldSubmit(t, e)) {
|
|
2905
|
+
if (this.opts.validate) {
|
|
2906
|
+
const i = runValidation(this.opts.validate, this.value);
|
|
2907
|
+
let s;
|
|
2908
|
+
i instanceof Promise ? (this.state = "validating", this.render(), s = await i) : s = i, s && (this.error = s instanceof Error ? s.message : s, this.state = "error", this.rl?.write(this.userInput));
|
|
2909
|
+
}
|
|
2910
|
+
this.state !== "error" && (this.state = "submit");
|
|
2911
|
+
}
|
|
2912
|
+
isActionKey([t, e?.name, e?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
close() {
|
|
2916
|
+
this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
|
|
2917
|
+
`), setRawMode(this.input, false), this.rl?.close(), this.rl = undefined, this.emit(`${this.state}`, this.value), this.unsubscribe();
|
|
2918
|
+
}
|
|
2919
|
+
restoreCursor() {
|
|
2920
|
+
const t = wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(`
|
|
2921
|
+
`).length - 1;
|
|
2922
|
+
this.output.write(import_sisteransi.cursor.move(-999, t * -1));
|
|
2923
|
+
}
|
|
2924
|
+
render() {
|
|
2925
|
+
const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
|
|
2926
|
+
hard: true,
|
|
2927
|
+
trim: false
|
|
2928
|
+
});
|
|
2929
|
+
if (t !== this._prevFrame) {
|
|
2930
|
+
if (this.state === "initial")
|
|
2931
|
+
this.output.write(import_sisteransi.cursor.hide);
|
|
2932
|
+
else {
|
|
2933
|
+
const e = diffLines(this._prevFrame, t), i = getRows(this.output);
|
|
2934
|
+
if (this.restoreCursor(), e) {
|
|
2935
|
+
const s = Math.max(0, e.numLinesAfter - i), r = Math.max(0, e.numLinesBefore - i);
|
|
2936
|
+
let n = e.lines.find((o) => o >= s);
|
|
2937
|
+
if (n === undefined) {
|
|
2938
|
+
this._prevFrame = t;
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (e.lines.length === 1) {
|
|
2942
|
+
this.output.write(import_sisteransi.cursor.move(0, n - r)), this.output.write(import_sisteransi.erase.lines(1));
|
|
2943
|
+
const o = t.split(`
|
|
2944
|
+
`);
|
|
2945
|
+
this.output.write(o[n]), this._prevFrame = t, this.output.write(import_sisteransi.cursor.move(0, o.length - n - 1));
|
|
2946
|
+
return;
|
|
2947
|
+
} else if (e.lines.length > 1) {
|
|
2948
|
+
if (s < r)
|
|
2949
|
+
n = s;
|
|
2950
|
+
else {
|
|
2951
|
+
const h = n - r;
|
|
2952
|
+
h > 0 && this.output.write(import_sisteransi.cursor.move(0, h));
|
|
2953
|
+
}
|
|
2954
|
+
this.output.write(import_sisteransi.erase.down());
|
|
2955
|
+
const f = t.split(`
|
|
2956
|
+
`).slice(n);
|
|
2957
|
+
this.output.write(f.join(`
|
|
2958
|
+
`)), this._prevFrame = t;
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
this.output.write(import_sisteransi.erase.down());
|
|
2963
|
+
}
|
|
2964
|
+
this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
class r extends y {
|
|
2969
|
+
get cursor() {
|
|
2970
|
+
return this.value ? 0 : 1;
|
|
2971
|
+
}
|
|
2972
|
+
get _value() {
|
|
2973
|
+
return this.cursor === 0;
|
|
2974
|
+
}
|
|
2975
|
+
constructor(t) {
|
|
2976
|
+
super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
|
|
2977
|
+
this.value = this._value;
|
|
2978
|
+
}), this.on("confirm", (i) => {
|
|
2979
|
+
this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
|
|
2980
|
+
}), this.on("cursor", () => {
|
|
2981
|
+
this.value = !this.value;
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
class a extends y {
|
|
2986
|
+
options;
|
|
2987
|
+
cursor = 0;
|
|
2988
|
+
get _value() {
|
|
2989
|
+
return this.options[this.cursor]?.value;
|
|
2990
|
+
}
|
|
2991
|
+
get _enabledOptions() {
|
|
2992
|
+
return this.options.filter((e) => e.disabled !== true);
|
|
2993
|
+
}
|
|
2994
|
+
toggleAll() {
|
|
2995
|
+
const e = this._enabledOptions, i = this.value !== undefined && this.value.length === e.length;
|
|
2996
|
+
this.value = i ? [] : e.map((t) => t.value);
|
|
2997
|
+
}
|
|
2998
|
+
toggleInvert() {
|
|
2999
|
+
const e = this.value;
|
|
3000
|
+
if (!e)
|
|
3001
|
+
return;
|
|
3002
|
+
const i = this._enabledOptions.filter((t) => !e.includes(t.value));
|
|
3003
|
+
this.value = i.map((t) => t.value);
|
|
3004
|
+
}
|
|
3005
|
+
toggleValue() {
|
|
3006
|
+
this.value === undefined && (this.value = []);
|
|
3007
|
+
const e = this.value.includes(this._value);
|
|
3008
|
+
this.value = e ? this.value.filter((i) => i !== this._value) : [...this.value, this._value];
|
|
3009
|
+
}
|
|
3010
|
+
constructor(e) {
|
|
3011
|
+
super(e, false), this.options = e.options, this.value = [...e.initialValues ?? []];
|
|
3012
|
+
const i = Math.max(this.options.findIndex(({ value: t }) => t === e.cursorAt), 0);
|
|
3013
|
+
this.cursor = this.options[i]?.disabled ? findCursor(i, 1, this.options) : i, this.on("key", (t, l) => {
|
|
3014
|
+
l.name === "a" && this.toggleAll(), l.name === "i" && this.toggleInvert();
|
|
3015
|
+
}), this.on("cursor", (t) => {
|
|
3016
|
+
switch (t) {
|
|
3017
|
+
case "left":
|
|
3018
|
+
case "up":
|
|
3019
|
+
this.cursor = findCursor(this.cursor, -1, this.options);
|
|
3020
|
+
break;
|
|
3021
|
+
case "down":
|
|
3022
|
+
case "right":
|
|
3023
|
+
this.cursor = findCursor(this.cursor, 1, this.options);
|
|
3024
|
+
break;
|
|
3025
|
+
case "space":
|
|
3026
|
+
this.toggleValue();
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
var u$1 = class u extends y {
|
|
3033
|
+
_mask = "\u2022";
|
|
3034
|
+
get cursor() {
|
|
3035
|
+
return this._cursor;
|
|
3036
|
+
}
|
|
3037
|
+
get masked() {
|
|
3038
|
+
return this.userInput.replaceAll(/./g, this._mask);
|
|
3039
|
+
}
|
|
3040
|
+
get userInputWithCursor() {
|
|
3041
|
+
if (this.state === "submit" || this.state === "cancel")
|
|
3042
|
+
return this.masked;
|
|
3043
|
+
const t = this.userInput;
|
|
3044
|
+
if (this.cursor >= t.length)
|
|
3045
|
+
return `${this.masked}${styleText(["inverse", "hidden"], "_")}`;
|
|
3046
|
+
const s = this.masked, r = s.slice(0, this.cursor), i = s.slice(this.cursor, this.cursor + 1), o = s.slice(this.cursor + 1);
|
|
3047
|
+
return `${r}${styleText("inverse", i)}${o}`;
|
|
3048
|
+
}
|
|
3049
|
+
clear() {
|
|
3050
|
+
this._clearUserInput();
|
|
3051
|
+
}
|
|
3052
|
+
constructor({ mask: t, ...s }) {
|
|
3053
|
+
super(s), this._mask = t ?? "\u2022", this.on("userInput", (r) => {
|
|
3054
|
+
this._setValue(r);
|
|
3055
|
+
}), this.on("finalize", () => {
|
|
3056
|
+
this.value === undefined && (this.value = "");
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
var n$1 = class n extends y {
|
|
3061
|
+
options;
|
|
3062
|
+
cursor = 0;
|
|
3063
|
+
get _selectedValue() {
|
|
3064
|
+
return this.options[this.cursor];
|
|
3065
|
+
}
|
|
3066
|
+
changeValue() {
|
|
3067
|
+
const e = this._selectedValue;
|
|
3068
|
+
this.value = e === undefined ? undefined : e.value;
|
|
3069
|
+
}
|
|
3070
|
+
constructor(e) {
|
|
3071
|
+
super(e, false), this.options = e.options;
|
|
3072
|
+
const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
|
|
3073
|
+
this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
|
|
3074
|
+
switch (s) {
|
|
3075
|
+
case "left":
|
|
3076
|
+
case "up":
|
|
3077
|
+
this.cursor = findCursor(this.cursor, -1, this.options);
|
|
3078
|
+
break;
|
|
3079
|
+
case "down":
|
|
3080
|
+
case "right":
|
|
3081
|
+
this.cursor = findCursor(this.cursor, 1, this.options);
|
|
3082
|
+
break;
|
|
3083
|
+
}
|
|
3084
|
+
this.changeValue();
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
class n extends y {
|
|
3089
|
+
get userInputWithCursor() {
|
|
3090
|
+
if (this.state === "submit")
|
|
3091
|
+
return this.userInput;
|
|
3092
|
+
const t = this.userInput;
|
|
3093
|
+
if (this.cursor >= t.length)
|
|
3094
|
+
return `${this.userInput}\u2588`;
|
|
3095
|
+
const r = t.slice(0, this.cursor), s = t.slice(this.cursor, this.cursor + 1), e = t.slice(this.cursor + 1);
|
|
3096
|
+
return `${r}${styleText("inverse", s)}${e}`;
|
|
3097
|
+
}
|
|
3098
|
+
get cursor() {
|
|
3099
|
+
return this._cursor;
|
|
3100
|
+
}
|
|
3101
|
+
constructor(t) {
|
|
3102
|
+
super({
|
|
3103
|
+
...t,
|
|
3104
|
+
initialUserInput: t.initialUserInput ?? t.initialValue
|
|
3105
|
+
}), this.on("userInput", (r) => {
|
|
3106
|
+
this._setValue(r);
|
|
3107
|
+
}), this.on("finalize", () => {
|
|
3108
|
+
this.value || (this.value = t.defaultValue), this.value === undefined && (this.value = "");
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// ../../node_modules/.bun/@clack+prompts@1.8.1/node_modules/@clack/prompts/dist/index.mjs
|
|
3114
|
+
import { styleText as styleText2, stripVTControlCharacters } from "util";
|
|
3115
|
+
import process$1 from "process";
|
|
3116
|
+
var import_sisteransi2 = __toESM(require_src(), 1);
|
|
3117
|
+
function isUnicodeSupported() {
|
|
3118
|
+
if (process$1.platform !== "win32") {
|
|
3119
|
+
return process$1.env.TERM !== "linux";
|
|
3120
|
+
}
|
|
3121
|
+
return Boolean(process$1.env.CI) || Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
3122
|
+
}
|
|
3123
|
+
var unicode = isUnicodeSupported();
|
|
3124
|
+
var isCI = () => process.env.CI === "true";
|
|
3125
|
+
var unicodeOr = (o, e) => unicode ? o : e;
|
|
3126
|
+
var S_STEP_ACTIVE = unicodeOr("\u25C6", "*");
|
|
3127
|
+
var S_STEP_CANCEL = unicodeOr("\u25A0", "x");
|
|
3128
|
+
var S_STEP_ERROR = unicodeOr("\u25B2", "x");
|
|
3129
|
+
var S_STEP_SUBMIT = unicodeOr("\u25C7", "o");
|
|
3130
|
+
var S_BAR_START = unicodeOr("\u250C", "T");
|
|
3131
|
+
var S_BAR = unicodeOr("\u2502", "|");
|
|
3132
|
+
var S_BAR_END = unicodeOr("\u2514", "\u2014");
|
|
3133
|
+
var S_BAR_START_RIGHT = unicodeOr("\u2510", "T");
|
|
3134
|
+
var S_BAR_END_RIGHT = unicodeOr("\u2518", "\u2014");
|
|
3135
|
+
var S_RADIO_ACTIVE = unicodeOr("\u25CF", ">");
|
|
3136
|
+
var S_RADIO_INACTIVE = unicodeOr("\u25CB", " ");
|
|
3137
|
+
var S_CHECKBOX_ACTIVE = unicodeOr("\u25FB", "[\u2022]");
|
|
3138
|
+
var S_CHECKBOX_SELECTED = unicodeOr("\u25FC", "[+]");
|
|
3139
|
+
var S_CHECKBOX_INACTIVE = unicodeOr("\u25FB", "[ ]");
|
|
3140
|
+
var S_PASSWORD_MASK = unicodeOr("\u25AA", "\u2022");
|
|
3141
|
+
var S_BAR_H = unicodeOr("\u2500", "-");
|
|
3142
|
+
var S_CORNER_TOP_RIGHT = unicodeOr("\u256E", "+");
|
|
3143
|
+
var S_CONNECT_LEFT = unicodeOr("\u251C", "+");
|
|
3144
|
+
var S_CORNER_BOTTOM_RIGHT = unicodeOr("\u256F", "+");
|
|
3145
|
+
var S_CORNER_BOTTOM_LEFT = unicodeOr("\u2570", "+");
|
|
3146
|
+
var S_CORNER_TOP_LEFT = unicodeOr("\u256D", "+");
|
|
3147
|
+
var S_INFO = unicodeOr("\u25CF", "\u2022");
|
|
3148
|
+
var S_SUCCESS = unicodeOr("\u25C6", "*");
|
|
3149
|
+
var S_WARN = unicodeOr("\u25B2", "!");
|
|
3150
|
+
var S_ERROR = unicodeOr("\u25A0", "x");
|
|
3151
|
+
var symbol = (o) => {
|
|
3152
|
+
switch (o) {
|
|
3153
|
+
case "initial":
|
|
3154
|
+
case "active":
|
|
3155
|
+
return styleText2("cyan", S_STEP_ACTIVE);
|
|
3156
|
+
case "cancel":
|
|
3157
|
+
return styleText2("red", S_STEP_CANCEL);
|
|
3158
|
+
case "error":
|
|
3159
|
+
return styleText2("yellow", S_STEP_ERROR);
|
|
3160
|
+
case "submit":
|
|
3161
|
+
return styleText2("green", S_STEP_SUBMIT);
|
|
3162
|
+
case "validating":
|
|
3163
|
+
return styleText2("dim", S_STEP_ACTIVE);
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
var symbolBar = (o) => {
|
|
3167
|
+
switch (o) {
|
|
3168
|
+
case "initial":
|
|
3169
|
+
case "active":
|
|
3170
|
+
return styleText2("cyan", S_BAR);
|
|
3171
|
+
case "cancel":
|
|
3172
|
+
return styleText2("red", S_BAR);
|
|
3173
|
+
case "error":
|
|
3174
|
+
return styleText2("yellow", S_BAR);
|
|
3175
|
+
case "submit":
|
|
3176
|
+
return styleText2("green", S_BAR);
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
function formatInstructionFooter(o, e) {
|
|
3180
|
+
const r = [`${e ? `${styleText2("cyan", S_BAR)} ` : ""}${o.join(" \u2022 ")}`];
|
|
3181
|
+
return e && r.push(styleText2("cyan", S_BAR_END)), r;
|
|
3182
|
+
}
|
|
3183
|
+
var I = (l, e, w, p, b, C = false) => {
|
|
3184
|
+
let r = e, O = 0;
|
|
3185
|
+
if (C)
|
|
3186
|
+
for (let i = p - 1;i >= w; i--) {
|
|
3187
|
+
const m = l[i];
|
|
3188
|
+
if (m && (r -= m.length), O++, r <= b)
|
|
3189
|
+
break;
|
|
3190
|
+
}
|
|
3191
|
+
else
|
|
3192
|
+
for (let i = w;i < p; i++) {
|
|
3193
|
+
const m = l[i];
|
|
3194
|
+
if (m && (r -= m.length), O++, r <= b)
|
|
3195
|
+
break;
|
|
3196
|
+
}
|
|
3197
|
+
return { lineCount: r, removals: O };
|
|
3198
|
+
};
|
|
3199
|
+
var limitOptions = ({
|
|
3200
|
+
cursor: l,
|
|
3201
|
+
options: e,
|
|
3202
|
+
style: w,
|
|
3203
|
+
output: p = process.stdout,
|
|
3204
|
+
maxItems: b = Number.POSITIVE_INFINITY,
|
|
3205
|
+
columnPadding: C = 0,
|
|
3206
|
+
rowPadding: r = 4
|
|
3207
|
+
}) => {
|
|
3208
|
+
const i = getColumns(p) - C, m = getRows(p), M = styleText2("dim", "..."), v = Math.max(m - r, 0), a = Math.max(Math.min(b, v), 5);
|
|
3209
|
+
let f = 0;
|
|
3210
|
+
l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
|
|
3211
|
+
let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
|
|
3212
|
+
const W = Math.min(f + a, e.length), s = [];
|
|
3213
|
+
let g = 0;
|
|
3214
|
+
d && g++, c && g++;
|
|
3215
|
+
const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
|
|
3216
|
+
for (let t = T;t < y; t++) {
|
|
3217
|
+
const n = e[t], o = n ? w(n, t === l) : "", h = wrapAnsi(o, i, {
|
|
3218
|
+
hard: true,
|
|
3219
|
+
trim: false
|
|
3220
|
+
}).split(`
|
|
3221
|
+
`);
|
|
3222
|
+
s.push(h), g += h.length;
|
|
3223
|
+
}
|
|
3224
|
+
if (g > v) {
|
|
3225
|
+
let t = 0, n = 0, o = g;
|
|
3226
|
+
const h = l - T;
|
|
3227
|
+
let u = v;
|
|
3228
|
+
const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
|
|
3229
|
+
d ? ({ lineCount: o, removals: t } = L(), o > u && (c || (u -= 1), { lineCount: o, removals: n } = E())) : (c || (u -= 1), { lineCount: o, removals: n } = E(), o > u && (u -= 1, { lineCount: o, removals: t } = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
|
|
3230
|
+
}
|
|
3231
|
+
const x = [];
|
|
3232
|
+
d && x.push(M);
|
|
3233
|
+
for (const t of s)
|
|
3234
|
+
for (const n of t)
|
|
3235
|
+
x.push(n);
|
|
3236
|
+
return c && x.push(M), x;
|
|
3237
|
+
};
|
|
3238
|
+
var confirm2 = (e) => {
|
|
3239
|
+
const a = e.active ?? "Yes", o = e.inactive ?? "No";
|
|
3240
|
+
return new r({
|
|
3241
|
+
active: a,
|
|
3242
|
+
inactive: o,
|
|
3243
|
+
signal: e.signal,
|
|
3244
|
+
input: e.input,
|
|
3245
|
+
output: e.output,
|
|
3246
|
+
initialValue: e.initialValue ?? true,
|
|
3247
|
+
render() {
|
|
3248
|
+
const i = e.withGuide ?? settings.withGuide, u = `${symbol(this.state)} `, l = i ? `${styleText2("gray", S_BAR)} ` : "", f = wrapTextWithPrefix(e.output, e.message, l, u), s = `${i ? `${styleText2("gray", S_BAR)}
|
|
3249
|
+
` : ""}${f}
|
|
3250
|
+
`, c = this.value ? a : o;
|
|
3251
|
+
switch (this.state) {
|
|
3252
|
+
case "submit": {
|
|
3253
|
+
const r = i ? `${styleText2("gray", S_BAR)} ` : "";
|
|
3254
|
+
return `${s}${r}${styleText2("dim", c)}`;
|
|
3255
|
+
}
|
|
3256
|
+
case "cancel": {
|
|
3257
|
+
const r = i ? `${styleText2("gray", S_BAR)} ` : "";
|
|
3258
|
+
return `${s}${r}${styleText2(["strikethrough", "dim"], c)}${i ? `
|
|
3259
|
+
${styleText2("gray", S_BAR)}` : ""}`;
|
|
3260
|
+
}
|
|
3261
|
+
default: {
|
|
3262
|
+
const r = i ? `${styleText2("cyan", S_BAR)} ` : "", g = i ? styleText2("cyan", S_BAR_END) : "";
|
|
3263
|
+
return `${s}${r}${this.value ? `${styleText2("green", S_RADIO_ACTIVE)} ${a}` : `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a)}`}${e.vertical ? i ? `
|
|
3264
|
+
${styleText2("cyan", S_BAR)} ` : `
|
|
3265
|
+
` : ` ${styleText2("dim", "/")} `}${this.value ? `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", o)}` : `${styleText2("green", S_RADIO_ACTIVE)} ${o}`}
|
|
3266
|
+
${g}
|
|
3267
|
+
`;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}).prompt();
|
|
3272
|
+
};
|
|
3273
|
+
var MULTISELECT_INSTRUCTIONS = [
|
|
3274
|
+
`${styleText2("dim", "\u2191/\u2193")} to navigate`,
|
|
3275
|
+
`${styleText2("dim", "Space:")} select`,
|
|
3276
|
+
`${styleText2("dim", "Enter:")} confirm`
|
|
3277
|
+
];
|
|
3278
|
+
var m = (i, u) => i.split(`
|
|
3279
|
+
`).map((d) => u(d)).join(`
|
|
3280
|
+
`);
|
|
3281
|
+
var multiselect = (i) => {
|
|
3282
|
+
const u = (t, a) => {
|
|
3283
|
+
const r = t.label ?? String(t.value);
|
|
3284
|
+
return a === "disabled" ? `${styleText2("gray", S_CHECKBOX_INACTIVE)} ${m(r, (o) => styleText2(["strikethrough", "gray"], o))}${t.hint ? ` ${styleText2("dim", `(${t.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${styleText2("cyan", S_CHECKBOX_ACTIVE)} ${r}${t.hint ? ` ${styleText2("dim", `(${t.hint})`)}` : ""}` : a === "selected" ? `${styleText2("green", S_CHECKBOX_SELECTED)} ${m(r, (o) => styleText2("dim", o))}${t.hint ? ` ${styleText2("dim", `(${t.hint})`)}` : ""}` : a === "cancelled" ? `${m(r, (o) => styleText2(["strikethrough", "dim"], o))}` : a === "active-selected" ? `${styleText2("green", S_CHECKBOX_SELECTED)} ${r}${t.hint ? ` ${styleText2("dim", `(${t.hint})`)}` : ""}` : a === "submitted" ? `${m(r, (o) => styleText2("dim", o))}` : `${styleText2("dim", S_CHECKBOX_INACTIVE)} ${m(r, (o) => styleText2("dim", o))}`;
|
|
3285
|
+
}, d = i.required ?? true, x = i.showInstructions ?? true;
|
|
3286
|
+
return new a({
|
|
3287
|
+
options: i.options,
|
|
3288
|
+
signal: i.signal,
|
|
3289
|
+
input: i.input,
|
|
3290
|
+
output: i.output,
|
|
3291
|
+
initialValues: i.initialValues,
|
|
3292
|
+
required: d,
|
|
3293
|
+
cursorAt: i.cursorAt,
|
|
3294
|
+
validate(t) {
|
|
3295
|
+
if (d && (t === undefined || t.length === 0))
|
|
3296
|
+
return `Please select at least one option.
|
|
3297
|
+
${styleText2("reset", styleText2("dim", `Press ${styleText2(["gray", "bgWhite", "inverse"], " space ")} to select, ${styleText2("gray", styleText2("bgWhite", styleText2("inverse", " enter ")))} to submit`))}`;
|
|
3298
|
+
},
|
|
3299
|
+
render() {
|
|
3300
|
+
const t = i.withGuide ?? settings.withGuide, a = wrapTextWithPrefix(i.output, i.message, t ? `${symbolBar(this.state)} ` : "", `${symbol(this.state)} `), r = `${t ? `${styleText2("gray", S_BAR)}
|
|
3301
|
+
` : ""}${a}
|
|
3302
|
+
`, o = this.value ?? [], g = (n, l) => {
|
|
3303
|
+
if (n.disabled)
|
|
3304
|
+
return u(n, "disabled");
|
|
3305
|
+
const s = o.includes(n.value);
|
|
3306
|
+
return l && s ? u(n, "active-selected") : s ? u(n, "selected") : u(n, l ? "active" : "inactive");
|
|
3307
|
+
};
|
|
3308
|
+
switch (this.state) {
|
|
3309
|
+
case "submit": {
|
|
3310
|
+
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "submitted")).join(styleText2("dim", ", ")) || styleText2("dim", "none"), l = wrapTextWithPrefix(i.output, n, t ? `${styleText2("gray", S_BAR)} ` : "");
|
|
3311
|
+
return `${r}${l}`;
|
|
3312
|
+
}
|
|
3313
|
+
case "cancel": {
|
|
3314
|
+
const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "cancelled")).join(styleText2("dim", ", "));
|
|
3315
|
+
if (n.trim() === "")
|
|
3316
|
+
return `${r}${styleText2("gray", S_BAR)}`;
|
|
3317
|
+
const l = wrapTextWithPrefix(i.output, n, t ? `${styleText2("gray", S_BAR)} ` : "");
|
|
3318
|
+
return `${r}${l}${t ? `
|
|
3319
|
+
${styleText2("gray", S_BAR)}` : ""}`;
|
|
3320
|
+
}
|
|
3321
|
+
case "error": {
|
|
3322
|
+
const n = t ? `${styleText2("yellow", S_BAR)} ` : "", l = this.error.split(`
|
|
3323
|
+
`).map(($, v) => v === 0 ? `${t ? `${styleText2("yellow", S_BAR_END)} ` : ""}${styleText2("yellow", $)}` : ` ${$}`).join(`
|
|
3324
|
+
`), s = r.split(`
|
|
3325
|
+
`).length, h = l.split(`
|
|
3326
|
+
`).length + 1;
|
|
3327
|
+
return `${r}${n}${limitOptions({
|
|
3328
|
+
output: i.output,
|
|
3329
|
+
options: this.options,
|
|
3330
|
+
cursor: this.cursor,
|
|
3331
|
+
maxItems: i.maxItems,
|
|
3332
|
+
columnPadding: n.length,
|
|
3333
|
+
rowPadding: s + h,
|
|
3334
|
+
style: g
|
|
3335
|
+
}).join(`
|
|
3336
|
+
${n}`)}
|
|
3337
|
+
${l}
|
|
3338
|
+
`;
|
|
3339
|
+
}
|
|
3340
|
+
default: {
|
|
3341
|
+
const n = t ? `${styleText2("cyan", S_BAR)} ` : "", l = r.split(`
|
|
3342
|
+
`).length, s = x ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS, t) : t ? [styleText2("cyan", S_BAR_END)] : [], h = s.join(`
|
|
3343
|
+
`), $ = s.length + 1;
|
|
3344
|
+
return `${r}${n}${limitOptions({
|
|
3345
|
+
output: i.output,
|
|
3346
|
+
options: this.options,
|
|
3347
|
+
cursor: this.cursor,
|
|
3348
|
+
maxItems: i.maxItems,
|
|
3349
|
+
columnPadding: n.length,
|
|
3350
|
+
rowPadding: l + $,
|
|
3351
|
+
style: g
|
|
3352
|
+
}).join(`
|
|
3353
|
+
${n}`)}
|
|
3354
|
+
${h}
|
|
3355
|
+
`;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}).prompt();
|
|
3360
|
+
};
|
|
3361
|
+
var log = {
|
|
3362
|
+
message: (s = [], {
|
|
3363
|
+
symbol: e = styleText2("gray", S_BAR),
|
|
3364
|
+
secondarySymbol: r = styleText2("gray", S_BAR),
|
|
3365
|
+
output: m = process.stdout,
|
|
3366
|
+
spacing: l = 1,
|
|
3367
|
+
withGuide: c
|
|
3368
|
+
} = {}) => {
|
|
3369
|
+
const t = [], o = c ?? settings.withGuide, f = o ? r : "", O = o ? `${e} ` : "", u = o ? `${r} ` : "";
|
|
3370
|
+
for (let i = 0;i < l; i++)
|
|
3371
|
+
t.push(f);
|
|
3372
|
+
const g = Array.isArray(s) ? s : s.split(`
|
|
3373
|
+
`);
|
|
3374
|
+
if (g.length > 0) {
|
|
3375
|
+
const [i, ...y] = g;
|
|
3376
|
+
i.length > 0 ? t.push(`${O}${i}`) : t.push(o ? e : "");
|
|
3377
|
+
for (const p of y)
|
|
3378
|
+
p.length > 0 ? t.push(`${u}${p}`) : t.push(o ? r : "");
|
|
3379
|
+
}
|
|
3380
|
+
m.write(`${t.join(`
|
|
3381
|
+
`)}
|
|
3382
|
+
`);
|
|
3383
|
+
},
|
|
3384
|
+
info: (s, e) => {
|
|
3385
|
+
log.message(s, { ...e, symbol: styleText2("blue", S_INFO) });
|
|
3386
|
+
},
|
|
3387
|
+
success: (s, e) => {
|
|
3388
|
+
log.message(s, { ...e, symbol: styleText2("green", S_SUCCESS) });
|
|
3389
|
+
},
|
|
3390
|
+
step: (s, e) => {
|
|
3391
|
+
log.message(s, { ...e, symbol: styleText2("green", S_STEP_SUBMIT) });
|
|
3392
|
+
},
|
|
3393
|
+
warn: (s, e) => {
|
|
3394
|
+
log.message(s, { ...e, symbol: styleText2("yellow", S_WARN) });
|
|
3395
|
+
},
|
|
3396
|
+
warning: (s, e) => {
|
|
3397
|
+
log.warn(s, e);
|
|
3398
|
+
},
|
|
3399
|
+
error: (s, e) => {
|
|
3400
|
+
log.message(s, { ...e, symbol: styleText2("red", S_ERROR) });
|
|
3401
|
+
}
|
|
3402
|
+
};
|
|
3403
|
+
var cancel = (o = "", t) => {
|
|
3404
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR_END)} ` : "";
|
|
3405
|
+
i.write(`${e}${styleText2("red", o)}
|
|
3406
|
+
|
|
3407
|
+
`);
|
|
3408
|
+
};
|
|
3409
|
+
var intro = (o = "", t) => {
|
|
3410
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR_START)} ` : "";
|
|
3411
|
+
i.write(`${e}${o}
|
|
3412
|
+
`);
|
|
3413
|
+
};
|
|
3414
|
+
var outro = (o = "", t) => {
|
|
3415
|
+
const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR)}
|
|
3416
|
+
${styleText2("gray", S_BAR_END)} ` : "";
|
|
3417
|
+
i.write(`${e}${o}
|
|
3418
|
+
|
|
3419
|
+
`);
|
|
3420
|
+
};
|
|
3421
|
+
var W$1 = (o) => o;
|
|
3422
|
+
var C = (o, e, s) => {
|
|
3423
|
+
const a = {
|
|
3424
|
+
hard: true,
|
|
3425
|
+
trim: false
|
|
3426
|
+
}, i = wrapAnsi(o, e, a).split(`
|
|
3427
|
+
`), c = i.reduce((n, t) => Math.max(dist_default2(t), n), 0), u = i.map(s).reduce((n, t) => Math.max(dist_default2(t), n), 0), g = e - (u - c);
|
|
3428
|
+
return wrapAnsi(o, g, a);
|
|
3429
|
+
};
|
|
3430
|
+
var note = (o = "", e = "", s) => {
|
|
3431
|
+
const a = s?.output ?? process$1.stdout, i = s?.withGuide ?? settings.withGuide, c = s?.format ?? W$1, g = ["", ...C(o, getColumns(a) - 6, c).split(`
|
|
3432
|
+
`).map(c), ""], n = dist_default2(e), t = Math.max(g.reduce((m, F) => {
|
|
3433
|
+
const O = dist_default2(F);
|
|
3434
|
+
return O > m ? O : m;
|
|
3435
|
+
}, 0), n) + 2, h = g.map((m) => `${styleText2("gray", S_BAR)} ${m}${" ".repeat(t - dist_default2(m))}${styleText2("gray", S_BAR)}`).join(`
|
|
3436
|
+
`), T = i ? `${styleText2("gray", S_BAR)}
|
|
3437
|
+
` : "", l$1 = i ? S_CONNECT_LEFT : S_CORNER_BOTTOM_LEFT;
|
|
3438
|
+
a.write(`${T}${styleText2("green", S_STEP_SUBMIT)} ${styleText2("reset", e)} ${styleText2("gray", S_BAR_H.repeat(Math.max(t - n - 1, 1)) + S_CORNER_TOP_RIGHT)}
|
|
3439
|
+
${h}
|
|
3440
|
+
${styleText2("gray", l$1 + S_BAR_H.repeat(t + 2) + S_CORNER_BOTTOM_RIGHT)}
|
|
3441
|
+
`);
|
|
3442
|
+
};
|
|
3443
|
+
var password = (e) => new u$1({
|
|
3444
|
+
validate: e.validate,
|
|
3445
|
+
mask: e.mask ?? S_PASSWORD_MASK,
|
|
3446
|
+
signal: e.signal,
|
|
3447
|
+
input: e.input,
|
|
3448
|
+
output: e.output,
|
|
3449
|
+
render() {
|
|
3450
|
+
const r = e.withGuide ?? settings.withGuide, o = `${r ? `${styleText2("gray", S_BAR)}
|
|
3451
|
+
` : ""}${symbol(this.state)} ${e.message}
|
|
3452
|
+
`, m = this.userInputWithCursor, i = this.masked;
|
|
3453
|
+
switch (this.state) {
|
|
3454
|
+
case "error": {
|
|
3455
|
+
const s = r ? `${styleText2("yellow", S_BAR)} ` : "", n = r ? `${styleText2("yellow", S_BAR_END)} ` : "", d = i ?? "";
|
|
3456
|
+
return e.clearOnError && this.clear(), `${o.trim()}
|
|
3457
|
+
${s}${d}
|
|
3458
|
+
${n}${styleText2("yellow", this.error)}
|
|
3459
|
+
`;
|
|
3460
|
+
}
|
|
3461
|
+
case "submit": {
|
|
3462
|
+
const s = r ? `${styleText2("gray", S_BAR)} ` : "", n = i ? styleText2("dim", i) : "";
|
|
3463
|
+
return `${o}${s}${n}`;
|
|
3464
|
+
}
|
|
3465
|
+
case "cancel": {
|
|
3466
|
+
const s = r ? `${styleText2("gray", S_BAR)} ` : "", n = i ? styleText2(["strikethrough", "dim"], i) : "";
|
|
3467
|
+
return `${o}${s}${n}${i && r ? `
|
|
3468
|
+
${styleText2("gray", S_BAR)}` : ""}`;
|
|
3469
|
+
}
|
|
3470
|
+
default: {
|
|
3471
|
+
const s = r ? `${styleText2("cyan", S_BAR)} ` : "", n = r ? styleText2("cyan", S_BAR_END) : "";
|
|
3472
|
+
return `${o}${s}${m}
|
|
3473
|
+
${n}
|
|
3474
|
+
`;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}).prompt();
|
|
3479
|
+
var W = (l) => styleText2("magenta", l);
|
|
3480
|
+
var spinner = ({
|
|
3481
|
+
indicator: l = "dots",
|
|
3482
|
+
onCancel: h,
|
|
3483
|
+
output: n = process.stdout,
|
|
3484
|
+
cancelMessage: G,
|
|
3485
|
+
errorMessage: O,
|
|
3486
|
+
frames: E = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"],
|
|
3487
|
+
delay: F = unicode ? 80 : 120,
|
|
3488
|
+
signal: m,
|
|
3489
|
+
...I
|
|
3490
|
+
} = {}) => {
|
|
3491
|
+
const u = isCI();
|
|
3492
|
+
let M, T, d = false, S = false, s = "", p, w = performance.now();
|
|
3493
|
+
const x = getColumns(n), k = I?.styleFrame ?? W, g = (e) => {
|
|
3494
|
+
const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
|
|
3495
|
+
S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
|
|
3496
|
+
}, f = () => g(2), i = () => g(1), A = () => {
|
|
3497
|
+
process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
|
|
3498
|
+
}, H = () => {
|
|
3499
|
+
process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
|
|
3500
|
+
}, y = () => {
|
|
3501
|
+
if (p === undefined)
|
|
3502
|
+
return;
|
|
3503
|
+
u && n.write(`
|
|
3504
|
+
`);
|
|
3505
|
+
const r = wrapAnsi(p, x, {
|
|
3506
|
+
hard: true,
|
|
3507
|
+
trim: false
|
|
3508
|
+
}).split(`
|
|
3509
|
+
`);
|
|
3510
|
+
r.length > 1 && n.write(import_sisteransi2.cursor.up(r.length - 1)), n.write(import_sisteransi2.cursor.to(0)), n.write(import_sisteransi2.erase.down());
|
|
3511
|
+
}, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
|
|
3512
|
+
const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
|
|
3513
|
+
return t > 0 ? `[${t}m ${o}s]` : `[${o}s]`;
|
|
3514
|
+
}, N = I.withGuide ?? settings.withGuide, P = (e = "") => {
|
|
3515
|
+
d = true, M = block({ output: n }), s = C(e), w = performance.now(), N && n.write(`${styleText2("gray", S_BAR)}
|
|
3516
|
+
`);
|
|
3517
|
+
let r = 0, t = 0;
|
|
3518
|
+
A(), T = setInterval(() => {
|
|
3519
|
+
if (u && s === p)
|
|
3520
|
+
return;
|
|
3521
|
+
y(), p = s;
|
|
3522
|
+
const o = k(E[r]);
|
|
3523
|
+
let v;
|
|
3524
|
+
if (u)
|
|
3525
|
+
v = `${o} ${s}...`;
|
|
3526
|
+
else if (l === "timer")
|
|
3527
|
+
v = `${o} ${s} ${_(w)}`;
|
|
3528
|
+
else {
|
|
3529
|
+
const B = ".".repeat(Math.floor(t)).slice(0, 3);
|
|
3530
|
+
v = `${o} ${s}${B}`;
|
|
3531
|
+
}
|
|
3532
|
+
const j = wrapAnsi(v, x, {
|
|
3533
|
+
hard: true,
|
|
3534
|
+
trim: false
|
|
3535
|
+
});
|
|
3536
|
+
n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
|
|
3537
|
+
}, F);
|
|
3538
|
+
}, a = (e = "", r = 0, t = false) => {
|
|
3539
|
+
if (!d)
|
|
3540
|
+
return;
|
|
3541
|
+
d = false, clearInterval(T), y();
|
|
3542
|
+
const o = r === 0 ? styleText2("green", S_STEP_SUBMIT) : r === 1 ? styleText2("red", S_STEP_CANCEL) : styleText2("red", S_STEP_ERROR);
|
|
3543
|
+
s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
|
|
3544
|
+
`) : n.write(`${o} ${s}
|
|
3545
|
+
`)), H(), M();
|
|
3546
|
+
};
|
|
3547
|
+
return {
|
|
3548
|
+
start: P,
|
|
3549
|
+
stop: (e = "") => a(e, 0),
|
|
3550
|
+
message: (e = "") => {
|
|
3551
|
+
s = C(e ?? s);
|
|
3552
|
+
},
|
|
3553
|
+
cancel: (e = "") => a(e, 1),
|
|
3554
|
+
error: (e = "") => a(e, 2),
|
|
3555
|
+
clear: () => a("", 0, true),
|
|
3556
|
+
get isCancelled() {
|
|
3557
|
+
return S;
|
|
3558
|
+
}
|
|
3559
|
+
};
|
|
3560
|
+
};
|
|
3561
|
+
var u2 = {
|
|
3562
|
+
light: unicodeOr("\u2500", "-"),
|
|
3563
|
+
heavy: unicodeOr("\u2501", "="),
|
|
3564
|
+
block: unicodeOr("\u2588", "#")
|
|
3565
|
+
};
|
|
3566
|
+
var SELECT_INSTRUCTIONS = [
|
|
3567
|
+
`${styleText2("dim", "\u2191/\u2193")} to navigate`,
|
|
3568
|
+
`${styleText2("dim", "Enter:")} confirm`
|
|
3569
|
+
];
|
|
3570
|
+
var c = (t, o) => t.includes(`
|
|
3571
|
+
`) ? t.split(`
|
|
3572
|
+
`).map((d) => o(d)).join(`
|
|
3573
|
+
`) : o(t);
|
|
3574
|
+
var select = (t) => {
|
|
3575
|
+
const o = (n, m) => {
|
|
3576
|
+
if (n === undefined)
|
|
3577
|
+
return "";
|
|
3578
|
+
const s = n.label ?? String(n.value);
|
|
3579
|
+
switch (m) {
|
|
3580
|
+
case "disabled":
|
|
3581
|
+
return `${styleText2("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText2("gray", i))}${n.hint ? ` ${styleText2("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
|
|
3582
|
+
case "selected":
|
|
3583
|
+
return `${c(s, (i) => styleText2("dim", i))}`;
|
|
3584
|
+
case "active":
|
|
3585
|
+
return `${styleText2("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText2("dim", `(${n.hint})`)}` : ""}`;
|
|
3586
|
+
case "cancelled":
|
|
3587
|
+
return `${c(s, (i) => styleText2(["strikethrough", "dim"], i))}`;
|
|
3588
|
+
default:
|
|
3589
|
+
return `${styleText2("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText2("dim", i))}`;
|
|
3590
|
+
}
|
|
3591
|
+
}, d = t.showInstructions ?? true;
|
|
3592
|
+
return new n$1({
|
|
3593
|
+
options: t.options,
|
|
3594
|
+
signal: t.signal,
|
|
3595
|
+
input: t.input,
|
|
3596
|
+
output: t.output,
|
|
3597
|
+
initialValue: t.initialValue,
|
|
3598
|
+
render() {
|
|
3599
|
+
const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText2("gray", S_BAR)}
|
|
3600
|
+
` : ""}${i}
|
|
3601
|
+
`;
|
|
3602
|
+
switch (this.state) {
|
|
3603
|
+
case "submit": {
|
|
3604
|
+
const r = n ? `${styleText2("gray", S_BAR)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r);
|
|
3605
|
+
return `${u}${a}`;
|
|
3606
|
+
}
|
|
3607
|
+
case "cancel": {
|
|
3608
|
+
const r = n ? `${styleText2("gray", S_BAR)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r);
|
|
3609
|
+
return `${u}${a}${n ? `
|
|
3610
|
+
${styleText2("gray", S_BAR)}` : ""}`;
|
|
3611
|
+
}
|
|
3612
|
+
default: {
|
|
3613
|
+
const r = n ? `${styleText2("cyan", S_BAR)} ` : "", a = u.split(`
|
|
3614
|
+
`).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText2("cyan", S_BAR_END)] : [], f = p.join(`
|
|
3615
|
+
`), b = p.length + 1;
|
|
3616
|
+
return `${u}${r}${limitOptions({
|
|
3617
|
+
output: t.output,
|
|
3618
|
+
cursor: this.cursor,
|
|
3619
|
+
options: this.options,
|
|
3620
|
+
maxItems: t.maxItems,
|
|
3621
|
+
columnPadding: r.length,
|
|
3622
|
+
rowPadding: a + b,
|
|
3623
|
+
style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
|
|
3624
|
+
}).join(`
|
|
3625
|
+
${r}`)}
|
|
3626
|
+
${f}
|
|
3627
|
+
`;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}).prompt();
|
|
3632
|
+
};
|
|
3633
|
+
var i = `${styleText2("gray", S_BAR)} `;
|
|
3634
|
+
var text = (t) => new n({
|
|
3635
|
+
validate: t.validate,
|
|
3636
|
+
placeholder: t.placeholder,
|
|
3637
|
+
defaultValue: t.defaultValue,
|
|
3638
|
+
initialValue: t.initialValue,
|
|
3639
|
+
output: t.output,
|
|
3640
|
+
signal: t.signal,
|
|
3641
|
+
input: t.input,
|
|
3642
|
+
render() {
|
|
3643
|
+
const r = t?.withGuide ?? settings.withGuide, l = `${`${r ? `${styleText2("gray", S_BAR)}
|
|
3644
|
+
` : ""}${symbol(this.state)} `}${t.message}
|
|
3645
|
+
`, d = t.placeholder && t.placeholder.length > 0 ? styleText2("inverse", t.placeholder[0]) + styleText2("dim", t.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o = this.userInput ? this.userInputWithCursor : d, s = this.value ?? "";
|
|
3646
|
+
switch (this.state) {
|
|
3647
|
+
case "validating": {
|
|
3648
|
+
const n = r ? `${styleText2("cyan", S_BAR)} ` : "", i = r ? styleText2("cyan", S_BAR_END) : "", c = styleText2("dim", o), $ = styleText2("dim", "Validating...");
|
|
3649
|
+
return `${l}${n}${c}
|
|
3650
|
+
${i} ${$}
|
|
3651
|
+
`;
|
|
3652
|
+
}
|
|
3653
|
+
case "error": {
|
|
3654
|
+
const n = this.error ? ` ${styleText2("yellow", this.error)}` : "", i = r ? `${styleText2("yellow", S_BAR)} ` : "", c = r ? styleText2("yellow", S_BAR_END) : "";
|
|
3655
|
+
return `${l.trim()}
|
|
3656
|
+
${i}${o}
|
|
3657
|
+
${c}${n}
|
|
3658
|
+
`;
|
|
3659
|
+
}
|
|
3660
|
+
case "submit": {
|
|
3661
|
+
const n = s ? `${r ? " " : ""}${styleText2("dim", s)}` : "", i = r ? styleText2("gray", S_BAR) : "";
|
|
3662
|
+
return `${l}${i}${n}`;
|
|
3663
|
+
}
|
|
3664
|
+
case "cancel": {
|
|
3665
|
+
const n = s ? ` ${styleText2(["strikethrough", "dim"], s)}` : "", i = r ? styleText2("gray", S_BAR) : "";
|
|
3666
|
+
return `${l}${i}${n}${s.trim() ? `
|
|
3667
|
+
${i}` : ""}`;
|
|
3668
|
+
}
|
|
3669
|
+
default: {
|
|
3670
|
+
const n = r ? `${styleText2("cyan", S_BAR)} ` : "", i = r ? styleText2("cyan", S_BAR_END) : "";
|
|
3671
|
+
return `${l}${n}${o}
|
|
3672
|
+
${i}
|
|
3673
|
+
`;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
}).prompt();
|
|
3678
|
+
|
|
3679
|
+
// src/server.ts
|
|
3680
|
+
import { join as join4 } from "path";
|
|
3681
|
+
import { styleText as styleText4 } from "util";
|
|
3682
|
+
|
|
3683
|
+
// src/server-docker.ts
|
|
3684
|
+
import { existsSync, readFileSync } from "fs";
|
|
3685
|
+
import { statfs } from "fs/promises";
|
|
3686
|
+
import { freemem, hostname as hostname2, networkInterfaces, totalmem } from "os";
|
|
3687
|
+
|
|
3688
|
+
// ../../docker/compose.yml
|
|
3689
|
+
var compose_default = `# Runs mybase: the control plane (API + dashboard), the shared Postgres server, Garage
|
|
3690
|
+
# (S3-compatible storage), Valkey (Redis-compatible cache), and SearXNG (web search). All images are multi-arch (arm64 + amd64).
|
|
3691
|
+
# Needs Docker Compose 2.23.1 or newer (for the inline Garage config below).
|
|
3692
|
+
#
|
|
3693
|
+
# Easiest: \`mybase server setup\` writes this file and its .env for you (keys, network, tunnel, start on boot):
|
|
3694
|
+
# curl -fsSL https://raw.githubusercontent.com/LandonHarter/mybase/master/scripts/install.sh | sh -s -- --setup
|
|
3695
|
+
#
|
|
3696
|
+
# By hand:
|
|
3697
|
+
# curl -fsSLO https://raw.githubusercontent.com/LandonHarter/mybase/master/docker/compose.yml
|
|
3698
|
+
# echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
|
|
3699
|
+
# echo "GARAGE_RPC_SECRET=$(openssl rand -hex 32)" >> .env
|
|
3700
|
+
# echo "GARAGE_ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
|
|
3701
|
+
# echo "VALKEY_PASSWORD=$(openssl rand -hex 32)" >> .env
|
|
3702
|
+
# echo "SEARXNG_SECRET=$(openssl rand -hex 32)" >> .env
|
|
3703
|
+
# echo "MYBASE_SECRETS_KEY=$(openssl rand -base64 32)" >> .env # secrets service; back it up!
|
|
3704
|
+
# docker compose up -d
|
|
3705
|
+
#
|
|
3706
|
+
# Then open http://localhost:4000 (or set MYBASE_PORT in .env) and create the admin account.
|
|
3707
|
+
# First-run setup only works from this machine or your LAN; or set MYBASE_ADMIN_PASSWORD in .env.
|
|
3708
|
+
# Apps on other machines: set MYBASE_PUBLIC_HOST in .env to this machine's LAN/Tailscale name.
|
|
3709
|
+
# Every service restarts with Docker (on boot, after crashes); MYBASE_RESTART=no turns that off.
|
|
3710
|
+
#
|
|
3711
|
+
# Public access (Cloudflare Tunnel, no router changes; the machine only needs outbound internet).
|
|
3712
|
+
# \`mybase server tunnel\` does all of this, automatically with a Cloudflare API token. By hand:
|
|
3713
|
+
# 1. Add your domain to Cloudflare (free plan) and switch its nameservers at your registrar.
|
|
3714
|
+
# 2. Cloudflare Zero Trust \u2192 Networks \u2192 Tunnels \u2192 Create a tunnel (cloudflared). Copy its token.
|
|
3715
|
+
# 3. In the tunnel's public hostnames, add:
|
|
3716
|
+
# mybase.<domain> \u2192 http://mybase:4000 (dashboard, API, public files, Postgres over WebSocket)
|
|
3717
|
+
# s3.<domain> \u2192 http://garage:3900 (S3 for apps)
|
|
3718
|
+
# 4. In .env:
|
|
3719
|
+
# COMPOSE_PROFILES=tunnel
|
|
3720
|
+
# CLOUDFLARE_TUNNEL_TOKEN=<token>
|
|
3721
|
+
# MYBASE_PUBLIC_URL=https://mybase.<domain>
|
|
3722
|
+
# MYBASE_S3_PUBLIC_URL=https://s3.<domain>
|
|
3723
|
+
# MYBASE_TRUST_PROXY=cloudflare
|
|
3724
|
+
# 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.
|
|
3725
|
+
# 5. docker compose up -d
|
|
3726
|
+
# Apps on serverless hosts reach Postgres at wss://mybase.<domain>/db with @neondatabase/serverless,
|
|
3727
|
+
# and the cache at https://mybase.<domain>/cache/<project> (@mybase-dev/sdk/cache or @upstash/redis).
|
|
3728
|
+
# Cloudflare's free plan caps request bodies at 100 MB: bigger dashboard uploads only work on the LAN.
|
|
3729
|
+
name: mybase
|
|
3730
|
+
|
|
3731
|
+
services:
|
|
3732
|
+
mybase:
|
|
3733
|
+
image: \${MYBASE_IMAGE:-ghcr.io/landonharter/mybase:latest}
|
|
3734
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3735
|
+
depends_on:
|
|
3736
|
+
postgres:
|
|
3737
|
+
condition: service_healthy
|
|
3738
|
+
# Garage's image has no shell for a healthcheck; the API waits for it on startup.
|
|
3739
|
+
garage:
|
|
3740
|
+
condition: service_started
|
|
3741
|
+
valkey:
|
|
3742
|
+
condition: service_healthy
|
|
3743
|
+
searxng:
|
|
3744
|
+
condition: service_started
|
|
3745
|
+
environment:
|
|
3746
|
+
POSTGRES_HOST: postgres
|
|
3747
|
+
POSTGRES_PORT: 5432
|
|
3748
|
+
POSTGRES_USER: \${POSTGRES_USER:-mybase}
|
|
3749
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
|
3750
|
+
# Where apps reach Postgres. Shown in each project's connection string.
|
|
3751
|
+
MYBASE_DB_PUBLIC_HOST: \${MYBASE_DB_PUBLIC_HOST:-\${MYBASE_PUBLIC_HOST:-localhost}}
|
|
3752
|
+
MYBASE_DB_PUBLIC_PORT: \${POSTGRES_PORT:-5432}
|
|
3753
|
+
GARAGE_ADMIN_URL: http://garage:3903
|
|
3754
|
+
GARAGE_ADMIN_TOKEN: \${GARAGE_ADMIN_TOKEN:?set GARAGE_ADMIN_TOKEN in .env}
|
|
3755
|
+
GARAGE_S3_URL: http://garage:3900
|
|
3756
|
+
GARAGE_CAPACITY: \${GARAGE_CAPACITY:-100G}
|
|
3757
|
+
# Where apps reach storage (S3 endpoint) and public files (served by this API).
|
|
3758
|
+
MYBASE_S3_PUBLIC_URL: \${MYBASE_S3_PUBLIC_URL:-http://\${MYBASE_PUBLIC_HOST:-localhost}:\${GARAGE_S3_PORT:-3900}}
|
|
3759
|
+
MYBASE_PUBLIC_URL: \${MYBASE_PUBLIC_URL:-http://\${MYBASE_PUBLIC_HOST:-localhost}:\${MYBASE_PORT:-4000}}
|
|
3760
|
+
# Postgres over WebSocket, for apps that can't reach the Postgres port. Defaults to <MYBASE_PUBLIC_URL>/db.
|
|
3761
|
+
MYBASE_DB_WS_URL: \${MYBASE_DB_WS_URL:-}
|
|
3762
|
+
MYBASE_MAX_UPLOAD: \${MYBASE_MAX_UPLOAD:-1073741824}
|
|
3763
|
+
VALKEY_HOST: valkey
|
|
3764
|
+
VALKEY_PORT: 6379
|
|
3765
|
+
VALKEY_PASSWORD: \${VALKEY_PASSWORD:?set VALKEY_PASSWORD in .env}
|
|
3766
|
+
# Where apps reach the cache over TCP. Shown in each project's redis:// URL.
|
|
3767
|
+
MYBASE_CACHE_PUBLIC_HOST: \${MYBASE_CACHE_PUBLIC_HOST:-\${MYBASE_PUBLIC_HOST:-localhost}}
|
|
3768
|
+
MYBASE_CACHE_PUBLIC_PORT: \${VALKEY_PORT:-6379}
|
|
3769
|
+
SEARXNG_URL: http://searxng:8080
|
|
3770
|
+
# Encrypts stored secrets (secrets service; it answers 503 without it). Keep a copy outside
|
|
3771
|
+
# this machine: if it's lost, stored secrets can't be recovered. To change it, move the old
|
|
3772
|
+
# one to MYBASE_SECRETS_KEY_OLD for one restart.
|
|
3773
|
+
MYBASE_SECRETS_KEY: \${MYBASE_SECRETS_KEY:-}
|
|
3774
|
+
MYBASE_SECRETS_KEY_OLD: \${MYBASE_SECRETS_KEY_OLD:-}
|
|
3775
|
+
# \`cloudflare\` when behind the tunnel: trusts CF-Connecting-IP for client IPs (login throttling).
|
|
3776
|
+
MYBASE_TRUST_PROXY: \${MYBASE_TRUST_PROXY:-}
|
|
3777
|
+
# Creates the admin account on first boot instead of the setup page. Ignored once it exists.
|
|
3778
|
+
MYBASE_ADMIN_USERNAME: \${MYBASE_ADMIN_USERNAME:-admin}
|
|
3779
|
+
MYBASE_ADMIN_PASSWORD: \${MYBASE_ADMIN_PASSWORD:-}
|
|
3780
|
+
# 1 for one restart turns the admin's 2FA off (lost phone and recovery codes).
|
|
3781
|
+
MYBASE_ADMIN_RESET_MFA: \${MYBASE_ADMIN_RESET_MFA:-}
|
|
3782
|
+
LOG_LEVEL: \${LOG_LEVEL:-info}
|
|
3783
|
+
ports:
|
|
3784
|
+
- "\${MYBASE_BIND:-0.0.0.0}:\${MYBASE_PORT:-4000}:4000"
|
|
3785
|
+
|
|
3786
|
+
# Official Postgres plus extensions projects can enable (see postgres.Dockerfile).
|
|
3787
|
+
# Upgrading from postgres:17-alpine: the data volume is compatible, but text sort order
|
|
3788
|
+
# changes (musl \u2192 glibc), so rebuild text indexes once, for each database:
|
|
3789
|
+
# docker compose exec postgres psql -U mybase -d <db> -c 'reindex database <db>'
|
|
3790
|
+
postgres:
|
|
3791
|
+
image: \${MYBASE_POSTGRES_IMAGE:-ghcr.io/landonharter/mybase-postgres:17}
|
|
3792
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3793
|
+
environment:
|
|
3794
|
+
POSTGRES_USER: \${POSTGRES_USER:-mybase}
|
|
3795
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
|
3796
|
+
# Apps connect to their project databases directly on this port.
|
|
3797
|
+
ports:
|
|
3798
|
+
- "\${POSTGRES_BIND:-0.0.0.0}:\${POSTGRES_PORT:-5432}:5432"
|
|
3799
|
+
volumes:
|
|
3800
|
+
- postgres-data:/var/lib/postgresql/data
|
|
3801
|
+
healthcheck:
|
|
3802
|
+
test: ["CMD-SHELL", "pg_isready -U $\${POSTGRES_USER}"]
|
|
3803
|
+
interval: 10s
|
|
3804
|
+
timeout: 5s
|
|
3805
|
+
retries: 5
|
|
3806
|
+
|
|
3807
|
+
# S3-compatible object storage: one bucket + key per project.
|
|
3808
|
+
garage:
|
|
3809
|
+
image: dxflrs/garage:v2.4.1
|
|
3810
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3811
|
+
environment:
|
|
3812
|
+
GARAGE_RPC_SECRET: \${GARAGE_RPC_SECRET:?set GARAGE_RPC_SECRET in .env}
|
|
3813
|
+
GARAGE_ADMIN_TOKEN: \${GARAGE_ADMIN_TOKEN:?set GARAGE_ADMIN_TOKEN in .env}
|
|
3814
|
+
configs:
|
|
3815
|
+
- source: garage
|
|
3816
|
+
target: /etc/garage.toml
|
|
3817
|
+
# Apps use their bucket directly on this port. The admin API (3903) stays internal.
|
|
3818
|
+
ports:
|
|
3819
|
+
- "\${GARAGE_S3_BIND:-0.0.0.0}:\${GARAGE_S3_PORT:-3900}:3900"
|
|
3820
|
+
volumes:
|
|
3821
|
+
- garage-meta:/var/lib/garage/meta
|
|
3822
|
+
- garage-data:/var/lib/garage/data
|
|
3823
|
+
|
|
3824
|
+
# Redis-compatible cache: an ACL user per project (cache service), plus rate limit counters.
|
|
3825
|
+
# RAM only up to CACHE_MAXMEMORY, least recently used keys evicted first. Snapshots to disk at
|
|
3826
|
+
# most every 15 minutes (and on shutdown), to spare SD cards.
|
|
3827
|
+
valkey:
|
|
3828
|
+
image: valkey/valkey:8.1-alpine
|
|
3829
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3830
|
+
environment:
|
|
3831
|
+
VALKEY_PASSWORD: \${VALKEY_PASSWORD:?set VALKEY_PASSWORD in .env}
|
|
3832
|
+
command: >-
|
|
3833
|
+
valkey-server --requirepass \${VALKEY_PASSWORD:?set VALKEY_PASSWORD in .env} --maxmemory \${CACHE_MAXMEMORY:-64mb}
|
|
3834
|
+
--maxmemory-policy allkeys-lru --save "900 1" --appendonly no
|
|
3835
|
+
# Apps use the cache directly on this port (over the LAN/Tailscale), as their project's user.
|
|
3836
|
+
ports:
|
|
3837
|
+
- "\${VALKEY_BIND:-0.0.0.0}:\${VALKEY_PORT:-6379}:6379"
|
|
3838
|
+
volumes:
|
|
3839
|
+
- valkey-data:/data
|
|
3840
|
+
healthcheck:
|
|
3841
|
+
test: ["CMD-SHELL", "REDISCLI_AUTH=$$VALKEY_PASSWORD valkey-cli ping | grep -q PONG"]
|
|
3842
|
+
interval: 10s
|
|
3843
|
+
timeout: 5s
|
|
3844
|
+
retries: 5
|
|
3845
|
+
|
|
3846
|
+
# Web search (search service): metasearch over Bing, Mojeek, Yahoo, DuckDuckGo, Brave, and a few
|
|
3847
|
+
# topic engines. Queries go out from this machine's IP; engines that rate-limit it are suspended
|
|
3848
|
+
# for a while and the rest keep answering. Only the API talks to it.
|
|
3849
|
+
searxng:
|
|
3850
|
+
image: searxng/searxng:2026.10.11-ad46ebd7d
|
|
3851
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3852
|
+
environment:
|
|
3853
|
+
SEARXNG_SECRET: \${SEARXNG_SECRET:?set SEARXNG_SECRET in .env}
|
|
3854
|
+
GRANIAN_WORKERS: 1
|
|
3855
|
+
GRANIAN_BLOCKING_THREADS: 4
|
|
3856
|
+
configs:
|
|
3857
|
+
- source: searxng
|
|
3858
|
+
target: /etc/searxng/settings.yml
|
|
3859
|
+
|
|
3860
|
+
# Cloudflare Tunnel: public HTTPS without opening router ports. Off unless COMPOSE_PROFILES=tunnel.
|
|
3861
|
+
# Hostnames are set up in the Cloudflare dashboard (see the top of this file).
|
|
3862
|
+
cloudflared:
|
|
3863
|
+
image: cloudflare/cloudflared:2026.10.4
|
|
3864
|
+
profiles: [tunnel]
|
|
3865
|
+
restart: \${MYBASE_RESTART:-unless-stopped}
|
|
3866
|
+
command: tunnel --no-autoupdate run
|
|
3867
|
+
environment:
|
|
3868
|
+
TUNNEL_TOKEN: \${CLOUDFLARE_TUNNEL_TOKEN:-}
|
|
3869
|
+
depends_on:
|
|
3870
|
+
- mybase
|
|
3871
|
+
|
|
3872
|
+
configs:
|
|
3873
|
+
garage:
|
|
3874
|
+
content: |
|
|
3875
|
+
metadata_dir = "/var/lib/garage/meta"
|
|
3876
|
+
data_dir = "/var/lib/garage/data"
|
|
3877
|
+
db_engine = "lmdb"
|
|
3878
|
+
replication_factor = 1
|
|
3879
|
+
rpc_bind_addr = "[::]:3901"
|
|
3880
|
+
rpc_public_addr = "127.0.0.1:3901"
|
|
3881
|
+
|
|
3882
|
+
[s3_api]
|
|
3883
|
+
s3_region = "garage"
|
|
3884
|
+
api_bind_addr = "[::]:3900"
|
|
3885
|
+
|
|
3886
|
+
[admin]
|
|
3887
|
+
api_bind_addr = "[::]:3903"
|
|
3888
|
+
|
|
3889
|
+
searxng:
|
|
3890
|
+
content: |
|
|
3891
|
+
use_default_settings:
|
|
3892
|
+
engines:
|
|
3893
|
+
keep_only: [bing, mojeek, yahoo, duckduckgo, brave, wikipedia, github, stackoverflow, duckduckgo news, bing news]
|
|
3894
|
+
search:
|
|
3895
|
+
formats: [html, json]
|
|
3896
|
+
server:
|
|
3897
|
+
limiter: false
|
|
3898
|
+
image_proxy: false
|
|
3899
|
+
outgoing:
|
|
3900
|
+
request_timeout: 3.0
|
|
3901
|
+
max_request_timeout: 6.0
|
|
3902
|
+
engines:
|
|
3903
|
+
- name: bing
|
|
3904
|
+
disabled: false
|
|
3905
|
+
- name: mojeek
|
|
3906
|
+
inactive: false
|
|
3907
|
+
- name: yahoo
|
|
3908
|
+
disabled: false
|
|
3909
|
+
|
|
3910
|
+
volumes:
|
|
3911
|
+
postgres-data:
|
|
3912
|
+
garage-meta:
|
|
3913
|
+
garage-data:
|
|
3914
|
+
valkey-data:
|
|
3915
|
+
`;
|
|
3916
|
+
|
|
3917
|
+
// src/server-env.ts
|
|
3918
|
+
var SECRET_KEYS = {
|
|
3919
|
+
POSTGRES_PASSWORD: { bytes: 24, encoding: "hex" },
|
|
3920
|
+
GARAGE_RPC_SECRET: { bytes: 32, encoding: "hex" },
|
|
3921
|
+
GARAGE_ADMIN_TOKEN: { bytes: 32, encoding: "hex" },
|
|
3922
|
+
VALKEY_PASSWORD: { bytes: 32, encoding: "hex" },
|
|
3923
|
+
SEARXNG_SECRET: { bytes: 32, encoding: "hex" },
|
|
3924
|
+
MYBASE_SECRETS_KEY: { bytes: 32, encoding: "base64" }
|
|
3925
|
+
};
|
|
3926
|
+
var SECTIONS = [
|
|
3927
|
+
{
|
|
3928
|
+
title: "Secrets (generated by `mybase server setup`). Changing one breaks the data it protects.\n# Back up MYBASE_SECRETS_KEY somewhere other than this machine: without it, stored secrets are lost.",
|
|
3929
|
+
keys: Object.keys(SECRET_KEYS)
|
|
3930
|
+
},
|
|
3931
|
+
{
|
|
3932
|
+
title: "Access: where apps and the dashboard reach this machine",
|
|
3933
|
+
keys: ["MYBASE_PORT", "MYBASE_PUBLIC_HOST", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY", "POSTGRES_PORT", "GARAGE_S3_PORT", "VALKEY_PORT", "POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"]
|
|
3934
|
+
},
|
|
3935
|
+
{ title: "Cloudflare Tunnel (public HTTPS without opening router ports)", keys: ["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN"] },
|
|
3936
|
+
{ title: "Startup: `no` keeps mybase from starting again after a reboot or crash", keys: ["MYBASE_RESTART"] },
|
|
3937
|
+
{ title: "Tuning", keys: ["CACHE_MAXMEMORY", "GARAGE_CAPACITY", "MYBASE_MAX_UPLOAD", "LOG_LEVEL"] }
|
|
3938
|
+
];
|
|
3939
|
+
var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
3940
|
+
function unquote(raw) {
|
|
3941
|
+
const value = raw.trim();
|
|
3942
|
+
const quote = value[0];
|
|
3943
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote) && value.length >= 2)
|
|
3944
|
+
return value.slice(1, -1);
|
|
3945
|
+
return value.replace(/\s+#.*$/, "");
|
|
3946
|
+
}
|
|
3947
|
+
function quote(value) {
|
|
3948
|
+
return /^[A-Za-z0-9_./:+=@,-]*$/.test(value) ? value : `'${value.replaceAll("'", "")}'`;
|
|
3949
|
+
}
|
|
3950
|
+
function parseEnv(text) {
|
|
3951
|
+
const values = {};
|
|
3952
|
+
for (const line of text.split(/\r?\n/)) {
|
|
3953
|
+
const match = LINE.exec(line);
|
|
3954
|
+
if (match)
|
|
3955
|
+
values[match[1]] = unquote(match[2]);
|
|
3956
|
+
}
|
|
3957
|
+
return values;
|
|
3958
|
+
}
|
|
3959
|
+
function renderEnv(values) {
|
|
3960
|
+
const blocks = [];
|
|
3961
|
+
const listed = new Set(SECTIONS.flatMap((s) => s.keys));
|
|
3962
|
+
const sections = [...SECTIONS, { title: "Other", keys: Object.keys(values).filter((k) => !listed.has(k)) }];
|
|
3963
|
+
for (const { title, keys } of sections) {
|
|
3964
|
+
const lines = keys.filter((k) => values[k] !== undefined).map((k) => `${k}=${quote(values[k])}`);
|
|
3965
|
+
if (lines.length)
|
|
3966
|
+
blocks.push(`# ${title}
|
|
3967
|
+
${lines.join(`
|
|
3968
|
+
`)}`);
|
|
3969
|
+
}
|
|
3970
|
+
return `# mybase server settings, read by compose.yml. Change with \`mybase server setup\` or by hand,
|
|
3971
|
+
# then run \`docker compose up -d\`.
|
|
3972
|
+
|
|
3973
|
+
${blocks.join(`
|
|
3974
|
+
|
|
3975
|
+
`)}
|
|
3976
|
+
`;
|
|
3977
|
+
}
|
|
3978
|
+
function mergeEnv(text, updates) {
|
|
3979
|
+
const pending = new Map(Object.entries(updates));
|
|
3980
|
+
const lines = [];
|
|
3981
|
+
for (const line of text.replace(/\n+$/, "").split(/\r?\n/)) {
|
|
3982
|
+
const key = LINE.exec(line)?.[1];
|
|
3983
|
+
if (!key || !pending.has(key)) {
|
|
3984
|
+
lines.push(line);
|
|
3985
|
+
continue;
|
|
3986
|
+
}
|
|
3987
|
+
const value = pending.get(key);
|
|
3988
|
+
pending.delete(key);
|
|
3989
|
+
if (value !== null && value !== undefined)
|
|
3990
|
+
lines.push(`${key}=${quote(value)}`);
|
|
3991
|
+
}
|
|
3992
|
+
const added = [...pending].filter((entry) => entry[1] !== null);
|
|
3993
|
+
if (added.length)
|
|
3994
|
+
lines.push("", "# Added by `mybase server setup`", ...added.map(([k, v]) => `${k}=${quote(v)}`));
|
|
3995
|
+
return `${lines.join(`
|
|
3996
|
+
`)}
|
|
3997
|
+
`;
|
|
3998
|
+
}
|
|
3999
|
+
function generateSecrets(existing = {}) {
|
|
4000
|
+
const out = {};
|
|
4001
|
+
for (const [key, { bytes, encoding }] of Object.entries(SECRET_KEYS)) {
|
|
4002
|
+
if (existing[key])
|
|
4003
|
+
continue;
|
|
4004
|
+
out[key] = Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString(encoding);
|
|
4005
|
+
}
|
|
4006
|
+
return out;
|
|
4007
|
+
}
|
|
4008
|
+
var isSecretKey = (key) => (key in SECRET_KEYS) || key === "CLOUDFLARE_TUNNEL_TOKEN";
|
|
4009
|
+
function maskSecret(value) {
|
|
4010
|
+
return value.length <= 8 ? "\u2022\u2022\u2022\u2022" : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
|
|
4011
|
+
}
|
|
4012
|
+
function tunnelIdFromToken(token) {
|
|
4013
|
+
try {
|
|
4014
|
+
const parsed = JSON.parse(Buffer.from(token.trim(), "base64").toString("utf8"));
|
|
4015
|
+
return typeof parsed.t === "string" && typeof parsed.a === "string" && typeof parsed.s === "string" ? parsed.t : null;
|
|
4016
|
+
} catch {
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
function tunnelEnv(token, apiHost, s3Host) {
|
|
4021
|
+
return {
|
|
4022
|
+
COMPOSE_PROFILES: "tunnel",
|
|
4023
|
+
CLOUDFLARE_TUNNEL_TOKEN: token,
|
|
4024
|
+
MYBASE_PUBLIC_URL: `https://${apiHost}`,
|
|
4025
|
+
MYBASE_S3_PUBLIC_URL: `https://${s3Host}`,
|
|
4026
|
+
MYBASE_TRUST_PROXY: "cloudflare"
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
function versionAtLeast(version, min) {
|
|
4030
|
+
const parts = (v) => v.replace(/^v/, "").split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
4031
|
+
const a = parts(version);
|
|
4032
|
+
const b = parts(min);
|
|
4033
|
+
for (let i = 0;i < 3; i++)
|
|
4034
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
4035
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
4036
|
+
return true;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/server-docker.ts
|
|
4040
|
+
var MIN_COMPOSE = "2.23.1";
|
|
4041
|
+
var DOCKER_INSTALL_URL = "https://docs.docker.com/engine/install/";
|
|
4042
|
+
function run2(cmd, cwd) {
|
|
4043
|
+
try {
|
|
4044
|
+
const proc = Bun.spawnSync(cmd, { cwd, stdout: "pipe", stderr: "pipe", env: process.env });
|
|
4045
|
+
return { ok: proc.exitCode === 0, stdout: proc.stdout.toString().trim(), stderr: proc.stderr.toString().trim() };
|
|
4046
|
+
} catch {
|
|
4047
|
+
return { ok: false, stdout: "", stderr: `${cmd[0]} not found` };
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
function composeEnv() {
|
|
4051
|
+
const env = { ...process.env };
|
|
4052
|
+
for (const [, name] of compose_default.matchAll(/\$\{([A-Z0-9_]+)/g))
|
|
4053
|
+
delete env[name];
|
|
4054
|
+
for (const name of Object.keys(env))
|
|
4055
|
+
if (name.startsWith("COMPOSE_"))
|
|
4056
|
+
delete env[name];
|
|
4057
|
+
return env;
|
|
4058
|
+
}
|
|
4059
|
+
function checkDocker() {
|
|
4060
|
+
const client = run2(["docker", "--version"]);
|
|
4061
|
+
if (!client.ok)
|
|
4062
|
+
return { ok: false, problem: `Docker isn't installed. Install it first: ${DOCKER_INSTALL_URL}` };
|
|
4063
|
+
const server = run2(["docker", "version", "--format", "{{.Server.Version}}"]);
|
|
4064
|
+
if (!server.ok) {
|
|
4065
|
+
const denied = /permission denied/i.test(server.stderr);
|
|
4066
|
+
return {
|
|
4067
|
+
ok: false,
|
|
4068
|
+
problem: denied ? "Docker is installed, but this user can't use it. Run `sudo usermod -aG docker $USER`, log out and back in, then try again." : "Docker is installed, but its daemon isn't running. Start it (`sudo systemctl start docker`) and try again."
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
const compose = run2(["docker", "compose", "version", "--short"]);
|
|
4072
|
+
if (!compose.ok)
|
|
4073
|
+
return { ok: false, docker: server.stdout, problem: "Docker Compose v2 is missing. Install the docker-compose-plugin package." };
|
|
4074
|
+
if (!versionAtLeast(compose.stdout, MIN_COMPOSE)) {
|
|
4075
|
+
return { ok: false, docker: server.stdout, compose: compose.stdout, problem: `Docker Compose ${compose.stdout} is too old: mybase needs ${MIN_COMPOSE} or newer.` };
|
|
4076
|
+
}
|
|
4077
|
+
return { ok: true, docker: server.stdout, compose: compose.stdout.replace(/^v/, "") };
|
|
4078
|
+
}
|
|
4079
|
+
async function compose(dir, args) {
|
|
4080
|
+
const proc = Bun.spawn(["docker", "compose", ...args], { cwd: dir, stdout: "pipe", stderr: "pipe", env: composeEnv() });
|
|
4081
|
+
const [stdout, stderr, code] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
4082
|
+
return { ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
4083
|
+
}
|
|
4084
|
+
async function stackExists(dir) {
|
|
4085
|
+
if (!existsSync(`${dir}/compose.yml`))
|
|
4086
|
+
return false;
|
|
4087
|
+
const ps = await compose(dir, ["ps", "-a", "-q"]);
|
|
4088
|
+
return ps.ok && ps.stdout.length > 0;
|
|
4089
|
+
}
|
|
4090
|
+
function otherInstall(dir) {
|
|
4091
|
+
const ls = run2(["docker", "compose", "ls", "--all", "--format", "json"]);
|
|
4092
|
+
if (!ls.ok)
|
|
4093
|
+
return null;
|
|
4094
|
+
try {
|
|
4095
|
+
const projects = JSON.parse(ls.stdout);
|
|
4096
|
+
const mine = projects.find((p) => p.Name === "mybase");
|
|
4097
|
+
if (!mine)
|
|
4098
|
+
return null;
|
|
4099
|
+
const file = mine.ConfigFiles.split(",")[0] ?? "";
|
|
4100
|
+
return file.startsWith(`${dir}/`) ? null : file.replace(/\/[^/]+$/, "");
|
|
4101
|
+
} catch {
|
|
4102
|
+
return null;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
function portFree(port) {
|
|
4106
|
+
try {
|
|
4107
|
+
const server = Bun.listen({ hostname: "0.0.0.0", port, socket: { data() {} } });
|
|
4108
|
+
server.stop(true);
|
|
4109
|
+
return true;
|
|
4110
|
+
} catch {
|
|
4111
|
+
return false;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
async function waitHealthy(url, timeoutMs = 180000) {
|
|
4115
|
+
const deadline = Date.now() + timeoutMs;
|
|
4116
|
+
while (Date.now() < deadline) {
|
|
4117
|
+
try {
|
|
4118
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) });
|
|
4119
|
+
if (res.ok)
|
|
4120
|
+
return (await res.json()).version ?? "unknown";
|
|
4121
|
+
} catch {}
|
|
4122
|
+
await Bun.sleep(2000);
|
|
4123
|
+
}
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
function machine() {
|
|
4127
|
+
let pi = null;
|
|
4128
|
+
try {
|
|
4129
|
+
const model = readFileSync("/proc/device-tree/model", "utf8").replace(/\0/g, "").trim();
|
|
4130
|
+
if (model.includes("Raspberry Pi"))
|
|
4131
|
+
pi = model;
|
|
4132
|
+
} catch {}
|
|
4133
|
+
return { hostname: hostname2(), arch: process.arch, pi, totalMemGb: totalmem() / 2 ** 30, freeMemGb: freemem() / 2 ** 30 };
|
|
4134
|
+
}
|
|
4135
|
+
async function freeDiskGb(dir) {
|
|
4136
|
+
for (let d = dir;; d = d.replace(/\/[^/]*$/, "") || "/") {
|
|
4137
|
+
if (existsSync(d)) {
|
|
4138
|
+
try {
|
|
4139
|
+
const s = await statfs(d);
|
|
4140
|
+
return s.bavail * s.bsize / 2 ** 30;
|
|
4141
|
+
} catch {
|
|
4142
|
+
return null;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (d === "/")
|
|
4146
|
+
return null;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
function addresses() {
|
|
4150
|
+
const out = [];
|
|
4151
|
+
const ts = run2(["tailscale", "status", "--json"]);
|
|
4152
|
+
if (ts.ok) {
|
|
4153
|
+
try {
|
|
4154
|
+
const self = JSON.parse(ts.stdout).Self;
|
|
4155
|
+
const dns = self?.DNSName?.replace(/\.$/, "");
|
|
4156
|
+
if (dns)
|
|
4157
|
+
out.push({ value: dns, label: "Tailscale (MagicDNS)" });
|
|
4158
|
+
const ip = self?.TailscaleIPs?.find((a) => a.includes("."));
|
|
4159
|
+
if (ip)
|
|
4160
|
+
out.push({ value: ip, label: "Tailscale IP" });
|
|
4161
|
+
} catch {}
|
|
4162
|
+
}
|
|
4163
|
+
const host = hostname2();
|
|
4164
|
+
out.push({ value: `${host}.local`, label: "mDNS name (most home networks)" });
|
|
4165
|
+
for (const [name, list] of Object.entries(networkInterfaces())) {
|
|
4166
|
+
if (/^(docker|br-|veth|virbr|tailscale|lo|cni|flannel)/.test(name))
|
|
4167
|
+
continue;
|
|
4168
|
+
for (const a of list ?? []) {
|
|
4169
|
+
if (a.family === "IPv4" && !a.internal)
|
|
4170
|
+
out.push({ value: a.address, label: `LAN IP (${name})` });
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return out;
|
|
4174
|
+
}
|
|
4175
|
+
function dockerOnBoot() {
|
|
4176
|
+
if (process.platform === "darwin" || process.platform === "win32")
|
|
4177
|
+
return "desktop";
|
|
4178
|
+
const state = run2(["systemctl", "is-enabled", "docker.service"]);
|
|
4179
|
+
if (state.stdout === "enabled")
|
|
4180
|
+
return "enabled";
|
|
4181
|
+
if (state.stdout === "disabled" || state.stdout === "masked")
|
|
4182
|
+
return "disabled";
|
|
4183
|
+
return "unknown";
|
|
4184
|
+
}
|
|
4185
|
+
async function enableDockerOnBoot() {
|
|
4186
|
+
const cmd = process.getuid?.() === 0 ? ["systemctl", "enable", "docker.service", "containerd.service"] : ["sudo", "systemctl", "enable", "docker.service", "containerd.service"];
|
|
4187
|
+
try {
|
|
4188
|
+
const proc = Bun.spawn(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
|
4189
|
+
return await proc.exited === 0;
|
|
4190
|
+
} catch {
|
|
4191
|
+
return false;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
// src/server-setup.ts
|
|
4196
|
+
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
4197
|
+
import { homedir as homedir2, hostname as hostname3 } from "os";
|
|
4198
|
+
import { join as join3, resolve as resolve4 } from "path";
|
|
4199
|
+
import { styleText as styleText3 } from "util";
|
|
4200
|
+
|
|
4201
|
+
// src/cloudflare.ts
|
|
4202
|
+
var API = "https://api.cloudflare.com/client/v4";
|
|
4203
|
+
var TOKEN_PERMISSIONS = ["Account \u203A Cloudflare Tunnel \u203A Edit", "Zone \u203A DNS \u203A Edit", "Zone \u203A Zone \u203A Read"];
|
|
4204
|
+
var TOKEN_URL = "https://dash.cloudflare.com/profile/api-tokens";
|
|
4205
|
+
|
|
4206
|
+
class CloudflareError extends Error {
|
|
4207
|
+
}
|
|
4208
|
+
function cloudflare(token, fetcher = fetch) {
|
|
4209
|
+
async function call(method, path, body) {
|
|
4210
|
+
const res = await fetcher(`${API}${path}`, {
|
|
4211
|
+
method,
|
|
4212
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
4213
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
4214
|
+
}).catch(() => {
|
|
4215
|
+
throw new CloudflareError("can't reach api.cloudflare.com");
|
|
4216
|
+
});
|
|
4217
|
+
const data = await res.json().catch(() => null);
|
|
4218
|
+
if (!res.ok || !data?.success) {
|
|
4219
|
+
const message = data?.errors?.map((e) => e.message).join("; ") || res.statusText;
|
|
4220
|
+
if (res.status === 401 || res.status === 403)
|
|
4221
|
+
throw new CloudflareError(`Cloudflare refused the token (${message}). It needs: ${TOKEN_PERMISSIONS.join(", ")}.`);
|
|
4222
|
+
throw new CloudflareError(`Cloudflare: ${message}`);
|
|
4223
|
+
}
|
|
4224
|
+
return data.result;
|
|
4225
|
+
}
|
|
4226
|
+
return {
|
|
4227
|
+
listAccounts: () => call("GET", "/accounts?per_page=50"),
|
|
4228
|
+
listZones: () => call("GET", "/zones?per_page=50&status=active"),
|
|
4229
|
+
async ensureTunnel(accountId, name) {
|
|
4230
|
+
const existing = await call("GET", `/accounts/${accountId}/cfd_tunnel?name=${encodeURIComponent(name)}&is_deleted=false`);
|
|
4231
|
+
if (existing[0])
|
|
4232
|
+
return { id: existing[0].id, name: existing[0].name, created: false };
|
|
4233
|
+
const tunnel = await call("POST", `/accounts/${accountId}/cfd_tunnel`, { name, config_src: "cloudflare" });
|
|
4234
|
+
return { id: tunnel.id, name: tunnel.name, created: true };
|
|
4235
|
+
},
|
|
4236
|
+
putIngress: (accountId, tunnelId, rules) => call("PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {
|
|
4237
|
+
config: { ingress: [...rules, { service: "http_status:404" }] }
|
|
4238
|
+
}),
|
|
4239
|
+
async findRecord(zoneId, name) {
|
|
4240
|
+
const records = await call("GET", `/zones/${zoneId}/dns_records?name=${encodeURIComponent(name)}`);
|
|
4241
|
+
return records[0] ?? null;
|
|
4242
|
+
},
|
|
4243
|
+
async upsertCname(zoneId, name, tunnelId) {
|
|
4244
|
+
const record = { type: "CNAME", name, content: `${tunnelId}.cfargotunnel.com`, proxied: true, comment: "mybase tunnel" };
|
|
4245
|
+
const existing = await call("GET", `/zones/${zoneId}/dns_records?name=${encodeURIComponent(name)}`);
|
|
4246
|
+
if (existing[0])
|
|
4247
|
+
await call("PUT", `/zones/${zoneId}/dns_records/${existing[0].id}`, record);
|
|
4248
|
+
else
|
|
4249
|
+
await call("POST", `/zones/${zoneId}/dns_records`, record);
|
|
4250
|
+
},
|
|
4251
|
+
tunnelToken: (accountId, tunnelId) => call("GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`)
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
var pointsAtTunnel = (record, tunnelId) => record.content === `${tunnelId}.cfargotunnel.com`;
|
|
4255
|
+
|
|
4256
|
+
// src/server-setup.ts
|
|
4257
|
+
var DEFAULT_DIR = join3(homedir2(), "mybase");
|
|
4258
|
+
var USERNAME_PATTERN = /^[a-zA-Z0-9_.@-]{1,64}$/;
|
|
4259
|
+
var HOST_PATTERN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
4260
|
+
var TUNNEL_KEYS = ["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY"];
|
|
4261
|
+
var BIND_KEYS = ["POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"];
|
|
4262
|
+
var bold = (s) => styleText3("bold", s);
|
|
4263
|
+
var dim = (s) => styleText3("dim", s);
|
|
4264
|
+
var cyan = (s) => styleText3("cyan", s);
|
|
4265
|
+
var yellow = (s) => styleText3("yellow", s);
|
|
4266
|
+
var tilde = (path) => path.replace(homedir2(), "~");
|
|
4267
|
+
function flag(args, name) {
|
|
4268
|
+
const i = args.indexOf(name);
|
|
4269
|
+
if (i === -1)
|
|
4270
|
+
return;
|
|
4271
|
+
const value = args[i + 1];
|
|
4272
|
+
if (!value || value.startsWith("--"))
|
|
4273
|
+
throw usageError(`${name} needs a value`);
|
|
4274
|
+
return value;
|
|
4275
|
+
}
|
|
4276
|
+
async function serverDir(args) {
|
|
4277
|
+
const given = flag(args, "--dir");
|
|
4278
|
+
if (given)
|
|
4279
|
+
return resolve4(given.replace(/^~(?=$|\/)/, homedir2()));
|
|
4280
|
+
return (await loadConfig()).server ?? DEFAULT_DIR;
|
|
4281
|
+
}
|
|
4282
|
+
var localUrl = (env) => `http://localhost:${env.MYBASE_PORT || "4000"}`;
|
|
4283
|
+
var ui = {
|
|
4284
|
+
info: (m) => !jsonMode && log.info(m),
|
|
4285
|
+
success: (m) => !jsonMode && log.success(m),
|
|
4286
|
+
warn: (m) => !jsonMode && log.warn(m),
|
|
4287
|
+
error: (m) => !jsonMode && log.error(m),
|
|
4288
|
+
step: (m) => !jsonMode && log.step(m),
|
|
4289
|
+
note: (m, title) => !jsonMode && note(m, title),
|
|
4290
|
+
spinner() {
|
|
4291
|
+
return jsonMode ? { start() {}, stop() {}, error() {}, message() {} } : spinner();
|
|
4292
|
+
}
|
|
4293
|
+
};
|
|
4294
|
+
async function ask(answer) {
|
|
4295
|
+
const value = await answer;
|
|
4296
|
+
if (isCancel(value)) {
|
|
4297
|
+
cancel("Cancelled. Nothing was changed.");
|
|
4298
|
+
process.exit(0);
|
|
4299
|
+
}
|
|
4300
|
+
return value;
|
|
4301
|
+
}
|
|
4302
|
+
var validPort = (v) => /^\d+$/.test(v ?? "") && Number(v) >= 1 && Number(v) <= 65535 ? undefined : "a port number, 1\u201365535";
|
|
4303
|
+
var tail = (text, n = 25) => text.split(`
|
|
4304
|
+
`).slice(-n).join(`
|
|
4305
|
+
`);
|
|
4306
|
+
async function preflight(dir) {
|
|
4307
|
+
const s = ui.spinner();
|
|
4308
|
+
s.start("Checking this machine");
|
|
4309
|
+
const docker = checkDocker();
|
|
4310
|
+
const m = machine();
|
|
4311
|
+
const disk = await freeDiskGb(dir);
|
|
4312
|
+
if (!docker.ok) {
|
|
4313
|
+
s.error("This machine isn't ready");
|
|
4314
|
+
throw new CliError(docker.problem);
|
|
4315
|
+
}
|
|
4316
|
+
s.stop("This machine is ready");
|
|
4317
|
+
const ok = styleText3("green", "\u2714");
|
|
4318
|
+
const warn = yellow("\u25B2");
|
|
4319
|
+
const lines = [
|
|
4320
|
+
`${ok} Docker ${docker.docker}, Compose ${docker.compose}`,
|
|
4321
|
+
`${ok} ${m.pi ?? m.hostname} (${m.arch})`,
|
|
4322
|
+
`${m.totalMemGb < 1.5 ? warn : ok} ${m.totalMemGb.toFixed(1)} GB memory${m.totalMemGb < 1.5 ? dim(" mybase uses ~600 MB; 2 GB+ recommended") : ""}`
|
|
4323
|
+
];
|
|
4324
|
+
if (disk !== null)
|
|
4325
|
+
lines.push(`${disk < 5 ? warn : ok} ${disk.toFixed(0)} GB free disk${disk < 5 ? dim(" images + data need a few GB") : ""}`);
|
|
4326
|
+
if (m.pi)
|
|
4327
|
+
lines.push(`${warn} ${dim("Raspberry Pi: put Docker's data on an SSD if you can. SD cards are slow and wear out.")}`);
|
|
4328
|
+
ui.note(lines.join(`
|
|
4329
|
+
`), "This machine");
|
|
4330
|
+
}
|
|
4331
|
+
async function chooseDir(args, interactive) {
|
|
4332
|
+
let dir = await serverDir(args);
|
|
4333
|
+
if (interactive && !args.includes("--dir")) {
|
|
4334
|
+
const answer = await ask(text({
|
|
4335
|
+
message: "Where should mybase keep its files?",
|
|
4336
|
+
placeholder: tilde(dir),
|
|
4337
|
+
defaultValue: dir,
|
|
4338
|
+
validate: (v) => v && !v.trim().startsWith("/") && !v.trim().startsWith("~") ? "use an absolute path (or ~/\u2026)" : undefined
|
|
4339
|
+
}));
|
|
4340
|
+
dir = resolve4(answer.trim().replace(/^~(?=$|\/)/, homedir2()));
|
|
4341
|
+
}
|
|
4342
|
+
const other = otherInstall(dir);
|
|
4343
|
+
if (other) {
|
|
4344
|
+
throw new CliError(`mybase is already installed in ${other}. Run \`mybase server setup --dir ${other}\` to change it, or stop it first (\`docker compose down\` there).`);
|
|
4345
|
+
}
|
|
4346
|
+
return dir;
|
|
4347
|
+
}
|
|
4348
|
+
async function chooseHost(current) {
|
|
4349
|
+
const found = addresses();
|
|
4350
|
+
const options = found.map((a) => ({ value: a.value, label: a.value, hint: a.label }));
|
|
4351
|
+
if (current && !found.some((a) => a.value === current))
|
|
4352
|
+
options.unshift({ value: current, label: current, hint: "current" });
|
|
4353
|
+
options.push({ value: "", label: "Something else\u2026", hint: "type a name or IP" });
|
|
4354
|
+
const picked = await ask(select({ message: "Which address should apps on your network use?", options, initialValue: current ?? options[0]?.value }));
|
|
4355
|
+
if (picked)
|
|
4356
|
+
return picked;
|
|
4357
|
+
return (await ask(text({ message: "Address (hostname or IP)", validate: (v) => v?.trim() ? undefined : "required" }))).trim();
|
|
4358
|
+
}
|
|
4359
|
+
var validHost = (v) => HOST_PATTERN.test(v?.trim() ?? "") ? undefined : "a full hostname, like mybase.example.com";
|
|
4360
|
+
async function tunnelStep() {
|
|
4361
|
+
const mode = await ask(select({
|
|
4362
|
+
message: "How do you want to set up the tunnel?",
|
|
4363
|
+
options: [
|
|
4364
|
+
{ value: "auto", label: "Automatically", hint: "paste a Cloudflare API token; mybase creates the tunnel + DNS" },
|
|
4365
|
+
{ value: "manual", label: "By hand", hint: "follow the steps in the Cloudflare dashboard, then paste the tunnel token" }
|
|
4366
|
+
]
|
|
4367
|
+
}));
|
|
4368
|
+
return mode === "auto" ? autoTunnel() : manualTunnel();
|
|
4369
|
+
}
|
|
4370
|
+
async function autoTunnel() {
|
|
4371
|
+
ui.note([
|
|
4372
|
+
`1. Your domain has to be on Cloudflare (free plan is fine).`,
|
|
4373
|
+
`2. Create a token at ${cyan(TOKEN_URL)}`,
|
|
4374
|
+
` ${dim("Create Token \u2192 Custom token, with these permissions:")}`,
|
|
4375
|
+
...TOKEN_PERMISSIONS.map((perm) => ` \u2022 ${perm}`),
|
|
4376
|
+
``,
|
|
4377
|
+
dim("The token is only used now, to create the tunnel and DNS records. It isn't saved.")
|
|
4378
|
+
].join(`
|
|
4379
|
+
`), "Cloudflare API token");
|
|
4380
|
+
for (;; ) {
|
|
4381
|
+
const token = await ask(password({ message: "Cloudflare API token", validate: (v) => v?.trim() ? undefined : "required" }));
|
|
4382
|
+
const cf = cloudflare(token.trim());
|
|
4383
|
+
const s = ui.spinner();
|
|
4384
|
+
s.start("Looking up your domains");
|
|
4385
|
+
let zones;
|
|
4386
|
+
try {
|
|
4387
|
+
zones = await cf.listZones();
|
|
4388
|
+
} catch (err) {
|
|
4389
|
+
s.error(err.message);
|
|
4390
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4391
|
+
continue;
|
|
4392
|
+
return manualTunnel();
|
|
4393
|
+
}
|
|
4394
|
+
if (zones.length === 0) {
|
|
4395
|
+
s.error("No active domains on this token");
|
|
4396
|
+
ui.info("Add your domain at https://dash.cloudflare.com (Add a site), wait until it's active, then try again.");
|
|
4397
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4398
|
+
continue;
|
|
4399
|
+
return manualTunnel();
|
|
4400
|
+
}
|
|
4401
|
+
s.stop(`Found ${zones.length} domain${zones.length === 1 ? "" : "s"}`);
|
|
4402
|
+
const zoneId = zones.length === 1 ? zones[0].id : await ask(select({ message: "Which domain?", options: zones.map((z) => ({ value: z.id, label: z.name })) }));
|
|
4403
|
+
const zone = zones.find((z) => z.id === zoneId);
|
|
4404
|
+
const tunnelName = `mybase-${hostname3()}`;
|
|
4405
|
+
s.start(`Creating tunnel ${tunnelName}`);
|
|
4406
|
+
let tunnel;
|
|
4407
|
+
try {
|
|
4408
|
+
tunnel = await cf.ensureTunnel(zone.account.id, tunnelName);
|
|
4409
|
+
} catch (err) {
|
|
4410
|
+
s.error(err.message);
|
|
4411
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4412
|
+
continue;
|
|
4413
|
+
return manualTunnel();
|
|
4414
|
+
}
|
|
4415
|
+
s.stop(tunnel.created ? `Created tunnel ${tunnelName}` : `Reusing tunnel ${tunnelName}`);
|
|
4416
|
+
let apiHost = "";
|
|
4417
|
+
let s3Host = "";
|
|
4418
|
+
for (;; ) {
|
|
4419
|
+
const apiSub = await ask(text({ message: "Subdomain for the dashboard + API", placeholder: "mybase", defaultValue: "mybase" }));
|
|
4420
|
+
const s3Sub = await ask(text({ message: "Subdomain for S3 storage", placeholder: "s3", defaultValue: "s3" }));
|
|
4421
|
+
apiHost = `${apiSub.trim()}.${zone.name}`;
|
|
4422
|
+
s3Host = `${s3Sub.trim()}.${zone.name}`;
|
|
4423
|
+
if (validHost(apiHost) || validHost(s3Host) || apiHost === s3Host) {
|
|
4424
|
+
ui.warn("Use two different, valid subdomains.");
|
|
4425
|
+
continue;
|
|
4426
|
+
}
|
|
4427
|
+
const taken = [];
|
|
4428
|
+
for (const host of [apiHost, s3Host]) {
|
|
4429
|
+
const record = await cf.findRecord(zone.id, host).catch(() => null);
|
|
4430
|
+
if (record && !pointsAtTunnel(record, tunnel.id))
|
|
4431
|
+
taken.push(`${host} (${record.type} \u2192 ${record.content})`);
|
|
4432
|
+
}
|
|
4433
|
+
if (taken.length === 0)
|
|
4434
|
+
break;
|
|
4435
|
+
const replace = await ask(confirm2({ message: `Already in use: ${taken.join(", ")}. Replace ${taken.length === 1 ? "it" : "them"}?`, initialValue: false }));
|
|
4436
|
+
if (replace)
|
|
4437
|
+
break;
|
|
4438
|
+
}
|
|
4439
|
+
s.start("Routing hostnames to the tunnel");
|
|
4440
|
+
try {
|
|
4441
|
+
await cf.putIngress(zone.account.id, tunnel.id, [
|
|
4442
|
+
{ hostname: apiHost, service: "http://mybase:4000" },
|
|
4443
|
+
{ hostname: s3Host, service: "http://garage:3900" }
|
|
4444
|
+
]);
|
|
4445
|
+
await cf.upsertCname(zone.id, apiHost, tunnel.id);
|
|
4446
|
+
await cf.upsertCname(zone.id, s3Host, tunnel.id);
|
|
4447
|
+
const tunnelToken = await cf.tunnelToken(zone.account.id, tunnel.id);
|
|
4448
|
+
s.stop(`${apiHost} and ${s3Host} \u2192 this machine`);
|
|
4449
|
+
return { env: tunnelEnv(tunnelToken, apiHost, s3Host), apiHost, s3Host };
|
|
4450
|
+
} catch (err) {
|
|
4451
|
+
s.error(err instanceof CloudflareError ? err.message : `Cloudflare: ${err.message}`);
|
|
4452
|
+
if (await ask(confirm2({ message: "Try another token?" })))
|
|
4453
|
+
continue;
|
|
4454
|
+
return manualTunnel();
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
async function manualTunnel() {
|
|
4459
|
+
ui.note([
|
|
4460
|
+
`1. Add your domain to Cloudflare (free plan) and switch its nameservers at your registrar.`,
|
|
4461
|
+
`2. Open ${cyan("https://one.dash.cloudflare.com")} \u2192 Networks \u2192 Tunnels \u2192 ${bold("Create a tunnel")}`,
|
|
4462
|
+
` \u2192 Cloudflared. Name it ${bold("mybase")}.`,
|
|
4463
|
+
`3. Skip installing a connector (mybase runs it in Docker). Copy the token: the long`,
|
|
4464
|
+
` string after ${bold("--token")} in any of the install commands.`,
|
|
4465
|
+
`4. Add two ${bold("public hostnames")} (Type: HTTP):`,
|
|
4466
|
+
` mybase.<your domain> \u2192 ${cyan("mybase:4000")} ${dim("dashboard, API, Postgres over WebSocket")}`,
|
|
4467
|
+
` s3.<your domain> \u2192 ${cyan("garage:3900")} ${dim("S3 for your apps")}`
|
|
4468
|
+
].join(`
|
|
4469
|
+
`), "Cloudflare Tunnel, by hand");
|
|
4470
|
+
const token = await ask(password({ message: "Tunnel token", validate: (v) => tunnelIdFromToken(v ?? "") ? undefined : "that isn't a tunnel token (copy the part after --token)" }));
|
|
4471
|
+
const apiHost = (await ask(text({ message: "Hostname for the dashboard + API", placeholder: "mybase.example.com", validate: validHost }))).trim();
|
|
4472
|
+
const defaultS3 = apiHost.replace(/^[^.]+/, "s3");
|
|
4473
|
+
const s3Host = (await ask(text({ message: "Hostname for S3", placeholder: defaultS3, defaultValue: defaultS3, validate: (v) => validHost(v || defaultS3) }))).trim();
|
|
4474
|
+
return { env: tunnelEnv(token.trim(), apiHost, s3Host), apiHost, s3Host };
|
|
4475
|
+
}
|
|
4476
|
+
async function bindStep(lan, existing) {
|
|
4477
|
+
const locked = existing.POSTGRES_BIND === "127.0.0.1";
|
|
4478
|
+
const lock = await ask(confirm2({
|
|
4479
|
+
message: "Close the Postgres, S3, and cache ports to your network? (apps then connect through the tunnel only)",
|
|
4480
|
+
initialValue: lan ? locked : true
|
|
4481
|
+
}));
|
|
4482
|
+
return Object.fromEntries(BIND_KEYS.map((k) => [k, lock ? "127.0.0.1" : null]));
|
|
4483
|
+
}
|
|
4484
|
+
async function adminStep() {
|
|
4485
|
+
const when = await ask(select({
|
|
4486
|
+
message: "Create the admin account now?",
|
|
4487
|
+
options: [
|
|
4488
|
+
{ value: "now", label: "Yes", hint: "and sign this CLI in" },
|
|
4489
|
+
{ value: "later", label: "Later, in the browser", hint: "from this machine or your network" }
|
|
4490
|
+
]
|
|
4491
|
+
}));
|
|
4492
|
+
if (when === "later")
|
|
4493
|
+
return null;
|
|
4494
|
+
const username = (await ask(text({ message: "Admin username", placeholder: "admin", defaultValue: "admin", validate: (v) => !v || USERNAME_PATTERN.test(v) ? undefined : "letters, digits, or . _ @ -" }))).trim();
|
|
4495
|
+
for (;; ) {
|
|
4496
|
+
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` }));
|
|
4497
|
+
const again = await ask(password({ message: "Same password again" }));
|
|
4498
|
+
if (again === password2)
|
|
4499
|
+
return { username, password: password2 };
|
|
4500
|
+
ui.warn("The passwords didn't match.");
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
async function bootStep(existing, interactive, args) {
|
|
4504
|
+
const docker = dockerOnBoot();
|
|
4505
|
+
if (!interactive) {
|
|
4506
|
+
const always = !args.includes("--no-autostart");
|
|
4507
|
+
if (always && docker === "disabled")
|
|
4508
|
+
ui.warn("Docker doesn't start at boot, so mybase won't either. Run `sudo systemctl enable docker`.");
|
|
4509
|
+
return { always, enableDocker: false };
|
|
4510
|
+
}
|
|
4511
|
+
const always = await ask(confirm2({
|
|
4512
|
+
message: `Start mybase automatically when this machine turns on? ${dim("(and restart it if it crashes)")}`,
|
|
4513
|
+
initialValue: existing.MYBASE_RESTART !== "no"
|
|
4514
|
+
}));
|
|
4515
|
+
if (!always)
|
|
4516
|
+
return { always, enableDocker: false };
|
|
4517
|
+
if (docker === "enabled")
|
|
4518
|
+
return { always, enableDocker: false };
|
|
4519
|
+
if (docker === "desktop") {
|
|
4520
|
+
ui.info(`Docker Desktop: turn on ${bold("Settings \u2192 General \u2192 Start Docker Desktop when you sign in")} too.`);
|
|
4521
|
+
return { always, enableDocker: false };
|
|
4522
|
+
}
|
|
4523
|
+
if (docker === "unknown") {
|
|
4524
|
+
ui.warn("Couldn't tell whether Docker starts at boot here (no systemd?). Make sure it does, or mybase won't either.");
|
|
4525
|
+
return { always, enableDocker: false };
|
|
4526
|
+
}
|
|
4527
|
+
const enableDocker = await ask(confirm2({ message: `Docker doesn't start at boot yet. Turn that on? ${dim("(sudo systemctl enable docker)")}` }));
|
|
4528
|
+
if (!enableDocker)
|
|
4529
|
+
ui.warn("Then mybase only comes back after a reboot once you start Docker yourself.");
|
|
4530
|
+
return { always, enableDocker };
|
|
4531
|
+
}
|
|
4532
|
+
async function turnOnDockerAtBoot() {
|
|
4533
|
+
ui.step("Turning on Docker at boot (sudo may ask for your password)");
|
|
4534
|
+
if (await enableDockerOnBoot())
|
|
4535
|
+
ui.success("Docker starts at boot");
|
|
4536
|
+
else
|
|
4537
|
+
ui.warn("Couldn't enable it. Run `sudo systemctl enable docker` yourself.");
|
|
4538
|
+
}
|
|
4539
|
+
async function advancedStep(env) {
|
|
4540
|
+
const out = {};
|
|
4541
|
+
const set = (key, value, fallback) => {
|
|
4542
|
+
if (value !== fallback)
|
|
4543
|
+
out[key] = value;
|
|
4544
|
+
else if (env[key] !== undefined)
|
|
4545
|
+
out[key] = null;
|
|
4546
|
+
};
|
|
4547
|
+
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));
|
|
4548
|
+
await port("MYBASE_PORT", "Dashboard + API", "4000");
|
|
4549
|
+
await port("POSTGRES_PORT", "Postgres", "5432");
|
|
4550
|
+
await port("GARAGE_S3_PORT", "S3", "3900");
|
|
4551
|
+
await port("VALKEY_PORT", "Cache", "6379");
|
|
4552
|
+
const memory = await ask(select({
|
|
4553
|
+
message: "Cache memory (shared by every project; least recently used keys go first)",
|
|
4554
|
+
initialValue: env.CACHE_MAXMEMORY ?? "64mb",
|
|
4555
|
+
options: ["64mb", "128mb", "256mb", "512mb", "1gb"].map((v) => ({ value: v, label: v, hint: v === "64mb" ? "default, fine for a Pi" : undefined }))
|
|
4556
|
+
}));
|
|
4557
|
+
set("CACHE_MAXMEMORY", memory, "64mb");
|
|
4558
|
+
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" }));
|
|
4559
|
+
set("GARAGE_CAPACITY", capacity.trim(), "100G");
|
|
4560
|
+
const level = await ask(select({ message: "Log level", initialValue: env.LOG_LEVEL ?? "info", options: ["debug", "info", "warn", "error"].map((v) => ({ value: v, label: v })) }));
|
|
4561
|
+
set("LOG_LEVEL", level, "info");
|
|
4562
|
+
return out;
|
|
4563
|
+
}
|
|
4564
|
+
async function portStep(env, interactive) {
|
|
4565
|
+
const ports = [
|
|
4566
|
+
["MYBASE_PORT", "dashboard + API", "4000"],
|
|
4567
|
+
["POSTGRES_PORT", "Postgres", "5432"],
|
|
4568
|
+
["GARAGE_S3_PORT", "S3", "3900"],
|
|
4569
|
+
["VALKEY_PORT", "cache", "6379"]
|
|
4570
|
+
];
|
|
4571
|
+
const out = {};
|
|
4572
|
+
for (const [key, label, fallback] of ports) {
|
|
4573
|
+
const current = env[key] ?? fallback;
|
|
4574
|
+
if (portFree(Number(current)))
|
|
4575
|
+
continue;
|
|
4576
|
+
if (!interactive)
|
|
4577
|
+
throw new CliError(`port ${current} (${label}) is already in use. Free it, or set ${key} in the .env.`);
|
|
4578
|
+
let next = Number(current) + 1;
|
|
4579
|
+
while (!portFree(next) && next < 65535)
|
|
4580
|
+
next++;
|
|
4581
|
+
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 }));
|
|
4582
|
+
out[key] = answer.trim();
|
|
4583
|
+
}
|
|
4584
|
+
return out;
|
|
4585
|
+
}
|
|
4586
|
+
async function writeFiles(dir, envText, interactive, replaceCompose = interactive) {
|
|
4587
|
+
await mkdir3(dir, { recursive: true });
|
|
4588
|
+
const envPath = join3(dir, ".env");
|
|
4589
|
+
await Bun.write(envPath, envText);
|
|
4590
|
+
await chmod2(envPath, 384);
|
|
4591
|
+
await writeCompose(dir, interactive, replaceCompose);
|
|
4592
|
+
}
|
|
4593
|
+
async function writeCompose(dir, interactive, replace) {
|
|
4594
|
+
const file = Bun.file(join3(dir, "compose.yml"));
|
|
4595
|
+
if (await file.exists()) {
|
|
4596
|
+
const current = await file.text();
|
|
4597
|
+
if (current === compose_default)
|
|
4598
|
+
return false;
|
|
4599
|
+
const ok = interactive ? await ask(confirm2({ message: `${tilde(join3(dir, "compose.yml"))} differs from this CLI's (${CLI_VERSION}). Replace it? (the old one is kept as compose.yml.bak)` })) : replace;
|
|
4600
|
+
if (!ok)
|
|
4601
|
+
return false;
|
|
4602
|
+
await Bun.write(join3(dir, "compose.yml.bak"), current);
|
|
4603
|
+
}
|
|
4604
|
+
await Bun.write(file, compose_default);
|
|
4605
|
+
return true;
|
|
4606
|
+
}
|
|
4607
|
+
async function missingImages(dir) {
|
|
4608
|
+
const images = await compose(dir, ["config", "--images"]);
|
|
4609
|
+
const missing = [];
|
|
4610
|
+
for (const image of images.stdout.split(`
|
|
4611
|
+
`).filter(Boolean)) {
|
|
4612
|
+
if (!imageExists(image))
|
|
4613
|
+
missing.push(image);
|
|
4614
|
+
}
|
|
4615
|
+
return missing;
|
|
4616
|
+
}
|
|
4617
|
+
var imageExists = (image) => Bun.spawnSync(["docker", "image", "inspect", image], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
|
|
4618
|
+
async function startStack(dir, env, pull = true) {
|
|
4619
|
+
const s = ui.spinner();
|
|
4620
|
+
if (pull) {
|
|
4621
|
+
s.start("Downloading images (the first time takes a few minutes, longer on a Pi)");
|
|
4622
|
+
const pulled = await compose(dir, ["pull", "--quiet"]);
|
|
4623
|
+
if (!pulled.ok) {
|
|
4624
|
+
const missing = await missingImages(dir);
|
|
4625
|
+
if (missing.length) {
|
|
4626
|
+
s.error("Couldn't download the images");
|
|
4627
|
+
throw new CliError(`couldn't download ${missing.join(", ")}:
|
|
4628
|
+
${tail(pulled.stderr || pulled.stdout)}`);
|
|
4629
|
+
}
|
|
4630
|
+
s.stop("Using the images already on this machine (couldn't download newer ones)");
|
|
4631
|
+
} else
|
|
4632
|
+
s.stop("Images downloaded");
|
|
4633
|
+
}
|
|
4634
|
+
s.start("Starting containers");
|
|
4635
|
+
const up = await compose(dir, ["up", "-d", "--remove-orphans"]);
|
|
4636
|
+
if (!up.ok) {
|
|
4637
|
+
s.error("Couldn't start the containers");
|
|
4638
|
+
throw new CliError(`docker compose up failed:
|
|
4639
|
+
${tail(up.stderr || up.stdout)}`);
|
|
4640
|
+
}
|
|
4641
|
+
s.message("Waiting for mybase to answer");
|
|
4642
|
+
const version = await waitHealthy(localUrl(env));
|
|
4643
|
+
if (!version) {
|
|
4644
|
+
s.error("mybase didn't come up");
|
|
4645
|
+
const logs = await compose(dir, ["logs", "--tail", "30", "mybase"]);
|
|
4646
|
+
throw new CliError(`mybase didn't answer at ${localUrl(env)}/health within 3 minutes. Its last log lines:
|
|
4647
|
+
${logs.stdout || logs.stderr}
|
|
4648
|
+
|
|
4649
|
+
Fix the problem, then run \`mybase server setup --dir ${dir}\` again (keys are kept).`);
|
|
4650
|
+
}
|
|
4651
|
+
s.stop(`mybase ${version} is running`);
|
|
4652
|
+
return version;
|
|
4653
|
+
}
|
|
4654
|
+
async function createAdmin(url, admin) {
|
|
4655
|
+
const post = async (path, body) => {
|
|
4656
|
+
const res = await fetch(`${url}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
4657
|
+
const data = await res.json().catch(() => ({}));
|
|
4658
|
+
if (!res.ok)
|
|
4659
|
+
throw new CliError(data.error ?? res.statusText);
|
|
4660
|
+
return data;
|
|
4661
|
+
};
|
|
4662
|
+
await post("/auth/setup", admin);
|
|
4663
|
+
const login = await post("/auth/login", { ...admin, tokenName: `cli on ${hostname3()}` });
|
|
4664
|
+
if (!login.token)
|
|
4665
|
+
throw new CliError("the server didn't return a token");
|
|
4666
|
+
const previous = await loadConfig();
|
|
4667
|
+
await saveConfig({ url, token: login.token });
|
|
4668
|
+
if (previous.token && previous.url)
|
|
4669
|
+
await api("/auth/logout", { method: "POST", url: previous.url, token: previous.token }).catch(() => {});
|
|
4670
|
+
return login.username;
|
|
4671
|
+
}
|
|
4672
|
+
async function setupNeeded(url) {
|
|
4673
|
+
const res = await fetch(`${url}/auth/me`).catch(() => null);
|
|
4674
|
+
if (!res?.ok)
|
|
4675
|
+
return true;
|
|
4676
|
+
return (await res.json()).setupNeeded !== false;
|
|
4677
|
+
}
|
|
4678
|
+
async function setup(args) {
|
|
4679
|
+
const yes = args.includes("--yes");
|
|
4680
|
+
const interactive = !yes && !jsonMode && process.stdin.isTTY === true;
|
|
4681
|
+
if (!interactive && !yes)
|
|
4682
|
+
throw usageError("refusing without --yes (no terminal to ask on). Pass --yes with flags: see `mybase help server`.");
|
|
4683
|
+
if (!jsonMode)
|
|
4684
|
+
intro(`${bold(" mybase ")} ${dim(`server setup \xB7 ${CLI_VERSION}`)}`);
|
|
4685
|
+
if (interactive)
|
|
4686
|
+
ui.info(`This sets up mybase on ${bold(hostname3())}: Postgres, storage, cache, and the dashboard, in Docker.
|
|
4687
|
+
${dim("Nothing is changed until you confirm at the end. Ctrl-C leaves at any time.")}`);
|
|
4688
|
+
const dir = await chooseDir(args, interactive);
|
|
4689
|
+
await preflight(dir);
|
|
4690
|
+
const envFile = Bun.file(join3(dir, ".env"));
|
|
4691
|
+
const envText = await envFile.exists() ? await envFile.text() : null;
|
|
4692
|
+
const existing = envText ? parseEnv(envText) : {};
|
|
4693
|
+
const running = await stackExists(dir);
|
|
4694
|
+
if (envText) {
|
|
4695
|
+
if (interactive) {
|
|
4696
|
+
const action = await ask(select({
|
|
4697
|
+
message: `mybase is already set up in ${tilde(dir)}.`,
|
|
4698
|
+
options: [
|
|
4699
|
+
{ value: "reconfigure", label: "Change its settings", hint: "keys and data are kept" },
|
|
4700
|
+
{ value: "cancel", label: "Leave it as it is" }
|
|
4701
|
+
]
|
|
4702
|
+
}));
|
|
4703
|
+
if (action === "cancel") {
|
|
4704
|
+
outro("Nothing changed.");
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
} else
|
|
4708
|
+
ui.info(`Updating the existing setup in ${dir} (keys are kept).`);
|
|
4709
|
+
}
|
|
4710
|
+
const updates = {};
|
|
4711
|
+
const secrets = generateSecrets(existing);
|
|
4712
|
+
Object.assign(updates, secrets);
|
|
4713
|
+
ui.note(Object.keys(SECRET_KEYS).map((key) => {
|
|
4714
|
+
const value = secrets[key] ?? existing[key];
|
|
4715
|
+
return `${key.padEnd(20)} ${dim(maskSecret(value))} ${secrets[key] ? styleText3("green", "new") : dim("kept")}`;
|
|
4716
|
+
}).join(`
|
|
4717
|
+
`), "Keys");
|
|
4718
|
+
let lan = Boolean(existing.MYBASE_PUBLIC_HOST);
|
|
4719
|
+
let tunnel = existing.CLOUDFLARE_TUNNEL_TOKEN ? { apiHost: (existing.MYBASE_PUBLIC_URL ?? "").replace(/^https?:\/\//, ""), s3Host: (existing.MYBASE_S3_PUBLIC_URL ?? "").replace(/^https?:\/\//, "") } : null;
|
|
4720
|
+
const tunnelToken = flag(args, "--tunnel-token");
|
|
4721
|
+
const publicHost = flag(args, "--public-host");
|
|
4722
|
+
if (interactive) {
|
|
4723
|
+
const port = existing.MYBASE_PORT || "4000";
|
|
4724
|
+
const reach = await ask(multiselect({
|
|
4725
|
+
message: `Who should reach mybase? ${dim(`(this machine always can, at localhost:${port})`)}`,
|
|
4726
|
+
required: false,
|
|
4727
|
+
initialValues: [...lan ? ["lan"] : [], ...tunnel ? ["tunnel"] : []],
|
|
4728
|
+
options: [
|
|
4729
|
+
{ value: "lan", label: "Devices on my network or Tailscale", hint: "apps on other machines at home" },
|
|
4730
|
+
{ value: "tunnel", label: "The internet, through a Cloudflare Tunnel", hint: "HTTPS on your domain, no router ports, home IP hidden" }
|
|
4731
|
+
]
|
|
4732
|
+
}));
|
|
4733
|
+
lan = reach.includes("lan");
|
|
4734
|
+
updates.MYBASE_PUBLIC_HOST = lan ? await chooseHost(existing.MYBASE_PUBLIC_HOST) : null;
|
|
4735
|
+
if (reach.includes("tunnel")) {
|
|
4736
|
+
const keep = tunnel && await ask(select({
|
|
4737
|
+
message: `A tunnel is set up for ${tunnel.apiHost}.`,
|
|
4738
|
+
options: [
|
|
4739
|
+
{ value: true, label: "Keep it" },
|
|
4740
|
+
{ value: false, label: "Set it up again" }
|
|
4741
|
+
]
|
|
4742
|
+
}));
|
|
4743
|
+
if (!keep) {
|
|
4744
|
+
const result = await tunnelStep();
|
|
4745
|
+
Object.assign(updates, result.env);
|
|
4746
|
+
tunnel = result;
|
|
4747
|
+
}
|
|
4748
|
+
Object.assign(updates, await bindStep(lan, existing));
|
|
4749
|
+
} else if (tunnel) {
|
|
4750
|
+
for (const key of [...TUNNEL_KEYS, ...BIND_KEYS])
|
|
4751
|
+
updates[key] = null;
|
|
4752
|
+
tunnel = null;
|
|
4753
|
+
}
|
|
4754
|
+
} else {
|
|
4755
|
+
if (publicHost) {
|
|
4756
|
+
updates.MYBASE_PUBLIC_HOST = publicHost;
|
|
4757
|
+
lan = true;
|
|
4758
|
+
}
|
|
4759
|
+
if (tunnelToken) {
|
|
4760
|
+
const domain = flag(args, "--domain");
|
|
4761
|
+
if (!tunnelIdFromToken(tunnelToken))
|
|
4762
|
+
throw usageError("--tunnel-token isn't a Cloudflare tunnel token");
|
|
4763
|
+
if (!domain)
|
|
4764
|
+
throw usageError("--tunnel-token needs --domain (serves mybase.<domain> and s3.<domain>)");
|
|
4765
|
+
tunnel = { apiHost: `mybase.${domain}`, s3Host: `s3.${domain}` };
|
|
4766
|
+
Object.assign(updates, tunnelEnv(tunnelToken, tunnel.apiHost, tunnel.s3Host));
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
const env = () => ({ ...existing, ...Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== null)) });
|
|
4770
|
+
const needsAdmin = !running || await setupNeeded(localUrl(env()));
|
|
4771
|
+
let admin = null;
|
|
4772
|
+
if (needsAdmin && interactive)
|
|
4773
|
+
admin = await adminStep();
|
|
4774
|
+
else if (needsAdmin && args.includes("--admin-password-stdin")) {
|
|
4775
|
+
const password = (await Bun.stdin.text()).replace(/\r?\n$/, "");
|
|
4776
|
+
if (password.length < ADMIN_PASSWORD_MIN)
|
|
4777
|
+
throw usageError(`the admin password must be at least ${ADMIN_PASSWORD_MIN} characters`);
|
|
4778
|
+
admin = { username: flag(args, "--admin-user") ?? "admin", password };
|
|
4779
|
+
}
|
|
4780
|
+
const boot = await bootStep(existing, interactive, args);
|
|
4781
|
+
updates.MYBASE_RESTART = boot.always ? null : "no";
|
|
4782
|
+
if (interactive) {
|
|
4783
|
+
const advanced = await ask(confirm2({ message: `Change advanced settings? ${dim("(ports, cache memory, storage size, log level)")}`, initialValue: false }));
|
|
4784
|
+
if (advanced)
|
|
4785
|
+
Object.assign(updates, await advancedStep(env()));
|
|
4786
|
+
} else {
|
|
4787
|
+
const port = flag(args, "--port");
|
|
4788
|
+
if (port) {
|
|
4789
|
+
if (validPort(port))
|
|
4790
|
+
throw usageError("--port needs a port number");
|
|
4791
|
+
updates.MYBASE_PORT = port;
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
if (!running)
|
|
4795
|
+
Object.assign(updates, await portStep(env(), interactive));
|
|
4796
|
+
const final = envText ? mergeEnv(envText, updates) : renderEnv(env());
|
|
4797
|
+
const values = parseEnv(final);
|
|
4798
|
+
ui.note([
|
|
4799
|
+
...Object.entries(values).map(([k, v]) => `${k.padEnd(24)} ${isSecretKey(k) ? dim(maskSecret(v)) : v}`),
|
|
4800
|
+
"",
|
|
4801
|
+
dim(`Writes ${tilde(join3(dir, ".env"))} (only you can read it) and ${tilde(join3(dir, "compose.yml"))}`)
|
|
4802
|
+
].join(`
|
|
4803
|
+
`), "Settings");
|
|
4804
|
+
if (interactive && !await ask(confirm2({ message: running ? "Save and restart mybase?" : "Save and start mybase?" }))) {
|
|
4805
|
+
outro("Nothing changed.");
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
await writeFiles(dir, final, interactive, yes);
|
|
4809
|
+
await saveServerDir(dir);
|
|
4810
|
+
ui.success(`Saved ${tilde(dir)}`);
|
|
4811
|
+
if (boot.enableDocker)
|
|
4812
|
+
await turnOnDockerAtBoot();
|
|
4813
|
+
const version = await startStack(dir, values);
|
|
4814
|
+
const url = localUrl(values);
|
|
4815
|
+
let signedIn = null;
|
|
4816
|
+
if (admin) {
|
|
4817
|
+
const s = ui.spinner();
|
|
4818
|
+
s.start("Creating the admin account");
|
|
4819
|
+
signedIn = await createAdmin(url, admin);
|
|
4820
|
+
s.stop(`Admin ${signedIn} created, and this CLI is signed in`);
|
|
4821
|
+
}
|
|
4822
|
+
const port = values.MYBASE_PORT || "4000";
|
|
4823
|
+
const urls = {
|
|
4824
|
+
local: url,
|
|
4825
|
+
network: values.MYBASE_PUBLIC_HOST ? `http://${values.MYBASE_PUBLIC_HOST}:${port}` : null,
|
|
4826
|
+
public: tunnel ? `https://${tunnel.apiHost}` : null,
|
|
4827
|
+
s3: values.MYBASE_S3_PUBLIC_URL ?? (values.MYBASE_PUBLIC_HOST ? `http://${values.MYBASE_PUBLIC_HOST}:${values.GARAGE_S3_PORT || "3900"}` : null)
|
|
4828
|
+
};
|
|
4829
|
+
if (jsonMode) {
|
|
4830
|
+
return print({ dir, version, urls, autostart: boot.always, adminCreated: Boolean(signedIn), signedIn: Boolean(signedIn), secretsKeyGenerated: Boolean(secrets.MYBASE_SECRETS_KEY) }, () => {});
|
|
4831
|
+
}
|
|
4832
|
+
ui.note([
|
|
4833
|
+
`${"Dashboard".padEnd(16)} ${cyan(url)}`,
|
|
4834
|
+
...urls.network ? [`${"On your network".padEnd(16)} ${cyan(urls.network)}`] : [],
|
|
4835
|
+
...urls.public ? [`${"On the internet".padEnd(16)} ${cyan(urls.public)} ${dim(`(S3: ${urls.s3})`)}`] : [],
|
|
4836
|
+
`${"Starts on boot".padEnd(16)} ${boot.always ? dockerOnBoot() === "disabled" ? yellow("no: Docker doesn't start at boot") : "yes" : "no"}`,
|
|
4837
|
+
`${"CLI".padEnd(16)} ${signedIn ? `signed in as ${signedIn}` : `run ${bold(`mybase login --url ${url}`)} after creating the admin`}`,
|
|
4838
|
+
"",
|
|
4839
|
+
bold("Next"),
|
|
4840
|
+
...signedIn ? [] : [` Open ${cyan(url)} and create the admin account`],
|
|
4841
|
+
` ${bold("mybase project create my-app")} a database + bucket + credentials`,
|
|
4842
|
+
` ${bold("mybase agent init")} teach your coding agent about it`,
|
|
4843
|
+
` Turn on two-factor sign-in in Settings${tunnel ? yellow(" (do this before sharing the public URL)") : ""}`,
|
|
4844
|
+
` ${bold("mybase server status")} \xB7 ${bold("mybase server update")}`
|
|
4845
|
+
].join(`
|
|
4846
|
+
`), "mybase is running");
|
|
4847
|
+
if (secrets.MYBASE_SECRETS_KEY) {
|
|
4848
|
+
ui.warn(`${bold("Back up MYBASE_SECRETS_KEY")} (in ${tilde(join3(dir, ".env"))}) somewhere other than this machine,
|
|
4849
|
+
like a password manager. If it's lost, every stored secret is lost with it.`);
|
|
4850
|
+
if (interactive && await ask(confirm2({ message: "Show it now so you can copy it?", initialValue: false }))) {
|
|
4851
|
+
log.message(secrets.MYBASE_SECRETS_KEY);
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
outro(`Done. Config for this CLI: ${dim(tilde(configPath))}`);
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
// src/server.ts
|
|
4858
|
+
var serverUsage = ` server setup [--dir <dir>] Install mybase on this machine: checks Docker, generates keys, sets up
|
|
4859
|
+
LAN/Tailscale access, a Cloudflare Tunnel, start on boot, and the admin
|
|
4860
|
+
[--yes] [--public-host <h>] [--tunnel-token <t> --domain <d>] [--port <n>] [--no-autostart]
|
|
4861
|
+
[--admin-user <u> --admin-password-stdin] Without a terminal: defaults plus these flags
|
|
4862
|
+
server status [--dir <dir>] Containers, health, and whether mybase starts on boot
|
|
4863
|
+
server update [--dir <dir>] Download new images and restart (also refreshes compose.yml)
|
|
4864
|
+
server tunnel [--dir <dir>] Add or replace the Cloudflare Tunnel [--tunnel-token <t> --domain <d>] | --off
|
|
4865
|
+
server autostart on|off Start mybase when this machine boots (and restart it after crashes)
|
|
4866
|
+
Default dir: the last one setup used, else ~/mybase`;
|
|
4867
|
+
var dim2 = (s) => styleText4("dim", s);
|
|
4868
|
+
var green = (s) => styleText4("green", s);
|
|
4869
|
+
var red = (s) => styleText4("red", s);
|
|
4870
|
+
async function loadEnv(dir) {
|
|
4871
|
+
const file = Bun.file(join4(dir, ".env"));
|
|
4872
|
+
if (!await file.exists() || !await Bun.file(join4(dir, "compose.yml")).exists()) {
|
|
4873
|
+
throw new CliError(`no mybase server in ${dir}. Run \`mybase server setup\` (or pass --dir).`, 4);
|
|
4874
|
+
}
|
|
4875
|
+
const text = await file.text();
|
|
4876
|
+
return { text, values: parseEnv(text) };
|
|
4877
|
+
}
|
|
4878
|
+
async function saveEnv(dir, text) {
|
|
4879
|
+
await Bun.write(join4(dir, ".env"), text);
|
|
4880
|
+
}
|
|
4881
|
+
async function status(args) {
|
|
4882
|
+
const dir = await serverDir(args);
|
|
4883
|
+
const { values } = await loadEnv(dir);
|
|
4884
|
+
const ps = await compose(dir, ["ps", "-a", "--format", "json"]);
|
|
4885
|
+
if (!ps.ok)
|
|
4886
|
+
throw new CliError(`docker compose ps failed: ${ps.stderr}`);
|
|
4887
|
+
const containers = ps.stdout.startsWith("[") ? JSON.parse(ps.stdout) : ps.stdout.split(`
|
|
4888
|
+
`).filter(Boolean).map((l) => JSON.parse(l));
|
|
4889
|
+
const url = localUrl(values);
|
|
4890
|
+
const health = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) }).then(async (r) => r.ok ? (await r.json()).version ?? "unknown" : null).catch(() => null);
|
|
4891
|
+
const autostart = values.MYBASE_RESTART !== "no";
|
|
4892
|
+
const docker = dockerOnBoot();
|
|
4893
|
+
const data = {
|
|
4894
|
+
dir,
|
|
4895
|
+
url,
|
|
4896
|
+
healthy: health !== null,
|
|
4897
|
+
version: health,
|
|
4898
|
+
autostart: autostart && docker !== "disabled",
|
|
4899
|
+
dockerOnBoot: docker,
|
|
4900
|
+
publicUrl: values.CLOUDFLARE_TUNNEL_TOKEN ? values.MYBASE_PUBLIC_URL ?? null : null,
|
|
4901
|
+
containers: containers.map((c) => ({ service: c.Service, state: c.State, health: c.Health || null }))
|
|
4902
|
+
};
|
|
4903
|
+
print(data, () => {
|
|
4904
|
+
console.log(`mybase in ${dir}`);
|
|
4905
|
+
console.log(` ${health ? green("\u25CF") : red("\u25CF")} ${url} ${health ? `version ${health}` : "not answering"}`);
|
|
4906
|
+
if (data.publicUrl)
|
|
4907
|
+
console.log(` ${dim2("public")} ${data.publicUrl}`);
|
|
4908
|
+
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"}`);
|
|
4909
|
+
console.log("");
|
|
4910
|
+
if (containers.length === 0)
|
|
4911
|
+
console.log(" No containers. Start them: mybase server update");
|
|
4912
|
+
for (const c of data.containers) {
|
|
4913
|
+
const up = c.state === "running" && (c.health === null || c.health === "healthy");
|
|
4914
|
+
console.log(` ${up ? green("\u25CF") : red("\u25CF")} ${c.service.padEnd(12)} ${c.state}${c.health ? ` (${c.health})` : ""}`);
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4917
|
+
if (!health)
|
|
4918
|
+
process.exitCode = 1;
|
|
4919
|
+
}
|
|
4920
|
+
async function update(args) {
|
|
4921
|
+
const dir = await serverDir(args);
|
|
4922
|
+
const { values } = await loadEnv(dir);
|
|
4923
|
+
const interactive = !jsonMode && process.stdin.isTTY === true && !args.includes("--yes");
|
|
4924
|
+
if (!jsonMode)
|
|
4925
|
+
intro(`${styleText4("bold", " mybase ")} ${dim2("server update")}`);
|
|
4926
|
+
const replaced = await writeCompose(dir, interactive, true);
|
|
4927
|
+
if (replaced)
|
|
4928
|
+
ui.success(`compose.yml updated to this CLI's (${CLI_VERSION})`);
|
|
4929
|
+
const version = await startStack(dir, values);
|
|
4930
|
+
const minor = (v) => v.split(".").slice(0, 2).join(".");
|
|
4931
|
+
if (minor(version) !== minor(CLI_VERSION)) {
|
|
4932
|
+
ui.warn(`The server is ${version}, this CLI ${CLI_VERSION}. Update the CLI: curl -fsSL https://raw.githubusercontent.com/LandonHarter/mybase/master/scripts/install.sh | sh`);
|
|
4933
|
+
}
|
|
4934
|
+
if (jsonMode)
|
|
4935
|
+
return print({ dir, version, composeUpdated: replaced }, () => {});
|
|
4936
|
+
outro(`mybase ${version} is running at ${localUrl(values)}`);
|
|
4937
|
+
}
|
|
4938
|
+
async function tunnel(args) {
|
|
4939
|
+
const dir = await serverDir(args);
|
|
4940
|
+
const { text, values } = await loadEnv(dir);
|
|
4941
|
+
const interactive = !jsonMode && process.stdin.isTTY === true;
|
|
4942
|
+
if (!jsonMode)
|
|
4943
|
+
intro(`${styleText4("bold", " mybase ")} ${dim2("server tunnel")}`);
|
|
4944
|
+
let updates;
|
|
4945
|
+
let host = null;
|
|
4946
|
+
if (args.includes("--off")) {
|
|
4947
|
+
updates = Object.fromEntries(["COMPOSE_PROFILES", "CLOUDFLARE_TUNNEL_TOKEN", "MYBASE_PUBLIC_URL", "MYBASE_S3_PUBLIC_URL", "MYBASE_TRUST_PROXY", "POSTGRES_BIND", "GARAGE_S3_BIND", "VALKEY_BIND"].map((k) => [k, null]));
|
|
4948
|
+
} else if (flag(args, "--tunnel-token")) {
|
|
4949
|
+
const token = flag(args, "--tunnel-token");
|
|
4950
|
+
const domain = flag(args, "--domain");
|
|
4951
|
+
if (!tunnelIdFromToken(token))
|
|
4952
|
+
throw usageError("--tunnel-token isn't a Cloudflare tunnel token");
|
|
4953
|
+
if (!domain)
|
|
4954
|
+
throw usageError("--tunnel-token needs --domain (serves mybase.<domain> and s3.<domain>)");
|
|
4955
|
+
host = `mybase.${domain}`;
|
|
4956
|
+
updates = tunnelEnv(token, host, `s3.${domain}`);
|
|
4957
|
+
} else if (interactive) {
|
|
4958
|
+
const result = await tunnelStep();
|
|
4959
|
+
host = result.apiHost;
|
|
4960
|
+
updates = result.env;
|
|
4961
|
+
} else
|
|
4962
|
+
throw usageError("without a terminal, pass --tunnel-token <t> --domain <d>, or --off");
|
|
4963
|
+
await saveEnv(dir, mergeEnv(text, updates));
|
|
4964
|
+
if (args.includes("--off"))
|
|
4965
|
+
await compose(dir, ["--profile", "tunnel", "rm", "-sf", "cloudflared"]);
|
|
4966
|
+
const version = await startStack(dir, { ...values }, !args.includes("--off"));
|
|
4967
|
+
if (jsonMode)
|
|
4968
|
+
return print({ dir, version, publicUrl: host ? `https://${host}` : null }, () => {});
|
|
4969
|
+
outro(host ? `Public at https://${host}. Turn on two-factor sign-in in Settings if you haven't.` : "Tunnel off. mybase is reachable on this machine and your network only.");
|
|
4970
|
+
}
|
|
4971
|
+
async function autostart(args) {
|
|
4972
|
+
const mode = args[0];
|
|
4973
|
+
if (mode !== "on" && mode !== "off")
|
|
4974
|
+
throw usageError("usage: mybase server autostart on|off");
|
|
4975
|
+
const dir = await serverDir(args);
|
|
4976
|
+
const { text, values } = await loadEnv(dir);
|
|
4977
|
+
await saveEnv(dir, mergeEnv(text, { MYBASE_RESTART: mode === "on" ? null : "no" }));
|
|
4978
|
+
if (mode === "on" && dockerOnBoot() === "disabled") {
|
|
4979
|
+
if (!jsonMode && process.stdin.isTTY)
|
|
4980
|
+
await turnOnDockerAtBoot();
|
|
4981
|
+
else
|
|
4982
|
+
ui.warn("Docker doesn't start at boot, so mybase won't either. Run `sudo systemctl enable docker`.");
|
|
4983
|
+
}
|
|
4984
|
+
const up = await compose(dir, ["up", "-d"]);
|
|
4985
|
+
if (!up.ok)
|
|
4986
|
+
throw new CliError(`docker compose up failed:
|
|
4987
|
+
${up.stderr}`);
|
|
4988
|
+
if (!await waitHealthy(localUrl(values), 60000))
|
|
4989
|
+
ui.warn("mybase didn't answer yet; check `mybase server status`.");
|
|
4990
|
+
print({ dir, autostart: mode === "on", dockerOnBoot: dockerOnBoot() }, () => console.log(mode === "on" ? "mybase now starts when this machine boots, and restarts if it crashes." : "mybase won't start on its own after a reboot. Start it with `mybase server update`."));
|
|
4991
|
+
}
|
|
4992
|
+
async function server(command, args) {
|
|
4993
|
+
if (command === "setup")
|
|
4994
|
+
await setup(args);
|
|
4995
|
+
else if (command === "status")
|
|
4996
|
+
await status(args);
|
|
4997
|
+
else if (command === "update")
|
|
4998
|
+
await update(args);
|
|
4999
|
+
else if (command === "tunnel")
|
|
5000
|
+
await tunnel(args);
|
|
5001
|
+
else if (command === "autostart")
|
|
5002
|
+
await autostart(args);
|
|
5003
|
+
else
|
|
5004
|
+
return false;
|
|
5005
|
+
return true;
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
// src/users.ts
|
|
5009
|
+
var usersUsage = ` users enable <project> Turn on users: an auth schema for Better Auth, plus setup steps
|
|
5010
|
+
users init <project> [--env <secrets env>] Print an auth.ts for Better Auth; --env stores a new BETTER_AUTH_SECRET there
|
|
5011
|
+
users list <project> [--search <text>] The app's users, newest first
|
|
5012
|
+
users show <project> <user-id> One user: sign-in methods and active sessions
|
|
5013
|
+
users revoke <project> <user-id> [<session-id>] Sign a user out (one session, or everywhere)
|
|
5014
|
+
users delete <project> <user-id> [--yes] Delete a user and their sessions
|
|
5015
|
+
users disable <project> [--yes] Turn it off, deleting every user account`;
|
|
5016
|
+
var base8 = (project) => `/projects/${encodeURIComponent(project)}/users`;
|
|
5017
|
+
function authSnippet(project, apiUrl) {
|
|
5018
|
+
return `// auth.ts: npm i better-auth pg @mybase-dev/sdk
|
|
5019
|
+
import { betterAuth } from "better-auth";
|
|
5020
|
+
import { Pool } from "pg";
|
|
5021
|
+
import { createCache } from "@mybase-dev/sdk/cache";
|
|
5022
|
+
import { authDatabaseUrl, mybaseAuth, mybaseSecondaryStorage } from "@mybase-dev/sdk/better-auth";
|
|
5023
|
+
|
|
5024
|
+
const mybase = { apiUrl: "${apiUrl}", project: "${project}", apiKey: process.env.MYBASE_API_KEY! };
|
|
5025
|
+
|
|
5026
|
+
export const auth = betterAuth({
|
|
5027
|
+
// Better Auth's tables go in the "auth" schema of the project's database.
|
|
5028
|
+
database: new Pool({ connectionString: authDatabaseUrl(process.env.DATABASE_URL!) }),
|
|
5029
|
+
// Sessions and rate limits in the mybase cache (turn on the cache service first).
|
|
5030
|
+
secondaryStorage: mybaseSecondaryStorage(createCache(mybase)),
|
|
5031
|
+
// Keep a copy in Postgres too, so the dashboard can list and revoke sessions.
|
|
5032
|
+
session: { storeSessionInDatabase: true },
|
|
5033
|
+
emailAndPassword: { enabled: true },
|
|
5034
|
+
// Sign-ups and sign-ins in the mybase audit log.
|
|
5035
|
+
plugins: [mybaseAuth(mybase)],
|
|
5036
|
+
});
|
|
5037
|
+
|
|
5038
|
+
// Then create the tables: npx @better-auth/cli migrate`;
|
|
5039
|
+
}
|
|
5040
|
+
async function users(sub, args) {
|
|
5041
|
+
const { positional, flags } = parseArgs(args);
|
|
5042
|
+
const [project, userId, sessionId] = positional;
|
|
5043
|
+
switch (sub) {
|
|
5044
|
+
case "enable": {
|
|
5045
|
+
const p = need(project, "project name");
|
|
5046
|
+
await api(`/projects/${encodeURIComponent(p)}/services/users`, { method: "PUT" });
|
|
5047
|
+
const o = await api(base8(p));
|
|
5048
|
+
print({ enabled: "users", project: p, ...o }, () => {
|
|
5049
|
+
console.log(`Users enabled for "${p}". Better Auth's tables go in the "${o.schema}" schema.
|
|
5050
|
+
`);
|
|
5051
|
+
console.log("Next:");
|
|
5052
|
+
console.log(` 1. An API key for the app: mybase token create ${p}-auth --project ${p} --perm users:write,cache:write`);
|
|
5053
|
+
console.log(` 2. The cache, for sessions: mybase cache enable ${p}`);
|
|
5054
|
+
console.log(` 3. An auth.ts to start from: mybase users init ${p}`);
|
|
5055
|
+
});
|
|
5056
|
+
return true;
|
|
5057
|
+
}
|
|
5058
|
+
case "init": {
|
|
5059
|
+
const p = need(project, "project name");
|
|
5060
|
+
const o = await api(base8(p));
|
|
5061
|
+
const env = flagString(flags, "env");
|
|
5062
|
+
let stored = null;
|
|
5063
|
+
if (env) {
|
|
5064
|
+
const secret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64");
|
|
5065
|
+
await api(`/projects/${encodeURIComponent(p)}/secrets/envs/${encodeURIComponent(env)}/secrets`, {
|
|
5066
|
+
method: "PATCH",
|
|
5067
|
+
body: JSON.stringify({ set: { BETTER_AUTH_SECRET: secret }, message: "mybase users init" })
|
|
5068
|
+
});
|
|
5069
|
+
stored = env;
|
|
5070
|
+
}
|
|
5071
|
+
const snippet = authSnippet(p, o.setup.apiUrl);
|
|
5072
|
+
print({ project: p, snippet, storedSecretIn: stored }, () => {
|
|
5073
|
+
console.log(snippet);
|
|
5074
|
+
console.log(stored ? `
|
|
5075
|
+
Stored a new BETTER_AUTH_SECRET in secrets env "${stored}". Run your app with: mybase run -e ${stored} -- <command>` : `
|
|
5076
|
+
Set BETTER_AUTH_SECRET to 32+ random bytes (openssl rand -base64 32), or rerun with --env <secrets env> to store one.`);
|
|
5077
|
+
});
|
|
5078
|
+
return true;
|
|
5079
|
+
}
|
|
5080
|
+
case "list": {
|
|
5081
|
+
const p = need(project, "project name");
|
|
5082
|
+
const search = flagString(flags, "search");
|
|
5083
|
+
const list = await api(`${base8(p)}/accounts?limit=100${search ? `&search=${encodeURIComponent(search)}` : ""}`);
|
|
5084
|
+
print(list, () => {
|
|
5085
|
+
if (list.users.length === 0)
|
|
5086
|
+
console.log(search ? "No users match." : "No users yet.");
|
|
5087
|
+
for (const u of list.users)
|
|
5088
|
+
console.log(`${u.id} ${u.email ?? "-"} ${u.name ?? "-"} ${u.createdAt ?? ""}${u.banned ? "\tbanned" : ""}`);
|
|
5089
|
+
if (list.total > list.users.length)
|
|
5090
|
+
console.log(`\u2026 and ${list.total - list.users.length} more (narrow it with --search)`);
|
|
5091
|
+
});
|
|
5092
|
+
return true;
|
|
5093
|
+
}
|
|
5094
|
+
case "show": {
|
|
5095
|
+
const p = need(project, "project name");
|
|
5096
|
+
const u = await api(`${base8(p)}/accounts/${encodeURIComponent(need(userId, "user id"))}`);
|
|
5097
|
+
print(u, () => {
|
|
5098
|
+
console.log(`${u.email ?? u.id}${u.name ? ` (${u.name})` : ""}${u.banned ? " BANNED" : ""}`);
|
|
5099
|
+
console.log(` id ${u.id}`);
|
|
5100
|
+
console.log(` created ${u.createdAt ?? "-"}`);
|
|
5101
|
+
console.log(` verified ${u.emailVerified ?? "-"}`);
|
|
5102
|
+
console.log(` sign-in ${u.providers.join(", ") || "-"}`);
|
|
5103
|
+
console.log(`
|
|
5104
|
+
Active sessions (${u.sessions.length}):`);
|
|
5105
|
+
for (const s of u.sessions)
|
|
5106
|
+
console.log(` ${s.id} since ${s.createdAt ?? "?"} ${s.ipAddress ?? ""} ${s.userAgent ?? ""}`);
|
|
5107
|
+
});
|
|
5108
|
+
return true;
|
|
5109
|
+
}
|
|
5110
|
+
case "revoke": {
|
|
5111
|
+
const p = need(project, "project name");
|
|
5112
|
+
const id = encodeURIComponent(need(userId, "user id"));
|
|
5113
|
+
if (sessionId) {
|
|
5114
|
+
await api(`${base8(p)}/accounts/${id}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
5115
|
+
print({ revoked: 1 }, () => console.log("Signed out that session."));
|
|
5116
|
+
} else {
|
|
5117
|
+
const r = await api(`${base8(p)}/accounts/${id}/sessions`, { method: "DELETE" });
|
|
5118
|
+
print(r, () => console.log(`Signed out ${r.revoked} session(s).`));
|
|
5119
|
+
}
|
|
5120
|
+
return true;
|
|
5121
|
+
}
|
|
5122
|
+
case "delete": {
|
|
5123
|
+
const p = need(project, "project name");
|
|
5124
|
+
const id = need(userId, "user id");
|
|
5125
|
+
confirm(args.includes("--yes"), `This deletes user ${id} of "${p}" and signs them out.`, id, "the user id");
|
|
5126
|
+
await api(`${base8(p)}/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
5127
|
+
print({ deleted: id }, () => console.log(`Deleted user ${id}`));
|
|
5128
|
+
return true;
|
|
5129
|
+
}
|
|
5130
|
+
case "disable": {
|
|
5131
|
+
const p = need(project, "project name");
|
|
5132
|
+
confirm(args.includes("--yes"), `This deletes every user account, session, and sign-in method of "${p}" (the auth schema).`, p, "the project name");
|
|
5133
|
+
await api(`/projects/${encodeURIComponent(p)}/services/users`, { method: "DELETE" });
|
|
5134
|
+
print({ disabled: "users", project: p }, () => console.log(`Users disabled for "${p}"`));
|
|
5135
|
+
return true;
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
return false;
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
// src/index.ts
|
|
5142
|
+
var projectUsage = ` project create <name> Create a new project (database + storage bucket)
|
|
5143
|
+
project list List projects
|
|
5144
|
+
project delete <name> [--yes] Delete a project, its database, and all its files`;
|
|
5145
|
+
var USAGE = {
|
|
5146
|
+
auth: authUsage,
|
|
5147
|
+
server: serverUsage,
|
|
5148
|
+
token: tokenUsage,
|
|
5149
|
+
project: projectUsage,
|
|
5150
|
+
context: contextUsage,
|
|
5151
|
+
doctor: doctorUsage,
|
|
5152
|
+
audit: auditUsage,
|
|
5153
|
+
alerts: alertsUsage,
|
|
5154
|
+
docs: docsUsage,
|
|
5155
|
+
mcp: mcpUsage,
|
|
5156
|
+
agent: agentUsage,
|
|
5157
|
+
cron: cronUsage,
|
|
5158
|
+
analytics: analyticsUsage,
|
|
5159
|
+
cache: cacheUsage,
|
|
5160
|
+
ratelimit: ratelimitUsage,
|
|
5161
|
+
queue: queueUsage,
|
|
5162
|
+
search: searchUsage,
|
|
5163
|
+
secrets: secretsUsage,
|
|
5164
|
+
vector: vectorUsage,
|
|
5165
|
+
users: usersUsage,
|
|
5166
|
+
captcha: captchaUsage,
|
|
5167
|
+
run: runUsage
|
|
5168
|
+
};
|
|
5169
|
+
var GLOBAL_FLAGS = ` --json Print results as JSON (errors too, on stderr)
|
|
5170
|
+
--yes Skip confirmations (required without a terminal)
|
|
5171
|
+
MYBASE_API_URL, MYBASE_TOKEN Override the saved server and token
|
|
5172
|
+
Exit codes: 0 ok, 1 failure, 2 usage, 3 not signed in / bad token, 4 not found`;
|
|
5173
|
+
var usage = `Usage: mybase <command> [--json]
|
|
5174
|
+
|
|
5175
|
+
Commands:
|
|
5176
|
+
${Object.values(USAGE).join(`
|
|
5177
|
+
`)}
|
|
5178
|
+
help [<command>] Show this message, or one command's
|
|
5179
|
+
|
|
5180
|
+
Flags:
|
|
5181
|
+
${GLOBAL_FLAGS}`;
|
|
5182
|
+
async function main(args) {
|
|
5183
|
+
const [command, sub, ...rest] = args;
|
|
5184
|
+
if (command === "help" && sub) {
|
|
5185
|
+
const key = ["login", "logout", "whoami", "version"].includes(sub) ? "auth" : sub;
|
|
5186
|
+
if (USAGE[key])
|
|
5187
|
+
return console.log(USAGE[key]);
|
|
5188
|
+
}
|
|
5189
|
+
if (command && await auth(command, args.slice(1)))
|
|
5190
|
+
return;
|
|
5191
|
+
if (command === "docs" && await docs(args.slice(1)))
|
|
5192
|
+
return;
|
|
5193
|
+
if (command === "context" && await context(args.slice(1)))
|
|
5194
|
+
return;
|
|
5195
|
+
if (command === "mcp" && await mcp(args.slice(1)))
|
|
5196
|
+
return;
|
|
5197
|
+
if (command === "agent" && sub && await agent(sub, rest))
|
|
5198
|
+
return;
|
|
5199
|
+
if (command === "run")
|
|
5200
|
+
return run(args.slice(1));
|
|
5201
|
+
if (command === "doctor")
|
|
5202
|
+
return doctor(args.slice(1));
|
|
5203
|
+
if (command === "audit")
|
|
5204
|
+
return audit(args.slice(1));
|
|
5205
|
+
if (command === "alerts")
|
|
5206
|
+
return alerts(args.slice(1));
|
|
5207
|
+
if (command === "project" && sub === "create") {
|
|
5208
|
+
const name = need(rest[0], "project name");
|
|
5209
|
+
const project = await api("/projects", {
|
|
5210
|
+
method: "POST",
|
|
5211
|
+
body: JSON.stringify({ name })
|
|
5212
|
+
});
|
|
5213
|
+
const { database, storage } = project.credentials;
|
|
5214
|
+
if (jsonMode)
|
|
5215
|
+
return print(project, () => {});
|
|
5216
|
+
console.log(`Created project "${project.name}"
|
|
5217
|
+
`);
|
|
5218
|
+
console.log(`Postgres:
|
|
5219
|
+
${database.url}
|
|
5220
|
+
`);
|
|
5221
|
+
console.log("Storage (S3):");
|
|
5222
|
+
console.log(` endpoint ${storage.endpoint}`);
|
|
5223
|
+
console.log(` region ${storage.region}`);
|
|
5224
|
+
console.log(` bucket ${storage.bucket}`);
|
|
5225
|
+
console.log(` access key id ${storage.accessKeyId}`);
|
|
1501
5226
|
console.log(` secret access key ${storage.secretAccessKey}`);
|
|
1502
5227
|
console.log(` public URL ${storage.publicUrl}<key> (objects under public/ only)
|
|
1503
5228
|
`);
|
|
@@ -1521,6 +5246,10 @@ async function main(args) {
|
|
|
1521
5246
|
print({ deleted: name }, () => console.log(`Deleted project "${name}"`));
|
|
1522
5247
|
return;
|
|
1523
5248
|
}
|
|
5249
|
+
if (command === "server" && sub) {
|
|
5250
|
+
if (await server(sub, rest))
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
1524
5253
|
if (command === "token" && sub) {
|
|
1525
5254
|
if (await token(sub, rest))
|
|
1526
5255
|
return;
|
|
@@ -1545,6 +5274,26 @@ async function main(args) {
|
|
|
1545
5274
|
if (await queue(sub, rest))
|
|
1546
5275
|
return;
|
|
1547
5276
|
}
|
|
5277
|
+
if (command === "search" && sub) {
|
|
5278
|
+
if (await search(sub, rest))
|
|
5279
|
+
return;
|
|
5280
|
+
}
|
|
5281
|
+
if (command === "secrets" && sub) {
|
|
5282
|
+
if (await secrets(sub, rest))
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
if (command === "vector" && sub) {
|
|
5286
|
+
if (await vector(sub, rest))
|
|
5287
|
+
return;
|
|
5288
|
+
}
|
|
5289
|
+
if (command === "users" && sub) {
|
|
5290
|
+
if (await users(sub, rest))
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
if (command === "captcha" && sub) {
|
|
5294
|
+
if (await captcha(sub, rest))
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
1548
5297
|
if (command && command !== "help")
|
|
1549
5298
|
throw usageError(`unknown command: mybase ${args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
|
|
1550
5299
|
|
|
@@ -1552,7 +5301,9 @@ ${usage}`);
|
|
|
1552
5301
|
console.log(usage);
|
|
1553
5302
|
}
|
|
1554
5303
|
if (import.meta.main) {
|
|
1555
|
-
|
|
5304
|
+
const args = Bun.argv.slice(2);
|
|
5305
|
+
const own = ownArgs(args);
|
|
5306
|
+
main([...own.filter((a) => a !== "--json"), ...args.slice(own.length)]).catch((err) => {
|
|
1556
5307
|
const code = err instanceof CliError ? err.exitCode : 1;
|
|
1557
5308
|
if (jsonMode)
|
|
1558
5309
|
console.error(JSON.stringify({ error: err.message.split(`
|