@easybits.cloud/cli 0.4.1 → 0.5.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/index.js CHANGED
@@ -1,170 +1,636 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import {
5
- EasybitsError,
6
- writeRcConfig,
7
- resolveApiKey,
8
- resolveBaseUrl,
9
- createClientFromEnv
10
- } from "@easybits.cloud/sdk";
11
- import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync } from "fs";
12
- function flag(name) {
13
- const i = process.argv.indexOf(name);
14
- return i > -1 ? process.argv[i + 1] : void 0;
15
- }
16
- async function getClient() {
4
+ import { parseArgs } from "util";
5
+
6
+ // src/commands/account.ts
7
+ import { resolveBaseUrl as resolveBaseUrl3 } from "@easybits.cloud/sdk";
8
+
9
+ // src/errors.ts
10
+ import { EasybitsError } from "@easybits.cloud/sdk";
11
+ var EXIT = { OK: 0, API: 1, USAGE: 2, AUTH: 3 };
12
+ var CliError = class extends Error {
13
+ constructor(message, exitCode = EXIT.API, hint, code = "error", status) {
14
+ super(message);
15
+ this.exitCode = exitCode;
16
+ this.hint = hint;
17
+ this.code = code;
18
+ this.status = status;
19
+ }
20
+ exitCode;
21
+ hint;
22
+ code;
23
+ status;
24
+ };
25
+ function usageError(message, usage2) {
26
+ return new CliError(
27
+ message,
28
+ EXIT.USAGE,
29
+ usage2 ? `Usage: ${usage2}` : "Run: easybits --help",
30
+ "usage"
31
+ );
32
+ }
33
+ function notLoggedIn() {
34
+ return new CliError(
35
+ "Not logged in.",
36
+ EXIT.AUTH,
37
+ "Run: easybits login (or: easybits login <api-key>, or set EASYBITS_API_KEY)",
38
+ "not_logged_in"
39
+ );
40
+ }
41
+ function apiMessage(body) {
17
42
  try {
18
- return await createClientFromEnv();
43
+ const j = JSON.parse(body);
44
+ if (typeof j.error === "string" && typeof j.message === "string") return { message: `${j.error}: ${j.message}`, kind: j.error };
45
+ if (typeof j.code === "string" && typeof j.error === "string") return { message: `${j.code}: ${j.error}`, kind: j.code };
46
+ const msg = j.message ?? j.error ?? j.detail;
47
+ if (typeof msg === "string") return { message: msg };
48
+ if (msg) return { message: JSON.stringify(msg) };
19
49
  } catch {
20
- console.error("Not logged in. Run: easybits login <api-key>");
21
- process.exit(1);
22
50
  }
51
+ return { message: body.trim() || "empty response" };
23
52
  }
24
- async function login() {
25
- const key = process.argv[3];
26
- if (!key) {
27
- console.error("Usage: easybits login <api-key>");
28
- process.exit(1);
53
+ var KIND_HINTS = {
54
+ SandboxBusy: "A snapshot or fork is running on this sandbox. Wait a few minutes and retry (easybits sb get <id> shows `activity`).",
55
+ SandboxUnreachable: "The sandbox is not answering inside. Wait and retry; if it persists, destroy it.",
56
+ SandboxHostTimeout: "The operation may still be running. Check `easybits sb get <id>` and retry in a few minutes.",
57
+ SandboxHostError: "The sandbox host failed. Retry; if it persists, contact support.",
58
+ SQL_ERROR: "Fix the SQL statement.",
59
+ DATABASE_STORAGE_MISSING: "This database has no storage; its data is unavailable. Delete it (easybits db rm) and create a new one.",
60
+ DATABASE_BACKEND_ERROR: "The database backend failed. Retry in a moment."
61
+ };
62
+ function toCliError(err) {
63
+ if (err instanceof CliError) return err;
64
+ if (err instanceof EasybitsError) {
65
+ const { message: message2, kind } = apiMessage(err.body);
66
+ if (err.status === 401) {
67
+ return new CliError(
68
+ `Credentials rejected (401): ${message2}`,
69
+ EXIT.AUTH,
70
+ "Run: easybits login (or pass a valid API key from https://www.easybits.cloud/dash/developer)",
71
+ "unauthorized",
72
+ 401
73
+ );
74
+ }
75
+ const hint = (kind && KIND_HINTS[kind]) ?? (err.status === 403 ? "Your key may lack the scope for this action (READ/WRITE/DELETE)." : err.status === 404 ? "Check the id. List what you have with the matching `ls` command." : err.status === 402 ? "Plan limit reached. See: easybits usage" : void 0);
76
+ return new CliError(`API error ${err.status}: ${message2}`, EXIT.API, hint, "api_error", err.status);
29
77
  }
30
- await writeRcConfig({ apiKey: key });
31
- console.log("Saved API key to ~/.easybitsrc");
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ return new CliError(message, EXIT.API);
80
+ }
81
+
82
+ // src/args.ts
83
+ function need(ctx, i, name, usage2) {
84
+ const v = ctx.args[i];
85
+ if (v == null || v === "") throw usageError(`Missing <${name}>.`, usage2);
86
+ return v;
87
+ }
88
+ function str(ctx, key) {
89
+ const v = ctx.opts[key];
90
+ return typeof v === "string" ? v : void 0;
91
+ }
92
+ function bool(ctx, key) {
93
+ return ctx.opts[key] === true;
94
+ }
95
+ function list(ctx, key) {
96
+ const v = ctx.opts[key];
97
+ return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
98
+ }
99
+ function int(ctx, key, usage2) {
100
+ const v = str(ctx, key);
101
+ if (v == null) return void 0;
102
+ const n = Number(v);
103
+ if (!Number.isInteger(n) || n < 0) throw usageError(`--${key} must be a non-negative integer.`, usage2);
104
+ return n;
32
105
  }
33
- async function filesList() {
34
- const client = await getClient();
106
+ function pairs(items, usage2) {
107
+ const out = {};
108
+ for (const item of items) {
109
+ const eq = item.indexOf("=");
110
+ if (eq < 1) throw usageError(`Expected KEY=VALUE, got "${item}".`, usage2);
111
+ out[item.slice(0, eq)] = item.slice(eq + 1);
112
+ }
113
+ return out;
114
+ }
115
+ async function readStdin() {
116
+ const chunks = [];
117
+ for await (const c of process.stdin) chunks.push(c);
118
+ return Buffer.concat(chunks);
119
+ }
120
+
121
+ // src/output.ts
122
+ function emit(ctx, data, human) {
123
+ if (ctx.json) process.stdout.write(JSON.stringify(data, null, 2) + "\n");
124
+ else human();
125
+ }
126
+ function fmtBytes(n) {
127
+ if (n == null) return "-";
128
+ if (n < 1024) return `${n}B`;
129
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
130
+ if (n < 1024 ** 3) return `${(n / (1024 * 1024)).toFixed(1)}MB`;
131
+ return `${(n / 1024 ** 3).toFixed(2)}GB`;
132
+ }
133
+ function fmtDate(s) {
134
+ if (!s) return "-";
135
+ const d = new Date(s);
136
+ return Number.isNaN(d.getTime()) ? s : d.toISOString().slice(0, 16).replace("T", " ");
137
+ }
138
+ function table(rows, cols, empty = "Nothing here yet.") {
139
+ if (rows.length === 0) {
140
+ console.log(empty);
141
+ return;
142
+ }
143
+ const cell = (v) => v == null || v === "" ? "-" : String(v);
144
+ const widths = cols.map(
145
+ ([k, label]) => Math.min(48, Math.max(label.length, ...rows.map((r) => cell(r[k]).length)))
146
+ );
147
+ const line = (vals) => vals.map((v, i) => i === vals.length - 1 ? v : v.slice(0, widths[i]).padEnd(widths[i])).join(" ").trimEnd();
148
+ console.log(line(cols.map(([, l]) => l)));
149
+ for (const r of rows) console.log(line(cols.map(([k]) => cell(r[k]))));
150
+ }
151
+
152
+ // src/client.ts
153
+ import { EasybitsClient, resolveBaseUrl as resolveBaseUrl2 } from "@easybits.cloud/sdk";
154
+
155
+ // src/auth.ts
156
+ import { createHash, randomBytes } from "crypto";
157
+ import { createServer } from "http";
158
+ import { spawn } from "child_process";
159
+ import { chmodSync, readFileSync, writeFileSync } from "fs";
160
+ import { homedir } from "os";
161
+ import { join } from "path";
162
+ import { resolveBaseUrl } from "@easybits.cloud/sdk";
163
+ var CLIENT_ID = "easybits-cli";
164
+ var LOGIN_TIMEOUT_MS = 5 * 6e4;
165
+ var RC_PATH = join(homedir(), ".easybitsrc");
166
+ function readRc() {
35
167
  try {
36
- const data = await client.listFiles();
37
- if (data.items.length === 0) {
38
- console.log("No files found");
39
- return;
40
- }
41
- console.log(
42
- `${"Name".padEnd(30)} ${"Size".padEnd(10)} ${"Status".padEnd(10)} ID`
43
- );
44
- console.log("-".repeat(70));
45
- for (const f of data.items) {
46
- const size = f.size < 1024 * 1024 ? `${(f.size / 1024).toFixed(1)}KB` : `${(f.size / (1024 * 1024)).toFixed(1)}MB`;
47
- console.log(
48
- `${f.name.slice(0, 29).padEnd(30)} ${size.padEnd(10)} ${f.status.padEnd(10)} ${f.id}`
49
- );
50
- }
51
- } catch (err) {
52
- if (err instanceof EasybitsError) {
53
- console.error(`Error ${err.status}: ${err.body}`);
54
- process.exit(1);
55
- }
56
- throw err;
57
- }
58
- }
59
- async function filesUpload() {
60
- const fileName = process.argv[4];
61
- if (!fileName) {
62
- console.error("Usage: easybits files upload <filename>");
63
- process.exit(1);
64
- }
65
- if (!existsSync(fileName)) {
66
- console.error(`File not found: ${fileName}`);
67
- process.exit(1);
68
- }
69
- const stat = statSync(fileName);
70
- const ext = fileName.split(".").pop() || "";
71
- const mimeMap = {
72
- pdf: "application/pdf",
73
- png: "image/png",
74
- jpg: "image/jpeg",
75
- jpeg: "image/jpeg",
76
- mp4: "video/mp4",
77
- zip: "application/zip"
78
- };
79
- const contentType = mimeMap[ext] || "application/octet-stream";
80
- const client = await getClient();
168
+ return JSON.parse(readFileSync(RC_PATH, "utf8"));
169
+ } catch {
170
+ return {};
171
+ }
172
+ }
173
+ function writeRc(rc) {
174
+ writeFileSync(RC_PATH, JSON.stringify(rc, null, 2) + "\n", { mode: 384 });
175
+ chmodSync(RC_PATH, 384);
176
+ }
177
+ var interactive = (ctx) => Boolean(process.stdin.isTTY && process.stderr.isTTY) && !ctx.json;
178
+ var b64url = (buf) => buf.toString("base64url");
179
+ var info = (s) => process.stderr.write(s + "\n");
180
+ function openUrl(url) {
181
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
182
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
81
183
  try {
82
- const data = await client.uploadFile({
83
- fileName: fileName.split("/").pop(),
84
- contentType,
85
- size: stat.size
184
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => {
185
+ }).unref();
186
+ } catch {
187
+ }
188
+ }
189
+ async function tokenRequest(base, form) {
190
+ const res = await fetch(`${base}/oauth/token`, {
191
+ method: "POST",
192
+ headers: { "content-type": "application/x-www-form-urlencoded" },
193
+ body: new URLSearchParams(form)
194
+ });
195
+ const body = await res.json().catch(() => ({}));
196
+ if (!res.ok || !body.access_token) {
197
+ throw new CliError(`Login failed (${body.error ?? res.status}).`, EXIT.AUTH, "Run: easybits login", "login_failed", res.status);
198
+ }
199
+ return body;
200
+ }
201
+ var FAIL_PAGE = (base) => `<!doctype html><html lang="es"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
202
+ <title>EasyBits CLI</title><body style="font-family:system-ui,sans-serif;background:#0b0b0f;color:#f4f5fb;display:grid;place-items:center;min-height:100vh;margin:0;padding:16px;box-sizing:border-box">
203
+ <div style="text-align:center;max-width:420px">
204
+ <img src="${base}/logo-purple.svg" alt="EasyBits" width="96" height="96" style="display:block;margin:0 auto 20px">
205
+ <h1 style="font-size:26px;margin:0 0 8px">No se pudo entrar</h1>
206
+ <p style="margin:0;color:#a1a1aa;line-height:1.5">Vuelve a la terminal y corre <code style="color:#fbbf24">easybits login</code> otra vez.</p></div>`;
207
+ async function fetchEmail(token) {
208
+ const base = (await resolveBaseUrl()).replace(/\/+$/, "");
209
+ const res = await fetch(`${base}/api/v2/me`, { headers: { authorization: `Bearer ${token}` } });
210
+ if (res.status === 401) {
211
+ throw new CliError("Credentials rejected (401).", EXIT.AUTH, "Check the key at https://www.easybits.cloud/dash/developer", "unauthorized", 401);
212
+ }
213
+ if (!res.ok) return void 0;
214
+ return (await res.json().catch(() => ({}))).email;
215
+ }
216
+ async function oauthLogin(ctx, opts = {}) {
217
+ const base = (await resolveBaseUrl()).replace(/\/+$/, "");
218
+ const verifier = b64url(randomBytes(32));
219
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
220
+ const state = b64url(randomBytes(16));
221
+ const server = createServer();
222
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
223
+ const port = server.address().port;
224
+ const redirectUri = `http://127.0.0.1:${port}/cb`;
225
+ try {
226
+ const clientId = CLIENT_ID;
227
+ const url = `${base}/oauth/authorize?` + new URLSearchParams({
228
+ response_type: "code",
229
+ client_id: clientId,
230
+ redirect_uri: redirectUri,
231
+ scope: "mcp",
232
+ state,
233
+ code_challenge: challenge,
234
+ code_challenge_method: "S256"
235
+ });
236
+ const code = await new Promise((resolve, reject) => {
237
+ const timer = setTimeout(
238
+ () => reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: easybits login", "login_timeout")),
239
+ LOGIN_TIMEOUT_MS
240
+ );
241
+ server.on("request", (req, res) => {
242
+ const u = new URL(req.url ?? "/", "http://127.0.0.1");
243
+ if (u.pathname !== "/cb") {
244
+ res.writeHead(404).end();
245
+ return;
246
+ }
247
+ const got = u.searchParams.get("code");
248
+ const ok = u.searchParams.get("state") === state && !!got;
249
+ clearTimeout(timer);
250
+ if (ok) {
251
+ res.writeHead(302, { location: `${base}/oauth/listo` }).end();
252
+ resolve(got);
253
+ } else {
254
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
255
+ reject(
256
+ new CliError(`Login was not completed (${u.searchParams.get("error") ?? "state mismatch"}).`, EXIT.AUTH, "Run: easybits login", "login_failed")
257
+ );
258
+ }
259
+ });
260
+ if (ctx.json) process.stdout.write(JSON.stringify({ event: "login_url", url }) + "\n");
261
+ if (opts.openBrowser !== false) {
262
+ info("Opening your browser to sign in to EasyBits\u2026");
263
+ openUrl(url);
264
+ }
265
+ info(`If it doesn't open, visit:
266
+ ${url}`);
267
+ info("Waiting for the browser\u2026");
86
268
  });
87
- const fileBuffer = readFileSync(fileName);
88
- const uploadRes = await fetch(data.putUrl, {
89
- method: "PUT",
90
- body: fileBuffer,
91
- headers: { "Content-Type": contentType }
269
+ const tok = await tokenRequest(base, {
270
+ grant_type: "authorization_code",
271
+ code,
272
+ redirect_uri: redirectUri,
273
+ client_id: clientId,
274
+ code_verifier: verifier
92
275
  });
93
- if (uploadRes.ok) {
94
- console.log(`Uploaded: ${data.file.id}`);
95
- } else {
96
- console.error(`Upload failed: ${uploadRes.status}`);
97
- }
98
- } catch (err) {
99
- if (err instanceof EasybitsError) {
100
- console.error(`Error ${err.status}: ${err.body}`);
101
- process.exit(1);
276
+ const session = {
277
+ clientId,
278
+ accessToken: tok.access_token,
279
+ refreshToken: tok.refresh_token,
280
+ expiresAt: Date.now() + (tok.expires_in ?? 3600) * 1e3
281
+ };
282
+ writeRc({ ...readRc(), oauth: session });
283
+ return session;
284
+ } finally {
285
+ server.close();
286
+ }
287
+ }
288
+ async function refreshSession(s) {
289
+ if (!s.refreshToken) return null;
290
+ try {
291
+ const tok = await tokenRequest((await resolveBaseUrl()).replace(/\/+$/, ""), {
292
+ grant_type: "refresh_token",
293
+ refresh_token: s.refreshToken,
294
+ client_id: s.clientId
295
+ });
296
+ const next = {
297
+ ...s,
298
+ accessToken: tok.access_token,
299
+ refreshToken: tok.refresh_token ?? s.refreshToken,
300
+ expiresAt: Date.now() + (tok.expires_in ?? 3600) * 1e3
301
+ };
302
+ writeRc({ ...readRc(), oauth: next });
303
+ return next;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+
309
+ // src/client.ts
310
+ function resolveApiKey(ctx) {
311
+ return process.env.EASYBITS_API_KEY || ctx.token || readRc().apiKey;
312
+ }
313
+ var fromSession = false;
314
+ var usedSession = () => fromSession;
315
+ async function forceRefresh() {
316
+ const s = readRc().oauth;
317
+ return Boolean(s && await refreshSession(s));
318
+ }
319
+ async function resolveCredential(ctx) {
320
+ fromSession = false;
321
+ const explicit = process.env.EASYBITS_API_KEY || ctx.token;
322
+ if (explicit) return explicit;
323
+ const rc = readRc();
324
+ if (rc.oauth?.accessToken) {
325
+ fromSession = true;
326
+ if (rc.oauth.expiresAt - Date.now() > 6e4) return rc.oauth.accessToken;
327
+ const next = await refreshSession(rc.oauth);
328
+ if (next) return next.accessToken;
329
+ fromSession = false;
330
+ if (rc.apiKey) return rc.apiKey;
331
+ if (!interactive(ctx)) {
332
+ throw new CliError("Your session expired.", EXIT.AUTH, "Run: easybits login (or set EASYBITS_API_KEY)", "session_expired");
102
333
  }
103
- throw err;
334
+ } else if (rc.apiKey) {
335
+ return rc.apiKey;
336
+ } else if (!interactive(ctx)) {
337
+ throw notLoggedIn();
104
338
  }
339
+ process.stderr.write("Not logged in yet \u2014 let's fix that.\n");
340
+ fromSession = true;
341
+ return (await oauthLogin(ctx)).accessToken;
342
+ }
343
+ async function getClient(ctx) {
344
+ const apiKey = await resolveCredential(ctx);
345
+ return new EasybitsClient({ apiKey, baseUrl: await resolveBaseUrl2() });
105
346
  }
106
- async function filesDelete() {
107
- const fileId = process.argv[4];
108
- if (!fileId) {
109
- console.error("Usage: easybits files delete <file-id>");
110
- process.exit(1);
347
+
348
+ // src/commands/account.ts
349
+ var login = {
350
+ name: "login",
351
+ group: "Account",
352
+ summary: "Sign in with your browser (or save an API key)",
353
+ synopsis: "login [api-key]",
354
+ leaf: {
355
+ summary: "Sign in with your browser (OAuth2 + PKCE), or save an API key instead",
356
+ usage: "easybits login [api-key] [--no-browser]",
357
+ options: {
358
+ "no-browser": { type: "boolean", description: "Only print the sign-in URL (e.g. for an agent to relay it)" }
359
+ },
360
+ examples: [
361
+ "easybits login # opens the browser; waits for you",
362
+ 'easybits login --json # agents: prints {"event":"login_url"} first',
363
+ "easybits login eb_sk_live_xxxxxxxx # API key instead of the browser (validated first)"
364
+ ],
365
+ async run(ctx) {
366
+ const key = ctx.args[0];
367
+ if (key) {
368
+ const email2 = await fetchEmail(key);
369
+ const { oauth: _drop, ...rest } = readRc();
370
+ writeRc({ ...rest, apiKey: key });
371
+ if (ctx.json) process.stdout.write(JSON.stringify({ event: "logged_in", email: email2, method: "apiKey" }) + "\n");
372
+ else console.log(`Logged in${email2 ? ` as ${email2}` : ""}. API key saved to ~/.easybitsrc`);
373
+ return;
374
+ }
375
+ const session = await oauthLogin(ctx, { openBrowser: !bool(ctx, "no-browser") });
376
+ const email = await fetchEmail(session.accessToken);
377
+ if (ctx.json) process.stdout.write(JSON.stringify({ event: "logged_in", email, method: "oauth" }) + "\n");
378
+ else console.log(`Logged in${email ? ` as ${email}` : ""}. Session saved to ~/.easybitsrc`);
379
+ }
111
380
  }
112
- const client = await getClient();
113
- try {
114
- await client.deleteFile(fileId);
115
- console.log("Deleted");
116
- } catch (err) {
117
- if (err instanceof EasybitsError) {
118
- console.error(`Error ${err.status}: ${err.body}`);
119
- process.exit(1);
381
+ };
382
+ var logout = {
383
+ name: "logout",
384
+ group: "Account",
385
+ summary: "Forget the saved session and API key",
386
+ synopsis: "logout",
387
+ leaf: {
388
+ summary: "Remove the browser session and the API key from ~/.easybitsrc",
389
+ usage: "easybits logout",
390
+ examples: ["easybits logout"],
391
+ async run(ctx) {
392
+ const { oauth: _o, apiKey: _k, ...rest } = readRc();
393
+ writeRc(rest);
394
+ emit(ctx, { ok: true }, () => console.log("Logged out. Removed credentials from ~/.easybitsrc"));
120
395
  }
121
- throw err;
122
- }
123
- }
124
- async function providersList() {
125
- console.log("Default provider: Tigris (platform)");
126
- console.log("Use the Developer Dashboard to add custom providers.");
127
- }
128
- async function printMcpConfig() {
129
- const apiKey = await resolveApiKey();
130
- const baseUrl = await resolveBaseUrl();
131
- const config = {
132
- mcpServers: {
133
- easybits: {
134
- type: "streamable-http",
135
- url: `${baseUrl}/api/mcp`,
136
- headers: {
137
- Authorization: `Bearer ${apiKey || "eb_sk_live_YOUR_KEY"}`
138
- }
396
+ }
397
+ };
398
+ var usage = {
399
+ name: "usage",
400
+ group: "Account",
401
+ summary: "Show your plan, storage and resource counts",
402
+ synopsis: "usage",
403
+ leaf: {
404
+ summary: "Show your plan, storage and resource counts",
405
+ usage: "easybits usage",
406
+ examples: ["easybits usage", "easybits usage --json"],
407
+ async run(ctx) {
408
+ const eb = await getClient(ctx);
409
+ const u = await eb.getUsageStats();
410
+ emit(ctx, u, () => {
411
+ console.log(`Plan: ${u.plan}`);
412
+ console.log(
413
+ `Storage: ${fmtBytes(u.storage.usedBytes)} of ${fmtBytes(u.storage.maxBytes)} (${u.storage.percentUsed}%)`
414
+ );
415
+ console.log(
416
+ `Files: ${u.counts.files} (trash: ${u.counts.deletedFiles})
417
+ Websites: ${u.counts.websites}
418
+ Webhooks: ${u.counts.webhooks}`
419
+ );
420
+ });
421
+ }
422
+ }
423
+ };
424
+ var websites = {
425
+ name: "websites",
426
+ group: "Storage",
427
+ summary: "List your static websites",
428
+ synopsis: "websites",
429
+ subs: {
430
+ ls: {
431
+ aliases: ["list"],
432
+ summary: "List your static websites",
433
+ usage: "easybits websites ls",
434
+ examples: ["easybits websites ls --json"],
435
+ async run(ctx) {
436
+ const eb = await getClient(ctx);
437
+ const { items } = await eb.listWebsites();
438
+ emit(
439
+ ctx,
440
+ items,
441
+ () => table(
442
+ items.map((w) => ({ ...w, size: fmtBytes(w.totalSize), createdAt: fmtDate(w.createdAt) })),
443
+ [["id", "ID"], ["name", "NAME"], ["status", "STATUS"], ["fileCount", "FILES"], ["size", "SIZE"], ["url", "URL"]],
444
+ "No websites yet."
445
+ )
446
+ );
139
447
  }
140
448
  }
141
- };
142
- console.log(JSON.stringify(config, null, 2));
143
- }
144
- function printMcpStdioConfig() {
145
- const config = {
146
- mcpServers: {
147
- easybits: {
148
- command: "npx",
149
- args: ["-y", "@easybits.cloud/mcp"],
150
- env: {
151
- EASYBITS_API_KEY: "eb_sk_live_YOUR_KEY"
152
- }
449
+ },
450
+ defaultSub: "ls"
451
+ };
452
+ var providers = {
453
+ name: "providers",
454
+ group: "Storage",
455
+ summary: "Show storage providers",
456
+ synopsis: "providers",
457
+ leaf: {
458
+ summary: "Show storage providers",
459
+ usage: "easybits providers [list]",
460
+ async run(ctx) {
461
+ const data = {
462
+ defaultProvider: "tigris",
463
+ note: "Use the Developer Dashboard to add custom providers."
464
+ };
465
+ emit(ctx, data, () => {
466
+ console.log("Default provider: Tigris (platform)");
467
+ console.log("Use the Developer Dashboard to add custom providers.");
468
+ });
469
+ }
470
+ }
471
+ };
472
+ var config = {
473
+ name: "config",
474
+ group: "MCP",
475
+ summary: "Print MCP config JSON (streamable HTTP)",
476
+ synopsis: "config",
477
+ leaf: {
478
+ summary: "Print MCP config JSON (streamable HTTP) with your key",
479
+ usage: "easybits config",
480
+ examples: ["easybits config > .mcp.json"],
481
+ async run(ctx) {
482
+ const apiKey = resolveApiKey(ctx);
483
+ const baseUrl = await resolveBaseUrl3();
484
+ console.log(
485
+ JSON.stringify(
486
+ {
487
+ mcpServers: {
488
+ easybits: {
489
+ type: "streamable-http",
490
+ url: `${baseUrl}/api/mcp`,
491
+ headers: { Authorization: `Bearer ${apiKey || "eb_sk_live_YOUR_KEY"}` }
492
+ }
493
+ }
494
+ },
495
+ null,
496
+ 2
497
+ )
498
+ );
499
+ }
500
+ }
501
+ };
502
+ var mcp = {
503
+ name: "mcp",
504
+ group: "MCP",
505
+ summary: "Print MCP stdio config JSON",
506
+ synopsis: "mcp",
507
+ leaf: {
508
+ summary: "Print MCP stdio config JSON (npx @easybits.cloud/mcp)",
509
+ usage: "easybits mcp",
510
+ async run() {
511
+ console.log(
512
+ JSON.stringify(
513
+ {
514
+ mcpServers: {
515
+ easybits: {
516
+ command: "npx",
517
+ args: ["-y", "@easybits.cloud/mcp"],
518
+ env: { EASYBITS_API_KEY: "eb_sk_live_YOUR_KEY" }
519
+ }
520
+ }
521
+ },
522
+ null,
523
+ 2
524
+ )
525
+ );
526
+ }
527
+ }
528
+ };
529
+
530
+ // src/commands/files.ts
531
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
532
+ import { basename, extname } from "path";
533
+ var MIME = {
534
+ pdf: "application/pdf",
535
+ png: "image/png",
536
+ jpg: "image/jpeg",
537
+ jpeg: "image/jpeg",
538
+ webp: "image/webp",
539
+ gif: "image/gif",
540
+ svg: "image/svg+xml",
541
+ mp4: "video/mp4",
542
+ mp3: "audio/mpeg",
543
+ zip: "application/zip",
544
+ gz: "application/gzip",
545
+ json: "application/json",
546
+ txt: "text/plain",
547
+ md: "text/markdown",
548
+ html: "text/html",
549
+ csv: "text/csv"
550
+ };
551
+ var files = {
552
+ name: "files",
553
+ group: "Storage",
554
+ summary: "Your files in EasyBits storage",
555
+ synopsis: "files",
556
+ defaultSub: "ls",
557
+ subs: {
558
+ ls: {
559
+ aliases: ["list"],
560
+ summary: "List your files",
561
+ usage: "easybits files ls",
562
+ examples: ["easybits files ls", "easybits files ls --json"],
563
+ async run(ctx) {
564
+ const eb = await getClient(ctx);
565
+ const data = await eb.listFiles();
566
+ emit(
567
+ ctx,
568
+ data.items,
569
+ () => table(
570
+ data.items.map((f) => ({ ...f, size: fmtBytes(f.size) })),
571
+ [["name", "NAME"], ["size", "SIZE"], ["status", "STATUS"], ["id", "ID"]],
572
+ "No files found"
573
+ )
574
+ );
575
+ }
576
+ },
577
+ upload: {
578
+ summary: "Upload a local file",
579
+ usage: "easybits files upload <path>",
580
+ examples: ["easybits files upload ./report.pdf", "easybits files upload ./logo.png --json"],
581
+ async run(ctx) {
582
+ const path = need(ctx, 0, "path", this.usage);
583
+ if (!existsSync(path)) throw usageError(`File not found: ${path}`, this.usage);
584
+ const size = statSync(path).size;
585
+ const contentType = MIME[extname(path).slice(1).toLowerCase()] || "application/octet-stream";
586
+ const eb = await getClient(ctx);
587
+ const data = await eb.uploadFile({ fileName: basename(path), contentType, size });
588
+ const res = await fetch(data.putUrl, {
589
+ method: "PUT",
590
+ body: readFileSync2(path),
591
+ headers: { "Content-Type": contentType }
592
+ });
593
+ if (!res.ok) throw new CliError(`Upload failed: storage answered ${res.status}`, 1, void 0, "upload_failed", res.status);
594
+ emit(ctx, data.file, () => console.log(`Uploaded: ${data.file.id}`));
595
+ }
596
+ },
597
+ delete: {
598
+ aliases: ["rm"],
599
+ summary: "Delete a file (soft delete, 7-day trash)",
600
+ usage: "easybits files delete <file-id>",
601
+ examples: ["easybits files delete 6650f0c2a1b2c3d4e5f60718"],
602
+ async run(ctx) {
603
+ const id = need(ctx, 0, "file-id", this.usage);
604
+ const eb = await getClient(ctx);
605
+ const r = await eb.deleteFile(id);
606
+ emit(ctx, r, () => console.log("Deleted"));
153
607
  }
154
608
  }
155
- };
156
- console.log(JSON.stringify(config, null, 2));
157
- }
158
- async function init() {
159
- const appDir = flag("--app-dir") ?? "/srv/app";
160
- const port = flag("--port") ?? "3000";
161
- if (!existsSync("package.json")) {
162
- console.error(
163
- "Aqu\xED no hay package.json. Corre esto en la ra\xEDz del repo de tu app."
164
- );
165
- process.exit(1);
166
- }
167
- const workflow = `# Despliegue a EasyBits en cada push a main.
609
+ }
610
+ };
611
+
612
+ // src/commands/init.ts
613
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
614
+ var init = {
615
+ name: "init",
616
+ group: "Hosting",
617
+ summary: "Write the GitHub Actions deploy workflow for this repo",
618
+ synopsis: "init",
619
+ leaf: {
620
+ summary: "Write the GitHub Actions workflow that deploys this repo on every push to main",
621
+ usage: "easybits init [--app-dir /srv/app] [--port 3000]",
622
+ options: {
623
+ "app-dir": { type: "string", value: "dir", description: "Where the app lives in the machine (default /srv/app)" },
624
+ port: { type: "string", value: "port", description: "Port your app listens on (default 3000)" }
625
+ },
626
+ examples: ["easybits init", "easybits init --app-dir /srv/app --port 8080"],
627
+ async run(ctx) {
628
+ const appDir = str(ctx, "app-dir") ?? "/srv/app";
629
+ const port = str(ctx, "port") ?? "3000";
630
+ if (!existsSync2("package.json")) {
631
+ throw usageError("No package.json here. Run this at the root of your app's repo.", this.usage);
632
+ }
633
+ const workflow = `# Despliegue a EasyBits en cada push a main.
168
634
  #
169
635
  # El build ocurre AQU\xCD, no dentro de la m\xE1quina: as\xED la caja s\xF3lo descarga y
170
636
  # arranca, y el sitio cabe en un tier peque\xF1o. Si el build falla, no llega a
@@ -205,237 +671,1098 @@ jobs:
205
671
  EASYBITS_PORT: "${port}"
206
672
  run: node .github/scripts/easybits-deploy.mjs
207
673
  `;
208
- const script = [
209
- "// Empaqueta lo que hace falta para servir y lo manda a la m\xE1quina.",
210
- "// Lo genera 'easybits init'; aj\xFAstalo si tu app necesita otros archivos.",
211
- "",
212
- 'import { createReadStream, statSync } from "node:fs";',
213
- 'import { execFileSync } from "node:child_process";',
214
- "",
215
- 'const API = process.env.EASYBITS_API || "https://www.easybits.cloud/api/v2";',
216
- "const KEY = process.env.EASYBITS_API_KEY;",
217
- "const MACHINE = process.env.EASYBITS_SANDBOX_ID;",
218
- 'const APP_DIR = process.env.EASYBITS_APP_DIR || "' + appDir + '";',
219
- "const PORT = Number(process.env.EASYBITS_PORT || " + port + ");",
220
- 'const TARBALL = "/tmp/easybits-build.tgz";',
221
- 'const SHA = (process.env.GITHUB_SHA || "manual").slice(0, 7);',
222
- "",
223
- "if (!KEY || !MACHINE) {",
224
- ' console.error("Faltan EASYBITS_API_KEY o EASYBITS_SANDBOX_ID en los secretos del repo.");',
225
- " process.exit(1);",
226
- "}",
227
- "",
228
- "async function api(path, body) {",
229
- " const res = await fetch(API + path, {",
230
- ' method: "POST",',
231
- ' headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json" },',
232
- " body: JSON.stringify(body),",
233
- " });",
234
- " const text = await res.text();",
235
- " let data;",
236
- " try { data = JSON.parse(text); } catch { data = {}; }",
237
- " if (!res.ok) {",
238
- ' console.error("x " + path + " respondio " + res.status);',
239
- " console.error(data.message || data.error || text);",
240
- " process.exit(1);",
241
- " }",
242
- " return data;",
243
- "}",
244
- "",
245
- "// Servir no necesita vite ni los compiladores: podarlos es la diferencia",
246
- "// entre mandar 170 MB por deploy y mandar lo que la maquina ejecuta.",
247
- 'console.log("-> podando dependencias de desarrollo");',
248
- 'execFileSync("npm", ["prune", "--omit=dev"], { stdio: "inherit" });',
249
- "",
250
- 'console.log("-> empaquetando");',
251
- 'execFileSync("tar", [',
252
- ' "czf", TARBALL, "--exclude=.git", "--exclude=node_modules/.cache",',
253
- ' "build", "node_modules", "package.json", "package-lock.json",',
254
- "]);",
255
- "const size = statSync(TARBALL).size;",
256
- 'console.log(" " + (size / 1048576).toFixed(1) + " MB");',
257
- "",
258
- "// Publico y de vida corta: la caja lo baja con curl, sin credenciales. No",
259
- "// lleva secretos: esos los inyecta EasyBits desde la boveda, ya dentro.",
260
- 'console.log("-> subiendo");',
261
- 'const up = await api("/files", {',
262
- ' fileName: "build-" + SHA + ".tgz",',
263
- ' contentType: "application/gzip",',
264
- " size,",
265
- ' access: "public",',
266
- "});",
267
- "const put = await fetch(up.putUrl, {",
268
- ' method: "PUT",',
269
- ' headers: { "Content-Type": "application/gzip", "Content-Length": String(size) },',
270
- " body: createReadStream(TARBALL),",
271
- ' duplex: "half",',
272
- "});",
273
- "if (!put.ok) {",
274
- ' console.error("x la subida devolvio " + put.status);',
275
- " process.exit(1);",
276
- "}",
277
- "",
278
- "// prebuilt: la caja no reconstruye nada, solo descomprime y arranca.",
279
- 'console.log("-> desplegando");',
280
- 'const out = await api("/machines/launch", {',
281
- " sandboxId: MACHINE,",
282
- " archiveUrl: up.file.url,",
283
- " prebuilt: true,",
284
- " appDir: APP_DIR,",
285
- " port: PORT,",
286
- ' message: "deploy " + SHA,',
287
- "});",
288
- "",
289
- "if (out.exitCode !== 0) {",
290
- ' console.error("x termino con codigo " + out.exitCode);',
291
- ' console.error(out.buildOutput || out.startOutput || "");',
292
- " process.exit(1);",
293
- "}",
294
- 'console.log("OK desplegado - version " + out.version);',
295
- 'console.log(" " + out.url);',
296
- ""
297
- ].join("\n");
298
- mkdirSync(".github/workflows", { recursive: true });
299
- mkdirSync(".github/scripts", { recursive: true });
300
- writeFileSync(".github/workflows/easybits-deploy.yml", workflow);
301
- writeFileSync(".github/scripts/easybits-deploy.mjs", script);
302
- console.log(`Listo. Escrib\xED:
674
+ const script = [
675
+ "// Empaqueta lo que hace falta para servir y lo manda a la m\xE1quina.",
676
+ "// Lo genera 'easybits init'; aj\xFAstalo si tu app necesita otros archivos.",
677
+ "",
678
+ 'import { createReadStream, statSync } from "node:fs";',
679
+ 'import { execFileSync } from "node:child_process";',
680
+ "",
681
+ 'const API = process.env.EASYBITS_API || "https://www.easybits.cloud/api/v2";',
682
+ "const KEY = process.env.EASYBITS_API_KEY;",
683
+ "const MACHINE = process.env.EASYBITS_SANDBOX_ID;",
684
+ 'const APP_DIR = process.env.EASYBITS_APP_DIR || "' + appDir + '";',
685
+ "const PORT = Number(process.env.EASYBITS_PORT || " + port + ");",
686
+ 'const TARBALL = "/tmp/easybits-build.tgz";',
687
+ 'const SHA = (process.env.GITHUB_SHA || "manual").slice(0, 7);',
688
+ "",
689
+ "if (!KEY || !MACHINE) {",
690
+ ' console.error("Faltan EASYBITS_API_KEY o EASYBITS_SANDBOX_ID en los secretos del repo.");',
691
+ " process.exit(1);",
692
+ "}",
693
+ "",
694
+ "async function api(path, body) {",
695
+ " const res = await fetch(API + path, {",
696
+ ' method: "POST",',
697
+ ' headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json" },',
698
+ " body: JSON.stringify(body),",
699
+ " });",
700
+ " const text = await res.text();",
701
+ " let data;",
702
+ " try { data = JSON.parse(text); } catch { data = {}; }",
703
+ " if (!res.ok) {",
704
+ ' console.error("x " + path + " respondio " + res.status);',
705
+ " console.error(data.message || data.error || text);",
706
+ " process.exit(1);",
707
+ " }",
708
+ " return data;",
709
+ "}",
710
+ "",
711
+ "// Servir no necesita vite ni los compiladores: podarlos es la diferencia",
712
+ "// entre mandar 170 MB por deploy y mandar lo que la maquina ejecuta.",
713
+ 'console.log("-> podando dependencias de desarrollo");',
714
+ 'execFileSync("npm", ["prune", "--omit=dev"], { stdio: "inherit" });',
715
+ "",
716
+ 'console.log("-> empaquetando");',
717
+ 'execFileSync("tar", [',
718
+ ' "czf", TARBALL, "--exclude=.git", "--exclude=node_modules/.cache",',
719
+ ' "build", "node_modules", "package.json", "package-lock.json",',
720
+ "]);",
721
+ "const size = statSync(TARBALL).size;",
722
+ 'console.log(" " + (size / 1048576).toFixed(1) + " MB");',
723
+ "",
724
+ "// Publico y de vida corta: la caja lo baja con curl, sin credenciales. No",
725
+ "// lleva secretos: esos los inyecta EasyBits desde la boveda, ya dentro.",
726
+ 'console.log("-> subiendo");',
727
+ 'const up = await api("/files", {',
728
+ ' fileName: "build-" + SHA + ".tgz",',
729
+ ' contentType: "application/gzip",',
730
+ " size,",
731
+ ' access: "public",',
732
+ "});",
733
+ "const put = await fetch(up.putUrl, {",
734
+ ' method: "PUT",',
735
+ ' headers: { "Content-Type": "application/gzip", "Content-Length": String(size) },',
736
+ " body: createReadStream(TARBALL),",
737
+ ' duplex: "half",',
738
+ "});",
739
+ "if (!put.ok) {",
740
+ ' console.error("x la subida devolvio " + put.status);',
741
+ " process.exit(1);",
742
+ "}",
743
+ "",
744
+ "// prebuilt: la caja no reconstruye nada, solo descomprime y arranca.",
745
+ 'console.log("-> desplegando");',
746
+ 'const out = await api("/machines/launch", {',
747
+ " sandboxId: MACHINE,",
748
+ " archiveUrl: up.file.url,",
749
+ " prebuilt: true,",
750
+ " appDir: APP_DIR,",
751
+ " port: PORT,",
752
+ ' message: "deploy " + SHA,',
753
+ "});",
754
+ "",
755
+ "if (out.exitCode !== 0) {",
756
+ ' console.error("x termino con codigo " + out.exitCode);',
757
+ ' console.error(out.buildOutput || out.startOutput || "");',
758
+ " process.exit(1);",
759
+ "}",
760
+ 'console.log("OK desplegado - version " + out.version);',
761
+ 'console.log(" " + out.url);',
762
+ ""
763
+ ].join("\n");
764
+ mkdirSync(".github/workflows", { recursive: true });
765
+ mkdirSync(".github/scripts", { recursive: true });
766
+ writeFileSync2(".github/workflows/easybits-deploy.yml", workflow);
767
+ writeFileSync2(".github/scripts/easybits-deploy.mjs", script);
768
+ const written = [".github/workflows/easybits-deploy.yml", ".github/scripts/easybits-deploy.mjs"];
769
+ emit(ctx, { written, appDir, port: Number(port) }, () => console.log(`Done. Wrote:
303
770
  .github/workflows/easybits-deploy.yml
304
771
  .github/scripts/easybits-deploy.mjs
305
772
 
306
- Falta, una sola vez:
773
+ One-time setup left:
307
774
 
308
- 1. Crea la m\xE1quina (si a\xFAn no la tienes):
775
+ 1. Create the machine (if you don't have one yet):
309
776
 
310
777
  curl -X POST https://www.easybits.cloud/api/v2/machines/launch \\
311
778
  -H "Authorization: Bearer $EASYBITS_API_KEY" \\
312
779
  -H "Content-Type: application/json" \\
313
- -d '{"repo":"https://github.com/TU/REPO.git","branch":"main",
780
+ -d '{"repo":"https://github.com/YOU/REPO.git","branch":"main",
314
781
  "tier":"micro","template":"node","appDir":"${appDir}","port":${port}}'
315
782
 
316
- Repo privado: deja el URL limpio y agrega el token aparte,
783
+ Private repo: keep the URL clean and pass the token separately,
317
784
  "repoToken":"github_pat_\u2026"
318
- (un token dentro del URL se rechaza).
785
+ (a token inside the URL is rejected).
319
786
 
320
- \xBFTu build cabe en la caja? Entonces no necesitas este workflow:
321
- POST /machines/SANDBOX_ID/push-deploy te da un webhook de GitHub
322
- y cada push despliega solo. Docs: https://www.easybits.cloud/docs
787
+ Does your build fit in the machine? Then you don't need this workflow:
788
+ POST /machines/SANDBOX_ID/push-deploy gives you a GitHub webhook
789
+ and every push deploys by itself. Docs: https://www.easybits.cloud/docs
323
790
 
324
- 2. Guarda en el repo (Settings \u2192 Secrets and variables \u2192 Actions):
325
- EASYBITS_API_KEY tu key
326
- EASYBITS_SANDBOX_ID el id que devolvi\xF3 el paso 1
791
+ 2. Add these repo secrets (Settings \u2192 Secrets and variables \u2192 Actions):
792
+ EASYBITS_API_KEY your key
793
+ EASYBITS_SANDBOX_ID the id returned by step 1
327
794
 
328
- 3. Si tu app usa variables secretas:
795
+ 3. If your app uses secret env vars:
329
796
 
330
- curl -X PUT https://www.easybits.cloud/api/v2/machines/SANDBOX_ID/secrets \\
331
- -H "Authorization: Bearer $EASYBITS_API_KEY" \\
332
- -H "Content-Type: application/json" \\
333
- -d '{"DATABASE_URL":"..."}'
334
-
335
- Desde ah\xED, cada push a main despliega.`);
336
- }
337
- async function sshProxy() {
338
- const target = process.argv[3];
339
- if (!target) {
340
- console.error("uso: easybits ssh-proxy <sandboxId|caja.ghosty>");
341
- process.exit(2);
342
- }
343
- let sandboxId = target.split(".")[0];
344
- const eb = await createClientFromEnv();
345
- if (!sandboxId.startsWith("sb_")) {
346
- const hits = (await eb.sandboxes.list()).filter((s) => s.name === sandboxId);
347
- if (hits.length === 0) throw new Error(`no hay ninguna caja llamada "${sandboxId}"`);
348
- if (hits.length > 1)
349
- throw new Error(`"${sandboxId}" es ambiguo: ${hits.length} cajas lo usan; pasa el sandboxId`);
350
- sandboxId = hits[0].sandboxId;
351
- }
352
- const sb = await eb.sandboxes.get(sandboxId);
353
- const { url } = await sb.sshTicket();
354
- const ws = new WebSocket(url);
355
- ws.binaryType = "arraybuffer";
356
- ws.onerror = () => {
357
- console.error("easybits ssh-proxy: no se pudo abrir el t\xFAnel");
358
- process.exit(1);
359
- };
360
- ws.onclose = () => process.exit(0);
361
- ws.onmessage = (ev) => {
362
- const data = ev.data;
363
- process.stdout.write(
364
- typeof data === "string" ? Buffer.from(data) : Buffer.from(data)
365
- );
797
+ easybits machines secrets set SANDBOX_ID DATABASE_URL=...
798
+
799
+ From then on, every push to main deploys.`));
800
+ }
801
+ }
802
+ };
803
+
804
+ // src/commands/ssh.ts
805
+ import { homedir as homedir2 } from "os";
806
+ import { join as join2 } from "path";
807
+ import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
808
+ import { execFileSync } from "child_process";
809
+ var SSH_CONFIG_SNIPPET = `Host *.ghosty
810
+ ProxyCommand easybits ssh-proxy %h
811
+ User root`;
812
+ var sshKey = {
813
+ name: "ssh-key",
814
+ group: "SSH",
815
+ summary: "Print your public key for sandbox SSH (creates it once)",
816
+ synopsis: "ssh-key",
817
+ leaf: {
818
+ summary: "Print your public key for sandbox_ssh_enable (creates ~/.ssh/easybits_ed25519 once)",
819
+ usage: "easybits ssh-key",
820
+ examples: ["easybits ssh-key"],
821
+ async run(ctx) {
822
+ const dir = join2(homedir2(), ".ssh");
823
+ const key = join2(dir, "easybits_ed25519");
824
+ const pub = `${key}.pub`;
825
+ if (!existsSync3(pub)) {
826
+ mkdirSync2(dir, { recursive: true, mode: 448 });
827
+ execFileSync("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", key, "-C", "easybits"], {
828
+ stdio: ["ignore", "ignore", "inherit"]
829
+ });
830
+ console.error(`easybits: key created at ${key}`);
831
+ }
832
+ const publicKey = readFileSync3(pub, "utf8").trim();
833
+ if (ctx.json) process.stdout.write(JSON.stringify({ publicKey, path: pub }, null, 2) + "\n");
834
+ else process.stdout.write(publicKey + "\n");
835
+ }
836
+ }
837
+ };
838
+ var sshProxy = {
839
+ name: "ssh-proxy",
840
+ group: "SSH",
841
+ summary: "SSH tunnel over 443 (use as ssh ProxyCommand)",
842
+ synopsis: "ssh-proxy <host>",
843
+ leaf: {
844
+ summary: "SSH tunnel over 443; meant to be the ProxyCommand in ~/.ssh/config",
845
+ usage: "easybits ssh-proxy <sandboxId|name.ghosty>",
846
+ examples: ["ssh mybox.ghosty # with the ~/.ssh/config snippet from `easybits --help`"],
847
+ async run(ctx) {
848
+ const target = need(ctx, 0, "sandboxId|name.ghosty", this.usage);
849
+ let sandboxId = target.split(".")[0];
850
+ const eb = await getClient(ctx);
851
+ if (!sandboxId.startsWith("sb_")) {
852
+ const hits = (await eb.sandboxes.list()).filter((s) => s.name === sandboxId);
853
+ if (hits.length === 0) throw usageError(`No sandbox named "${sandboxId}".`, this.usage);
854
+ if (hits.length > 1)
855
+ throw usageError(`"${sandboxId}" is ambiguous: ${hits.length} sandboxes use it; pass the sandboxId.`, this.usage);
856
+ sandboxId = hits[0].sandboxId;
857
+ }
858
+ const sb = await eb.sandboxes.get(sandboxId);
859
+ const { url } = await sb.sshTicket();
860
+ const ws = new WebSocket(url);
861
+ ws.binaryType = "arraybuffer";
862
+ await new Promise((resolve, reject) => {
863
+ ws.onopen = () => resolve();
864
+ ws.onerror = () => reject(new CliError("easybits ssh-proxy: could not open the tunnel"));
865
+ });
866
+ ws.onerror = () => {
867
+ console.error("easybits ssh-proxy: tunnel error");
868
+ process.exit(1);
869
+ };
870
+ ws.onclose = () => process.exit(0);
871
+ ws.onmessage = (ev) => {
872
+ const data = ev.data;
873
+ process.stdout.write(typeof data === "string" ? Buffer.from(data) : Buffer.from(data));
874
+ };
875
+ process.stdin.on("data", (chunk) => ws.send(chunk));
876
+ process.stdin.on("end", () => ws.close());
877
+ await new Promise(() => {
878
+ });
879
+ }
880
+ }
881
+ };
882
+
883
+ // src/commands/sandboxes.ts
884
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
885
+ function sandboxRecord(s) {
886
+ return {
887
+ sandboxId: s.sandboxId,
888
+ name: s.name,
889
+ template: s.template,
890
+ status: s.status,
891
+ createdAt: s.createdAt,
892
+ expiresAt: s.expiresAt,
893
+ persistent: s.persistent,
894
+ tier: s.tier,
895
+ cpuMode: s.cpuMode,
896
+ monthlyMxn: s.monthlyMxn,
897
+ metadata: s.metadata,
898
+ activity: s.activity
366
899
  };
367
- await new Promise((resolve) => ws.onopen = () => resolve());
368
- process.stdin.on("data", (chunk) => ws.send(chunk));
369
- process.stdin.on("end", () => ws.close());
370
- }
371
- async function sshKey() {
372
- const { homedir } = await import("os");
373
- const { join } = await import("path");
374
- const { existsSync: existsSync2, readFileSync: readFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
375
- const { execFileSync } = await import("child_process");
376
- const dir = join(homedir(), ".ssh");
377
- const key = join(dir, "easybits_ed25519");
378
- const pub = `${key}.pub`;
379
- if (!existsSync2(pub)) {
380
- mkdirSync2(dir, { recursive: true, mode: 448 });
381
- execFileSync("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", key, "-C", "easybits"], {
382
- stdio: ["ignore", "ignore", "inherit"]
900
+ }
901
+ function printSandbox(s) {
902
+ console.log(`ID: ${s.sandboxId}`);
903
+ if (s.name) console.log(`Name: ${s.name}`);
904
+ console.log(`Template: ${s.template}`);
905
+ console.log(`Status: ${s.status}`);
906
+ if (s.activity) console.log(`Activity: ${s.activity} (busy: exec/suspend/destroy answer 409 SandboxBusy until it ends)`);
907
+ console.log(`Created: ${fmtDate(s.createdAt)}`);
908
+ console.log(`Expires: ${s.persistent ? "never (permanent)" : fmtDate(s.expiresAt)}`);
909
+ if (s.tier) console.log(`Tier: ${s.tier}${s.monthlyMxn != null ? ` ($${s.monthlyMxn} MXN/month)` : ""}`);
910
+ }
911
+ async function sandbox(ctx, usage2) {
912
+ const id = need(ctx, 0, "sandbox-id", usage2);
913
+ const eb = await getClient(ctx);
914
+ return eb.sandboxes.get(id);
915
+ }
916
+ var FILES_USAGE = "easybits sandboxes files <ls|read|write> <sandbox-id> <path> [local-file]";
917
+ var sandboxes = {
918
+ name: "sandboxes",
919
+ aliases: ["sandbox", "sb"],
920
+ group: "Compute",
921
+ summary: "Firecracker microVMs (alias: sb)",
922
+ synopsis: "sandboxes",
923
+ subs: {
924
+ ls: {
925
+ aliases: ["list"],
926
+ summary: "List your sandboxes",
927
+ usage: "easybits sandboxes ls",
928
+ examples: ["easybits sandboxes ls", "easybits sb ls --json"],
929
+ async run(ctx) {
930
+ const eb = await getClient(ctx);
931
+ const rows = (await eb.sandboxes.list()).map(sandboxRecord);
932
+ emit(
933
+ ctx,
934
+ rows,
935
+ () => table(
936
+ rows.map((r) => ({ ...r, expires: r.persistent ? "permanent" : fmtDate(r.expiresAt) })),
937
+ [["sandboxId", "ID"], ["name", "NAME"], ["template", "TEMPLATE"], ["status", "STATUS"], ["expires", "EXPIRES"]],
938
+ "No sandboxes. Create one: easybits sandboxes create --template node"
939
+ )
940
+ );
941
+ }
942
+ },
943
+ create: {
944
+ aliases: ["new"],
945
+ summary: "Create a sandbox and wait until it is running",
946
+ usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--timeout <s>] [--size s|m|l|xl] [--env K=V]...",
947
+ options: {
948
+ template: { type: "string", value: "name", description: "Base template (default ubuntu): ubuntu, python, node, code-interpreter\u2026" },
949
+ name: { type: "string", value: "name", description: "Human label (lets you `ssh <name>.ghosty`)" },
950
+ timeout: { type: "string", value: "seconds", description: "Time to live before auto-destroy" },
951
+ size: { type: "string", value: "s|m|l|xl", description: "VM size class (gated by plan)" },
952
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable (repeatable)" },
953
+ "no-wait": { type: "boolean", description: "Return right away instead of waiting for running" }
954
+ },
955
+ examples: [
956
+ "easybits sandboxes create --template node --name scratch",
957
+ "ID=$(easybits sb create --template python --json | jq -r .sandboxId)"
958
+ ],
959
+ async run(ctx) {
960
+ const size = str(ctx, "size");
961
+ if (size && !["s", "m", "l", "xl"].includes(size)) throw usageError("--size must be s, m, l or xl.", this.usage);
962
+ const env = pairs(list(ctx, "env"), this.usage);
963
+ const eb = await getClient(ctx);
964
+ const sb = await eb.sandboxes.create({
965
+ template: str(ctx, "template") ?? "ubuntu",
966
+ name: str(ctx, "name"),
967
+ timeoutSeconds: int(ctx, "timeout", this.usage),
968
+ size,
969
+ env: Object.keys(env).length ? env : void 0,
970
+ waitForReady: !bool(ctx, "no-wait")
971
+ });
972
+ const rec = sandboxRecord(sb);
973
+ emit(ctx, rec, () => printSandbox(rec));
974
+ }
975
+ },
976
+ get: {
977
+ aliases: ["show", "status"],
978
+ summary: "Show one sandbox",
979
+ usage: "easybits sandboxes get <sandbox-id>",
980
+ examples: ["easybits sandboxes get sb_abc123"],
981
+ async run(ctx) {
982
+ const rec = sandboxRecord(await sandbox(ctx, this.usage));
983
+ emit(ctx, rec, () => printSandbox(rec));
984
+ }
985
+ },
986
+ exec: {
987
+ summary: "Run a shell command inside a sandbox",
988
+ usage: "easybits sandboxes exec <sandbox-id> [--cwd <dir>] [--timeout <s>] -- <command...>",
989
+ options: {
990
+ cwd: { type: "string", value: "dir", description: "Working directory" },
991
+ timeout: { type: "string", value: "seconds", description: "Kill the command after this long" },
992
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable (repeatable)" }
993
+ },
994
+ examples: [
995
+ "easybits sandboxes exec sb_abc123 -- uname -a",
996
+ "easybits sb exec sb_abc123 --cwd /data/work -- npm test",
997
+ "easybits sb exec sb_abc123 --json -- 'ls -la /' | jq .exitCode"
998
+ ],
999
+ async run(ctx) {
1000
+ need(ctx, 0, "sandbox-id", this.usage);
1001
+ const command = ctx.args.slice(1).join(" ");
1002
+ if (!command) throw usageError("Missing <command>.", this.usage);
1003
+ const env = pairs(list(ctx, "env"), this.usage);
1004
+ const sb = await sandbox(ctx, this.usage);
1005
+ const r = await sb.exec(command, {
1006
+ cwd: str(ctx, "cwd"),
1007
+ timeoutSeconds: int(ctx, "timeout", this.usage),
1008
+ env: Object.keys(env).length ? env : void 0
1009
+ });
1010
+ if (ctx.json) {
1011
+ emit(ctx, r, () => {
1012
+ });
1013
+ return;
1014
+ }
1015
+ if (r.stdout) process.stdout.write(r.stdout);
1016
+ if (r.stderr) process.stderr.write(r.stderr);
1017
+ if (r.truncated) console.error("easybits: output truncated");
1018
+ process.exitCode = r.exitCode;
1019
+ }
1020
+ },
1021
+ logs: {
1022
+ summary: "Read the sandbox journal (optionally one systemd unit)",
1023
+ usage: "easybits sandboxes logs <sandbox-id> [--unit <unit>] [--lines <n>] [--since <spec>] [--grep <text>]",
1024
+ options: {
1025
+ unit: { type: "string", value: "unit", description: "systemd unit to filter" },
1026
+ lines: { type: "string", value: "n", description: "Last N lines (default 200)" },
1027
+ since: { type: "string", value: "spec", description: 'journalctl time spec, e.g. "10 min ago"' },
1028
+ grep: { type: "string", value: "text", description: "Only lines matching" }
1029
+ },
1030
+ examples: ["easybits sandboxes logs sb_abc123 --lines 50", "easybits sb logs sb_abc123 --unit myapp --since '10 min ago'"],
1031
+ async run(ctx) {
1032
+ const sb = await sandbox(ctx, this.usage);
1033
+ const r = await sb.logs({
1034
+ unit: str(ctx, "unit"),
1035
+ lines: int(ctx, "lines", this.usage),
1036
+ since: str(ctx, "since"),
1037
+ grep: str(ctx, "grep")
1038
+ });
1039
+ emit(ctx, r, () => process.stdout.write(r.output.endsWith("\n") || !r.output ? r.output : r.output + "\n"));
1040
+ }
1041
+ },
1042
+ files: {
1043
+ summary: "List, read or write files inside a sandbox",
1044
+ usage: FILES_USAGE,
1045
+ options: {
1046
+ content: { type: "string", value: "text", description: "write: inline content instead of a local file" },
1047
+ out: { type: "string", value: "file", description: "read: save to a local file (binary-safe)" }
1048
+ },
1049
+ examples: [
1050
+ "easybits sandboxes files ls sb_abc123 /data/work",
1051
+ "easybits sb files read sb_abc123 /etc/os-release",
1052
+ "easybits sb files read sb_abc123 /data/out.png --out out.png",
1053
+ "easybits sb files write sb_abc123 /data/work/app.js ./app.js",
1054
+ "echo hello | easybits sb files write sb_abc123 /tmp/hello.txt"
1055
+ ],
1056
+ async run(ctx) {
1057
+ const action = need(ctx, 0, "ls|read|write", FILES_USAGE);
1058
+ const id = need(ctx, 1, "sandbox-id", FILES_USAGE);
1059
+ const path = need(ctx, 2, "path", FILES_USAGE);
1060
+ const eb = await getClient(ctx);
1061
+ const sb = await eb.sandboxes.get(id);
1062
+ if (action === "ls" || action === "list") {
1063
+ const { entries } = await sb.files.list(path);
1064
+ emit(
1065
+ ctx,
1066
+ entries,
1067
+ () => table(
1068
+ entries.map((e) => ({ ...e, size: e.isDir ? "dir" : fmtBytes(e.size), modifiedAt: fmtDate(e.modifiedAt) })),
1069
+ [["name", "NAME"], ["size", "SIZE"], ["modifiedAt", "MODIFIED"]],
1070
+ "Empty directory."
1071
+ )
1072
+ );
1073
+ } else if (action === "read" || action === "cat") {
1074
+ const out = str(ctx, "out");
1075
+ if (out) {
1076
+ const r = await sb.files.read(path, { encoding: "base64" });
1077
+ writeFileSync3(out, Buffer.from(r.content, "base64"));
1078
+ emit(ctx, { path, out, size: r.size }, () => console.error(`Saved ${fmtBytes(r.size)} to ${out}`));
1079
+ } else {
1080
+ const r = await sb.files.read(path);
1081
+ emit(ctx, r, () => process.stdout.write(r.content));
1082
+ }
1083
+ } else if (action === "write") {
1084
+ const local = ctx.args[3];
1085
+ const inline = str(ctx, "content");
1086
+ let buf;
1087
+ if (inline != null) buf = Buffer.from(inline, "utf8");
1088
+ else if (local) {
1089
+ if (!existsSync4(local)) throw usageError(`File not found: ${local}`, FILES_USAGE);
1090
+ buf = readFileSync4(local);
1091
+ } else if (!process.stdin.isTTY) buf = await readStdin();
1092
+ else throw usageError("Give a local file, --content, or pipe data on stdin.", FILES_USAGE);
1093
+ const r = await sb.files.write(path, buf.toString("base64"), { encoding: "base64" });
1094
+ emit(ctx, { path, ...r }, () => console.log(`Wrote ${fmtBytes(r.bytes)} to ${path}`));
1095
+ } else {
1096
+ throw usageError(`Unknown files action "${action}".`, FILES_USAGE);
1097
+ }
1098
+ }
1099
+ },
1100
+ suspend: {
1101
+ summary: "Suspend to disk (frees CPU; TTL paused)",
1102
+ usage: "easybits sandboxes suspend <sandbox-id>",
1103
+ examples: ["easybits sandboxes suspend sb_abc123"],
1104
+ async run(ctx) {
1105
+ const r = await (await sandbox(ctx, this.usage)).suspend();
1106
+ emit(ctx, r, () => console.log(`Suspended ${r.sandboxId ?? ctx.args[0]} (${r.status ?? "suspended"})`));
1107
+ }
1108
+ },
1109
+ resume: {
1110
+ summary: "Wake a suspended sandbox",
1111
+ usage: "easybits sandboxes resume <sandbox-id> [--env K=V]...",
1112
+ options: { env: { type: "string", multiple: true, value: "K=V", description: "Rewrite env on wake (repeatable)" } },
1113
+ examples: ["easybits sandboxes resume sb_abc123"],
1114
+ async run(ctx) {
1115
+ const env = pairs(list(ctx, "env"), this.usage);
1116
+ const r = await (await sandbox(ctx, this.usage)).resume(Object.keys(env).length ? { env } : {});
1117
+ emit(ctx, r, () => console.log(`Resumed ${r.sandboxId ?? ctx.args[0]} (${r.status ?? "running"})`));
1118
+ }
1119
+ },
1120
+ destroy: {
1121
+ aliases: ["rm", "delete"],
1122
+ summary: "Destroy a sandbox (irreversible)",
1123
+ usage: "easybits sandboxes destroy <sandbox-id>",
1124
+ examples: ["easybits sandboxes destroy sb_abc123"],
1125
+ async run(ctx) {
1126
+ const r = await (await sandbox(ctx, this.usage)).destroy();
1127
+ emit(ctx, { ...r, sandboxId: ctx.args[0] }, () => console.log(`Destroyed ${ctx.args[0]}`));
1128
+ }
1129
+ },
1130
+ snapshot: {
1131
+ summary: "Capture a copy-on-write snapshot without stopping the box",
1132
+ usage: "easybits sandboxes snapshot <sandbox-id> [--name <name>]",
1133
+ options: { name: { type: "string", value: "name", description: "Snapshot label" } },
1134
+ examples: ["easybits sandboxes snapshot sb_abc123 --name before-upgrade"],
1135
+ async run(ctx) {
1136
+ const r = await (await sandbox(ctx, this.usage)).snapshot(str(ctx, "name"));
1137
+ emit(ctx, r, () => console.log(`Snapshot ${r.snapshotId} (${fmtBytes(r.sizeBytes)})`));
1138
+ }
1139
+ }
1140
+ }
1141
+ };
1142
+
1143
+ // src/commands/machines.ts
1144
+ var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine-id> [KEY=VALUE... | NAME]";
1145
+ var machines = {
1146
+ name: "machines",
1147
+ aliases: ["deploy", "machine"],
1148
+ group: "Hosting",
1149
+ summary: "Permanent machines and their releases (alias: deploy)",
1150
+ synopsis: "machines",
1151
+ subs: {
1152
+ ls: {
1153
+ aliases: ["list"],
1154
+ summary: "List your permanent machines",
1155
+ usage: "easybits machines ls",
1156
+ examples: ["easybits machines ls", "easybits deploy ls --json"],
1157
+ async run(ctx) {
1158
+ const eb = await getClient(ctx);
1159
+ const rows = (await eb.machines.list()).map(sandboxRecord);
1160
+ emit(
1161
+ ctx,
1162
+ rows,
1163
+ () => table(
1164
+ rows,
1165
+ [["sandboxId", "ID"], ["name", "NAME"], ["tier", "TIER"], ["status", "STATUS"], ["monthlyMxn", "MXN/MONTH"]],
1166
+ "No permanent machines. See: easybits docs hosting"
1167
+ )
1168
+ );
1169
+ }
1170
+ },
1171
+ deploy: {
1172
+ aliases: ["release"],
1173
+ summary: "Publish the machine's current app code as a new release",
1174
+ usage: "easybits machines deploy <machine-id> [--message <text>]",
1175
+ options: { message: { type: "string", short: "m", value: "text", description: "Release note" } },
1176
+ examples: ['easybits machines deploy sb_abc123 -m "v1.2: fix checkout"'],
1177
+ async run(ctx) {
1178
+ const id = need(ctx, 0, "machine-id", this.usage);
1179
+ const eb = await getClient(ctx);
1180
+ const r = await eb.machines.deploy(id, { message: str(ctx, "message") });
1181
+ emit(ctx, r, () => console.log(`Release v${r.version} ${r.releaseId} (${r.status}, ${fmtBytes(r.sizeBytes)})`));
1182
+ }
1183
+ },
1184
+ releases: {
1185
+ summary: "List releases, newest first",
1186
+ usage: "easybits machines releases <machine-id> [--limit <n>]",
1187
+ options: { limit: { type: "string", value: "n", description: "How many" } },
1188
+ examples: ["easybits machines releases sb_abc123 --limit 5"],
1189
+ async run(ctx) {
1190
+ const id = need(ctx, 0, "machine-id", this.usage);
1191
+ const eb = await getClient(ctx);
1192
+ const r = await eb.machines.releases(id, { limit: int(ctx, "limit", this.usage) });
1193
+ emit(
1194
+ ctx,
1195
+ r,
1196
+ () => table(
1197
+ r.items.map((x) => ({ ...x, size: fmtBytes(x.sizeBytes), createdAt: fmtDate(x.createdAt) })),
1198
+ [["version", "VERSION"], ["releaseId", "ID"], ["status", "STATUS"], ["size", "SIZE"], ["createdAt", "CREATED"], ["message", "MESSAGE"]],
1199
+ "No releases yet. Publish one: easybits machines deploy <machine-id>"
1200
+ )
1201
+ );
1202
+ }
1203
+ },
1204
+ logs: {
1205
+ summary: "Tail the app's own log",
1206
+ usage: "easybits machines logs <machine-id> [--lines <n>] [--grep <text>]",
1207
+ options: {
1208
+ lines: { type: "string", value: "n", description: "Last N lines" },
1209
+ grep: { type: "string", value: "text", description: "Only lines matching" }
1210
+ },
1211
+ examples: ["easybits machines logs sb_abc123 --lines 100", "easybits machines logs sb_abc123 --grep ERROR"],
1212
+ async run(ctx) {
1213
+ const id = need(ctx, 0, "machine-id", this.usage);
1214
+ const eb = await getClient(ctx);
1215
+ const r = await eb.machines.logs(id, { lines: int(ctx, "lines", this.usage), grep: str(ctx, "grep") });
1216
+ emit(ctx, r, () => process.stdout.write(r.output.endsWith("\n") || !r.output ? r.output : r.output + "\n"));
1217
+ }
1218
+ },
1219
+ rollback: {
1220
+ summary: "Put a previous release back on the same machine (data untouched)",
1221
+ usage: "easybits machines rollback <machine-id> <release-id>",
1222
+ examples: ["easybits machines rollback sb_abc123 rel_789"],
1223
+ async run(ctx) {
1224
+ const id = need(ctx, 0, "machine-id", this.usage);
1225
+ const rel = need(ctx, 1, "release-id", this.usage);
1226
+ const eb = await getClient(ctx);
1227
+ const r = await eb.machines.rollback(id, rel);
1228
+ emit(ctx, r, () => console.log(`Rolled back to v${r.version} (exit ${r.exitCode})`));
1229
+ if (r.exitCode !== 0) process.exitCode = 1;
1230
+ }
1231
+ },
1232
+ secrets: {
1233
+ summary: "App env vars stored encrypted in your vault",
1234
+ usage: SECRETS_USAGE,
1235
+ examples: [
1236
+ "easybits machines secrets ls sb_abc123",
1237
+ "easybits machines secrets set sb_abc123 DATABASE_URL=postgres://\u2026 API_KEY=xyz",
1238
+ "easybits machines secrets unset sb_abc123 API_KEY"
1239
+ ],
1240
+ async run(ctx) {
1241
+ const action = need(ctx, 0, "ls|set|unset", SECRETS_USAGE);
1242
+ const id = need(ctx, 1, "machine-id", SECRETS_USAGE);
1243
+ const eb = await getClient(ctx);
1244
+ if (action === "ls" || action === "list") {
1245
+ const r = await eb.machines.secrets(id);
1246
+ emit(ctx, r, () => {
1247
+ console.log(`Injected: ${r.secretNames.length ? r.secretNames.join(", ") : "(none)"}`);
1248
+ console.log(`In vault: ${r.inVault.length ? r.inVault.join(", ") : "(none)"}`);
1249
+ });
1250
+ } else if (action === "set") {
1251
+ const values = pairs(ctx.args.slice(2), SECRETS_USAGE);
1252
+ if (!Object.keys(values).length) throw usageError("Give at least one KEY=VALUE.", SECRETS_USAGE);
1253
+ const r = await eb.machines.setSecrets(id, values);
1254
+ emit(ctx, r, () => console.log(`Set ${Object.keys(values).join(", ")}. Injected now: ${r.secretNames.join(", ")}`));
1255
+ } else if (action === "unset") {
1256
+ const name = need(ctx, 2, "NAME", SECRETS_USAGE);
1257
+ const r = await eb.machines.unsetSecret(id, name);
1258
+ emit(ctx, r, () => console.log(`Unset ${name} (value kept in the vault)`));
1259
+ } else {
1260
+ throw usageError(`Unknown secrets action "${action}".`, SECRETS_USAGE);
1261
+ }
1262
+ }
1263
+ }
1264
+ }
1265
+ };
1266
+
1267
+ // src/commands/domains.ts
1268
+ async function sandbox2(ctx, usage2) {
1269
+ const id = need(ctx, 0, "sandbox-id", usage2);
1270
+ return (await getClient(ctx)).sandboxes.get(id);
1271
+ }
1272
+ var domains = {
1273
+ name: "domains",
1274
+ aliases: ["domain"],
1275
+ group: "Hosting",
1276
+ summary: "Custom domains with auto TLS on a sandbox or machine",
1277
+ synopsis: "domains",
1278
+ subs: {
1279
+ ls: {
1280
+ aliases: ["list"],
1281
+ summary: "List domains attached to a sandbox",
1282
+ usage: "easybits domains ls <sandbox-id>",
1283
+ examples: ["easybits domains ls sb_abc123"],
1284
+ async run(ctx) {
1285
+ const items = await (await sandbox2(ctx, this.usage)).listDomains();
1286
+ emit(ctx, items, () => table(items, [["domain", "DOMAIN"], ["port", "PORT"]], "No custom domains."));
1287
+ }
1288
+ },
1289
+ add: {
1290
+ summary: "Attach a domain to a port; prints the DNS record to create",
1291
+ usage: "easybits domains add <sandbox-id> <domain> --port <port>",
1292
+ options: { port: { type: "string", value: "port", description: "Port inside the sandbox (required)" } },
1293
+ examples: ["easybits domains add sb_abc123 shop.example.com --port 3000"],
1294
+ async run(ctx) {
1295
+ const domain = need(ctx, 1, "domain", this.usage);
1296
+ const port = int(ctx, "port", this.usage);
1297
+ if (port == null) throw usageError("Missing --port.", this.usage);
1298
+ const r = await (await sandbox2(ctx, this.usage)).addDomain(domain, port);
1299
+ emit(ctx, r, () => {
1300
+ console.log(`Attached ${r.domain} \u2192 port ${r.port}`);
1301
+ console.log(`Create this DNS record: ${JSON.stringify(r.dns)}`);
1302
+ console.log(`Then run: easybits domains verify ${ctx.args[0]} ${r.domain}`);
1303
+ });
1304
+ }
1305
+ },
1306
+ verify: {
1307
+ summary: "Check DNS + HTTPS for a domain",
1308
+ usage: "easybits domains verify <sandbox-id> <domain>",
1309
+ examples: ["easybits domains verify sb_abc123 shop.example.com"],
1310
+ async run(ctx) {
1311
+ const domain = need(ctx, 1, "domain", this.usage);
1312
+ const r = await (await sandbox2(ctx, this.usage)).verifyDomain(domain);
1313
+ emit(ctx, r, () => {
1314
+ console.log(`${r.domain}: ${r.ready ? "ready" : "not ready yet"}`);
1315
+ console.log(`DNS resolved: ${r.dns.resolved} HTTPS ok: ${r.https.ok}`);
1316
+ if (r.hint) console.log(r.hint);
1317
+ });
1318
+ if (!r.ready) process.exitCode = 1;
1319
+ }
1320
+ },
1321
+ rm: {
1322
+ aliases: ["remove", "delete"],
1323
+ summary: "Detach a domain",
1324
+ usage: "easybits domains rm <sandbox-id> <domain>",
1325
+ examples: ["easybits domains rm sb_abc123 shop.example.com"],
1326
+ async run(ctx) {
1327
+ const domain = need(ctx, 1, "domain", this.usage);
1328
+ const r = await (await sandbox2(ctx, this.usage)).removeDomain(domain);
1329
+ emit(ctx, r, () => console.log(`Removed ${domain}`));
1330
+ }
1331
+ }
1332
+ }
1333
+ };
1334
+
1335
+ // src/commands/db.ts
1336
+ async function findDb(eb, ref) {
1337
+ const { items } = await eb.listDatabases();
1338
+ const hit = items.find((d) => d.id === ref) ?? items.find((d) => d.name === ref);
1339
+ if (!hit) throw new CliError(`No database "${ref}".`, 1, "List them with: easybits db ls", "not_found", 404);
1340
+ return hit;
1341
+ }
1342
+ var db = {
1343
+ name: "db",
1344
+ aliases: ["databases"],
1345
+ group: "Data",
1346
+ summary: "SQL databases (libSQL)",
1347
+ synopsis: "db",
1348
+ subs: {
1349
+ ls: {
1350
+ aliases: ["list"],
1351
+ summary: "List your databases",
1352
+ usage: "easybits db ls",
1353
+ examples: ["easybits db ls --json"],
1354
+ async run(ctx) {
1355
+ const eb = await getClient(ctx);
1356
+ const { items } = await eb.listDatabases();
1357
+ emit(
1358
+ ctx,
1359
+ items,
1360
+ () => table(
1361
+ items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
1362
+ [["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
1363
+ "No databases. Create one: easybits db create <name>"
1364
+ )
1365
+ );
1366
+ }
1367
+ },
1368
+ create: {
1369
+ summary: "Create a database",
1370
+ usage: "easybits db create <name> [--description <text>]",
1371
+ options: { description: { type: "string", value: "text", description: "What it is for" } },
1372
+ examples: ["easybits db create leads --description 'CRM leads'"],
1373
+ async run(ctx) {
1374
+ const name = need(ctx, 0, "name", this.usage);
1375
+ const eb = await getClient(ctx);
1376
+ const d = await eb.createDatabase({ name, description: str(ctx, "description") });
1377
+ emit(ctx, d, () => console.log(`Created ${d.name} (${d.id})`));
1378
+ }
1379
+ },
1380
+ rm: {
1381
+ aliases: ["delete", "drop"],
1382
+ summary: "Delete a database (irreversible)",
1383
+ usage: "easybits db rm <db-id|name>",
1384
+ examples: ["easybits db rm leads"],
1385
+ async run(ctx) {
1386
+ const ref = need(ctx, 0, "db-id|name", this.usage);
1387
+ const eb = await getClient(ctx);
1388
+ const hit = await findDb(eb, ref);
1389
+ const r = await eb.deleteDatabase(hit.id);
1390
+ emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(`Deleted ${hit.name} (${hit.id})`));
1391
+ }
1392
+ },
1393
+ query: {
1394
+ aliases: ["sql"],
1395
+ summary: "Run one SQL statement (by database id or name)",
1396
+ usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
1397
+ options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
1398
+ examples: [
1399
+ "easybits db query leads 'SELECT * FROM leads LIMIT 10'",
1400
+ "easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
1401
+ "easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
1402
+ ],
1403
+ async run(ctx) {
1404
+ const ref = need(ctx, 0, "db-id|name", this.usage);
1405
+ const sql = need(ctx, 1, "sql", this.usage);
1406
+ const eb = await getClient(ctx);
1407
+ const hit = await findDb(eb, ref);
1408
+ const args = list(ctx, "arg");
1409
+ const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
1410
+ emit(ctx, r, () => {
1411
+ if (r.cols.length) {
1412
+ table(
1413
+ r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
1414
+ r.cols.map((c) => [c, c]),
1415
+ "(0 rows)"
1416
+ );
1417
+ } else {
1418
+ console.log(`OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`);
1419
+ }
1420
+ });
1421
+ }
1422
+ }
1423
+ }
1424
+ };
1425
+
1426
+ // src/commands/agents.ts
1427
+ var agents = {
1428
+ name: "agents",
1429
+ aliases: ["agent"],
1430
+ group: "Compute",
1431
+ summary: "Agents running in their own sandbox",
1432
+ synopsis: "agents",
1433
+ subs: {
1434
+ ls: {
1435
+ aliases: ["list"],
1436
+ summary: "List your agents",
1437
+ usage: "easybits agents ls",
1438
+ examples: ["easybits agents ls --json"],
1439
+ async run(ctx) {
1440
+ const eb = await getClient(ctx);
1441
+ const items = await eb.listAgents();
1442
+ emit(
1443
+ ctx,
1444
+ items,
1445
+ () => table(
1446
+ items.map((a) => ({ ...a, createdAt: fmtDate(a.createdAt) })),
1447
+ [["agentId", "ID"], ["name", "NAME"], ["template", "TEMPLATE"], ["status", "STATUS"], ["createdAt", "CREATED"]],
1448
+ "No agents. Create one: easybits agents create --template <template>"
1449
+ )
1450
+ );
1451
+ }
1452
+ },
1453
+ create: {
1454
+ summary: "Create an agent from a template",
1455
+ usage: "easybits agents create --template <template> [--name <name>] [--env K=V]... [--timeout <s>]",
1456
+ options: {
1457
+ template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
1458
+ name: { type: "string", value: "name", description: "Label" },
1459
+ env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent (repeatable)" },
1460
+ timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" }
1461
+ },
1462
+ examples: [
1463
+ "easybits agents create --template goose --name helper",
1464
+ "easybits agents create --template chat-anthropic --env ANTHROPIC_API_KEY=sk-ant-\u2026 --json"
1465
+ ],
1466
+ async run(ctx) {
1467
+ const template = str(ctx, "template");
1468
+ if (!template) throw usageError("Missing --template.", this.usage);
1469
+ const eb = await getClient(ctx);
1470
+ const a = await eb.createAgent({
1471
+ template,
1472
+ name: str(ctx, "name"),
1473
+ env: pairs(list(ctx, "env"), this.usage),
1474
+ timeoutSeconds: int(ctx, "timeout", this.usage)
1475
+ });
1476
+ emit(ctx, a, () => {
1477
+ console.log(`Agent: ${a.agentId}`);
1478
+ console.log(`Sandbox: ${a.sandboxId}`);
1479
+ console.log(`URL: ${a.agentUrl}`);
1480
+ console.log(`Talk to it: easybits agents message ${a.agentId} "hello"`);
1481
+ });
1482
+ }
1483
+ },
1484
+ message: {
1485
+ aliases: ["msg", "send"],
1486
+ summary: "Send a message; streams the reply",
1487
+ usage: "easybits agents message <agent-id> <text...> [--session <id>]",
1488
+ options: { session: { type: "string", value: "id", description: "Continue a conversation" } },
1489
+ examples: ['easybits agents message ag_123 "summarize /data/work/README.md"', "easybits agents message ag_123 hi --json"],
1490
+ async run(ctx) {
1491
+ const id = need(ctx, 0, "agent-id", this.usage);
1492
+ const content = ctx.args.slice(1).join(" ");
1493
+ if (!content) throw usageError("Missing <text>.", this.usage);
1494
+ const eb = await getClient(ctx);
1495
+ const params = { content, sessionId: str(ctx, "session") };
1496
+ if (ctx.json) {
1497
+ emit(ctx, await eb.messageAgent(id, params), () => {
1498
+ });
1499
+ return;
1500
+ }
1501
+ for await (const tok of eb.streamAgent(id, params)) process.stdout.write(tok);
1502
+ process.stdout.write("\n");
1503
+ }
1504
+ },
1505
+ destroy: {
1506
+ aliases: ["rm", "delete"],
1507
+ summary: "Destroy an agent and its sandbox",
1508
+ usage: "easybits agents destroy <agent-id>",
1509
+ examples: ["easybits agents destroy ag_123"],
1510
+ async run(ctx) {
1511
+ const id = need(ctx, 0, "agent-id", this.usage);
1512
+ const eb = await getClient(ctx);
1513
+ const r = await eb.destroyAgent(id);
1514
+ emit(ctx, { ...r, agentId: id }, () => console.log(`Destroyed ${id}`));
1515
+ }
1516
+ }
1517
+ }
1518
+ };
1519
+
1520
+ // src/commands/docs.ts
1521
+ import { spawn as spawn2 } from "child_process";
1522
+ import { resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
1523
+ var docs = {
1524
+ name: "docs",
1525
+ group: "Help",
1526
+ summary: "Print the docs URL, or a docs section as markdown",
1527
+ synopsis: "docs [section]",
1528
+ leaf: {
1529
+ summary: "Print the docs URL, or a docs section as markdown",
1530
+ usage: "easybits docs [section] [--en] [--open]",
1531
+ options: {
1532
+ en: { type: "boolean", description: "English docs" },
1533
+ open: { type: "boolean", description: "Open the docs in your browser" }
1534
+ },
1535
+ examples: ["easybits docs", "easybits docs cli --en", "easybits docs hosting", "easybits docs --open"],
1536
+ async run(ctx) {
1537
+ const base = await resolveBaseUrl4();
1538
+ const prefix = bool(ctx, "en") ? "/en/docs" : "/docs";
1539
+ const section = ctx.args[0];
1540
+ if (!section) {
1541
+ const url2 = `${base}${prefix}`;
1542
+ if (bool(ctx, "open")) {
1543
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
1544
+ spawn2(opener, [url2], { stdio: "ignore", detached: true }).unref();
1545
+ }
1546
+ emit(ctx, { url: url2, markdown: `${url2}.md`, llms: `${base}/llms.txt` }, () => console.log(url2));
1547
+ return;
1548
+ }
1549
+ const url = `${base}${prefix}/${encodeURIComponent(section)}.md`;
1550
+ const res = await fetch(url);
1551
+ if (!res.ok) {
1552
+ throw new CliError(
1553
+ `Docs section "${section}" not found (${res.status}).`,
1554
+ 2,
1555
+ `See the section list: ${base}/llms.txt`,
1556
+ "usage",
1557
+ res.status
1558
+ );
1559
+ }
1560
+ const markdown = await res.text();
1561
+ emit(ctx, { section, url, markdown }, () => process.stdout.write(markdown.endsWith("\n") ? markdown : markdown + "\n"));
1562
+ }
1563
+ }
1564
+ };
1565
+
1566
+ // src/commands/index.ts
1567
+ var COMMANDS = [
1568
+ login,
1569
+ logout,
1570
+ usage,
1571
+ sandboxes,
1572
+ agents,
1573
+ machines,
1574
+ domains,
1575
+ init,
1576
+ db,
1577
+ files,
1578
+ websites,
1579
+ providers,
1580
+ config,
1581
+ mcp,
1582
+ sshKey,
1583
+ sshProxy,
1584
+ docs
1585
+ ];
1586
+ function findCommand(name) {
1587
+ return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
1588
+ }
1589
+
1590
+ // src/help.ts
1591
+ var GLOBAL_FLAGS = `Global flags:
1592
+ --json Machine output: JSON on stdout, errors included
1593
+ --token <key> API key for this call (EASYBITS_API_KEY wins if set)
1594
+ -h, --help Help (also: easybits <command> --help)
1595
+ -v, --version Print the CLI version
1596
+
1597
+ Exit codes: 0 ok, 1 API error, 2 usage error, 3 not logged in or bad key`;
1598
+ var COL = 17;
1599
+ function globalHelp(version) {
1600
+ const groups = /* @__PURE__ */ new Map();
1601
+ for (const c of COMMANDS) groups.set(c.group, [...groups.get(c.group) ?? [], c]);
1602
+ const lines = [`easybits ${version} \u2014 the cloud for AI agents, from your terminal`, "", "Usage: easybits <command> [subcommand] [args] [flags]", ""];
1603
+ for (const [group, cmds] of groups) {
1604
+ lines.push(`${group}:`);
1605
+ for (const c of cmds) {
1606
+ lines.push(` ${c.synopsis.padEnd(COL)}${c.summary}`);
1607
+ if (c.subs) lines.push(` ${"".padEnd(COL)}${Object.keys(c.subs).join(" ")}`);
1608
+ }
1609
+ lines.push("");
1610
+ }
1611
+ lines.push(GLOBAL_FLAGS, "", "SSH to a sandbox \u2014 add to ~/.ssh/config:", indent(SSH_CONFIG_SNIPPET, 2), "");
1612
+ lines.push("Docs: https://www.easybits.cloud/docs/cli.md (or: easybits docs cli)");
1613
+ return lines.join("\n");
1614
+ }
1615
+ function commandHelp(c) {
1616
+ if (c.leaf) return leafHelp(c.leaf);
1617
+ const subs = Object.entries(c.subs ?? {});
1618
+ const width = Math.max(...subs.map(([n]) => n.length)) + 2;
1619
+ const lines = [`easybits ${c.name} \u2014 ${c.summary}`, "", `Usage: easybits ${c.name} <subcommand> [args] [flags]`, ""];
1620
+ if (c.aliases?.length) lines.push(`Aliases: ${c.aliases.join(", ")}`, "");
1621
+ lines.push("Subcommands:");
1622
+ for (const [n, l] of subs) lines.push(` ${n.padEnd(width)}${l.summary}`);
1623
+ lines.push("", `Run: easybits ${c.name} <subcommand> --help`);
1624
+ return lines.join("\n");
1625
+ }
1626
+ function leafHelp(l) {
1627
+ const lines = [l.summary, "", `Usage: ${l.usage}`];
1628
+ if (l.aliases?.length) lines.push(`Aliases: ${l.aliases.join(", ")}`);
1629
+ const opts = Object.entries(l.options ?? {});
1630
+ if (opts.length) {
1631
+ const flag = ([n, o]) => `${o.short ? `-${o.short}, ` : ""}--${n}${o.type === "string" ? ` <${o.value ?? "value"}>` : ""}`;
1632
+ const width = Math.max(...opts.map((o) => flag(o).length)) + 2;
1633
+ lines.push("", "Flags:");
1634
+ for (const o of opts) lines.push(` ${flag(o).padEnd(width)}${o[1].description ?? ""}`);
1635
+ }
1636
+ if (l.examples?.length) {
1637
+ lines.push("", "Examples:");
1638
+ for (const e of l.examples) lines.push(` ${e}`);
1639
+ }
1640
+ lines.push("", "Add --json for machine output.");
1641
+ return lines.join("\n");
1642
+ }
1643
+ function indent(s, n) {
1644
+ return s.split("\n").map((l) => " ".repeat(n) + l).join("\n");
1645
+ }
1646
+
1647
+ // src/banner.ts
1648
+ var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;2;152;112;237m\u28C0\u28C0\u2840 \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28E4\u28FF\x1B[0m\n \x1B[38;2;244;245;251m\u28A0\u2854\u2809\u2809\u2809\u2822\u2840\u285E\x1B[38;2;152;112;237m\u2880\u2874\u288B\u282D\x1B[38;2;244;245;251m\u2809\u28AF\x1B[38;2;152;112;237m\u28FD\u28F7\u2844 \x1B[38;2;244;245;251m\u28FF\u28FF \u28E4\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28E4\u28E4 \u28E4\u28FF\u28FF\u28E4\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4\x1B[0m\n \x1B[38;2;152;112;237m\u2880\x1B[38;2;244;245;251m\u28FF\x1B[38;2;152;112;237m\u289B\u280D\u2809\u2899\u2873\u28FC\u2857\u28FE\u28B0\x1B[38;2;244;245;251m\u28A1\u28F6\u28E4\u2818\u2846 \x1B[38;2;152;112;237m\u28BB \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF \u28E4\u28E4\u28E4\u28E4\u28FF\u28FF \u28FF\u28FF\u28E4\u28E4\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u281B\u28FF\u28FF\u281B\u281B \u28FF\u28FF\u28E4\u28E4\u28E4\x1B[0m\n\x1B[38;2;152;112;237m\u28E4\u28C4\u28C0\u28FF\x1B[38;2;244;245;251m\u28DF\x1B[38;2;152;112;237m\u2801 \x1B[38;2;244;245;251m\u28FE\u28FF\u280E\x1B[38;2;152;112;237m\u28E7\u28BF\u284C\x1B[38;2;244;245;251m\u2808\u2819\u280B \u2847\x1B[38;2;152;112;237m\u2880\u287E \x1B[38;2;244;245;251m\u28FF\u28FF \u28FF\u28FF\u281B\u281B\u281B\u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28E4\u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF\x1B[0m\n\x1B[38;2;152;112;237m\u2801 \u2808\u28E7\x1B[38;2;244;245;251m\u28BF\u28C6 \u2809\x1B[38;2;152;112;237m\u2880\u285F\x1B[38;2;244;245;251m\u28E7\x1B[38;2;152;112;237m\u2859\u2832\u28A4\u28E4\u2874\u2835\u280B \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B \u28E4\u28FF\u28FF\u281B \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u281B \u28FF\u28FF \u281B\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;152;112;237m\u2808\u283B\x1B[38;2;244;245;251m\u28BF\u28E6\x1B[38;2;152;112;237m\u28C0\u28E0\u28F4\u281F\x1B[38;2;244;245;251m\u2801\u2808\u2819\u2833\u2836\u280A \u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;244;245;251m\u2808\u2809\u2809\u2801\x1B[0m";
1649
+
1650
+ // src/index.ts
1651
+ import { EasybitsError as EasybitsError2 } from "@easybits.cloud/sdk";
1652
+ var VERSION = true ? "0.5.1" : "dev";
1653
+ var GLOBAL_OPTIONS = {
1654
+ json: { type: "boolean" },
1655
+ token: { type: "string" },
1656
+ help: { type: "boolean", short: "h" },
1657
+ version: { type: "boolean", short: "v" }
1658
+ };
1659
+ function wantsJson(argv2) {
1660
+ const end = argv2.indexOf("--");
1661
+ return (end === -1 ? argv2 : argv2.slice(0, end)).includes("--json");
1662
+ }
1663
+ function showBanner(json) {
1664
+ if (!BANNER || json || !process.stdout.isTTY || process.env.NO_COLOR) return;
1665
+ process.stdout.write((BANNER.endsWith("\n") ? BANNER : BANNER + "\n") + "\n");
1666
+ }
1667
+ function positionalIndexes(argv2) {
1668
+ const out = [];
1669
+ for (let i = 0; i < argv2.length; i++) {
1670
+ const a = argv2[i];
1671
+ if (a === "--") break;
1672
+ if (a === "--token") {
1673
+ i++;
1674
+ continue;
1675
+ }
1676
+ if (a.startsWith("-") && a !== "-") continue;
1677
+ out.push(i);
1678
+ }
1679
+ return out;
1680
+ }
1681
+ function parse(argv2, skip, leaf) {
1682
+ const rest = argv2.filter((_, i) => !skip.includes(i));
1683
+ try {
1684
+ const { values, positionals } = parseArgs({
1685
+ args: rest,
1686
+ options: { ...GLOBAL_OPTIONS, ...leaf?.options ?? {} },
1687
+ allowPositionals: true,
1688
+ strict: true
383
1689
  });
384
- console.error(`easybits: llave creada en ${key}`);
385
- }
386
- process.stdout.write(readFileSync2(pub, "utf8").trim() + "\n");
387
- }
388
- var [cmd, sub] = [process.argv[2], process.argv[3]];
389
- switch (cmd) {
390
- case "login":
391
- login();
392
- break;
393
- case "files":
394
- if (sub === "list" || !sub) filesList();
395
- else if (sub === "upload") filesUpload();
396
- else if (sub === "delete") filesDelete();
397
- else console.error(`Unknown: files ${sub}`);
398
- break;
399
- case "providers":
400
- providersList();
401
- break;
402
- case "init":
403
- init();
404
- break;
405
- case "config":
406
- printMcpConfig();
407
- break;
408
- case "mcp":
409
- printMcpStdioConfig();
410
- break;
411
- case "ssh-proxy":
412
- sshProxy();
413
- break;
414
- case "ssh-key":
415
- sshKey();
416
- break;
417
- case "help":
418
- case void 0:
419
- console.log(`easybits CLI \u2014 @easybits.cloud/cli
420
-
421
- Commands:
422
- login <key> Save API key
423
- init Write the GitHub Actions deploy workflow
424
- (--app-dir /srv/app, --port 3000)
425
- files list List your files
426
- files upload <f> Upload a file
427
- files delete <id> Delete a file
428
- providers list Show storage providers
429
- config Print MCP config JSON (streamable HTTP)
430
- mcp Print MCP stdio config JSON
431
- ssh-key Print your public key for sandbox_ssh_enable (creates it once)
432
- ssh-proxy <id> SSH tunnel over 443 (use as ssh ProxyCommand)
433
-
434
- SSH to a box \u2014 add to ~/.ssh/config:
435
- Host *.ghosty
436
- ProxyCommand easybits ssh-proxy %h
437
- User root`);
438
- break;
439
- default:
440
- console.error(`Unknown command: ${cmd}. Run 'easybits help'`);
1690
+ return { values, positionals };
1691
+ } catch (e) {
1692
+ throw usageError(e.message.split("\n")[0].split(". To specify")[0].replace(/\.?$/, "."), leaf?.usage);
1693
+ }
1694
+ }
1695
+ async function main(argv2) {
1696
+ const pos = positionalIndexes(argv2);
1697
+ const cmdName = pos.length ? argv2[pos[0]] : void 0;
1698
+ if (!cmdName || cmdName === "help") {
1699
+ const topic = cmdName === "help" && pos[1] != null ? findCommand(argv2[pos[1]]) : void 0;
1700
+ const { values: values2 } = parse(argv2, pos.slice(0, 2), void 0);
1701
+ if (values2.version) {
1702
+ console.log(VERSION);
1703
+ return;
1704
+ }
1705
+ if (topic) {
1706
+ console.log(commandHelp(topic));
1707
+ return;
1708
+ }
1709
+ showBanner(values2.json === true);
1710
+ console.log(globalHelp(VERSION));
1711
+ return;
1712
+ }
1713
+ const cmd = findCommand(cmdName);
1714
+ if (!cmd) throw usageError(`Unknown command "${cmdName}".`);
1715
+ let leaf = cmd.leaf;
1716
+ const skip = [pos[0]];
1717
+ if (cmd.subs) {
1718
+ const subName = pos[1] != null ? argv2[pos[1]] : void 0;
1719
+ const entry = subName ? Object.entries(cmd.subs).find(([n, l]) => n === subName || l.aliases?.includes(subName)) : void 0;
1720
+ if (entry) {
1721
+ leaf = entry[1];
1722
+ skip.push(pos[1]);
1723
+ } else if (subName) {
1724
+ throw usageError(`Unknown subcommand "${cmd.name} ${subName}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs).join("|")}>`);
1725
+ } else if (cmd.defaultSub && !argv2.includes("--help") && !argv2.includes("-h")) {
1726
+ leaf = cmd.subs[cmd.defaultSub];
1727
+ } else if (!argv2.includes("--help") && !argv2.includes("-h")) {
1728
+ throw usageError(`Missing subcommand for "${cmd.name}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs).join("|")}>`);
1729
+ }
1730
+ }
1731
+ const { values, positionals } = parse(argv2, skip, leaf);
1732
+ if (values.version) {
1733
+ console.log(VERSION);
1734
+ return;
1735
+ }
1736
+ if (values.help || !leaf) {
1737
+ console.log(leaf && (leaf !== cmd.subs?.[cmd.defaultSub ?? ""] || skip.length > 1) ? leafHelp(leaf) : commandHelp(cmd));
1738
+ return;
1739
+ }
1740
+ const ctx = {
1741
+ json: values.json === true,
1742
+ token: typeof values.token === "string" ? values.token : void 0,
1743
+ args: positionals,
1744
+ opts: values
1745
+ };
1746
+ try {
1747
+ await leaf.run.call(leaf, ctx);
1748
+ } catch (e) {
1749
+ if (e instanceof EasybitsError2 && e.status === 401 && usedSession() && await forceRefresh()) {
1750
+ await leaf.run.call(leaf, ctx);
1751
+ return;
1752
+ }
1753
+ throw e;
1754
+ }
441
1755
  }
1756
+ var argv = process.argv.slice(2);
1757
+ main(argv).catch((err) => {
1758
+ const e = toCliError(err);
1759
+ if (wantsJson(argv)) {
1760
+ process.stdout.write(JSON.stringify({ error: e.message, code: e.exitCode, ...e.hint ? { hint: e.hint } : {} }) + "\n");
1761
+ } else {
1762
+ process.stderr.write(`Error: ${e.message}
1763
+ `);
1764
+ if (e.hint) process.stderr.write(`${e.hint}
1765
+ `);
1766
+ }
1767
+ process.exitCode = e.exitCode || EXIT.API;
1768
+ });