@easybits.cloud/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -10
  2. package/dist/index.js +281 -63
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -17,7 +17,7 @@ Requires Node 22+.
17
17
 
18
18
  ```bash
19
19
  easybits login # opens your browser (OAuth2 + PKCE); session renews itself
20
- easybits login eb_sk_live_YOUR_KEY # or save an API key instead
20
+ easybits login - < key.txt # or save an API key, read from stdin (never in argv)
21
21
  easybits usage
22
22
  easybits logout
23
23
  ```
@@ -25,13 +25,14 @@ easybits logout
25
25
  Running a command without a session in a terminal starts the login automatically;
26
26
  without a terminal (agents, CI) it exits `3`. Precedence: `EASYBITS_API_KEY` env var >
27
27
  `--token <key>` > browser session > saved API key. Keys: https://www.easybits.cloud/dash/developer.
28
+ In CI and agents, `EASYBITS_API_KEY` needs no login at all.
28
29
 
29
30
  ## Commands
30
31
 
31
32
  ```bash
32
33
  # Sandboxes (alias: sb)
33
34
  easybits sandboxes ls
34
- easybits sandboxes create --template node --name scratch
35
+ easybits sandboxes create --template node --name scratch --dotenv .env
35
36
  easybits sandboxes get sb_abc123
36
37
  easybits sandboxes exec sb_abc123 -- npm test
37
38
  easybits sandboxes logs sb_abc123 --unit myapp --lines 100
@@ -41,7 +42,7 @@ easybits sandboxes files write sb_abc123 /data/work/app.js ./app.js
41
42
  easybits sandboxes suspend sb_abc123
42
43
  easybits sandboxes resume sb_abc123
43
44
  easybits sandboxes snapshot sb_abc123 --name before-upgrade
44
- easybits sandboxes destroy sb_abc123
45
+ easybits sandboxes destroy sb_abc123 --yes
45
46
 
46
47
  # Hosting: permanent machines (alias: deploy)
47
48
  easybits machines ls
@@ -50,7 +51,7 @@ easybits machines releases sb_abc123
50
51
  easybits machines logs sb_abc123 --grep ERROR
51
52
  easybits machines rollback sb_abc123 rel_789
52
53
  easybits machines secrets ls sb_abc123
53
- easybits machines secrets set sb_abc123 DATABASE_URL=postgres://...
54
+ easybits machines secrets set sb_abc123 --dotenv .env.production # - reads stdin
54
55
  easybits machines secrets unset sb_abc123 DATABASE_URL
55
56
  easybits init --port 3000 # GitHub Actions workflow that deploys on every push
56
57
 
@@ -58,24 +59,25 @@ easybits init --port 3000 # GitHub Actions workflow that deploys on e
58
59
  easybits domains ls sb_abc123
59
60
  easybits domains add sb_abc123 shop.example.com --port 3000
60
61
  easybits domains verify sb_abc123 shop.example.com
61
- easybits domains rm sb_abc123 shop.example.com
62
+ easybits domains rm sb_abc123 shop.example.com --yes
62
63
 
63
64
  # Databases
64
65
  easybits db ls
65
66
  easybits db create leads
67
+ easybits db tables leads # tables, row counts, columns
66
68
  easybits db query leads "SELECT * FROM leads" --json
67
- easybits db rm leads
69
+ easybits db rm leads --yes
68
70
 
69
71
  # Agents
70
72
  easybits agents ls
71
- easybits agents create --template goose --name helper
73
+ easybits agents create --template goose --name helper --dotenv .env
72
74
  easybits agents message ag_123 "hello"
73
- easybits agents destroy ag_123
75
+ easybits agents destroy ag_123 --yes
74
76
 
75
77
  # Files, websites, account
76
78
  easybits files ls
77
79
  easybits files upload ./report.pdf
78
- easybits files delete FILE_ID
80
+ easybits files delete FILE_ID --yes
79
81
  easybits websites ls
80
82
  easybits usage
81
83
 
@@ -86,7 +88,18 @@ easybits ssh-key # public key for sandbox SSH
86
88
  easybits docs cli # a docs section as markdown (--en for English)
87
89
  ```
88
90
 
89
- Every command has help with examples: `easybits <command> <subcommand> --help`.
91
+ Every command has help with examples: `easybits <command> <subcommand> --help`
92
+ (or `easybits help <command> <subcommand>`). A typo gets a "Did you mean" suggestion.
93
+
94
+ **Destructive commands** (`db rm`, `agents destroy`, `sandboxes destroy`, `domains rm`,
95
+ `files delete`) ask before acting in a terminal: `[y/N]`, or typing the name/id for
96
+ irreversible ones (a database, an agent, a permanent sandbox). Without a terminal they
97
+ never prompt: pass `--yes` / `-y`, or they exit `2`.
98
+
99
+ **Secrets** go in a dotenv file or on stdin, never in argv (visible in `ps` and shell
100
+ history): `--dotenv <path>` (`KEY=VALUE` lines, `-` = stdin) on `sandboxes create|exec|resume`,
101
+ `agents create` and `machines secrets set`. `--env K=V` stays for non-secret values and wins
102
+ over the file. (It is not called `--env-file` because Node 22 intercepts that flag name.)
90
103
 
91
104
  ## SSH over 443
92
105
 
@@ -103,11 +116,21 @@ Host *.ghosty
103
116
  - `--json`: stdout is JSON only, errors included: `{"error":"…","code":3,"hint":"…"}` (`code` = exit code).
104
117
  - Exit codes: `0` ok · `1` API error · `2` usage error · `3` no session, expired or rejected.
105
118
  - Login for a person: `easybits login --json` prints `{"event":"login_url","url":…}` first (show it to them), then `{"event":"logged_in","email":…}`.
119
+ - Destructive commands need `--yes` (no TTY means no prompt: exit `2` with the exact command to rerun).
106
120
  - `sandboxes exec` without `--json` exits with the remote command's code; with `--json` it exits 0 and reports `exitCode`.
107
121
  - Skill: `npx skills add https://easybits.cloud` (includes `easybits-cli`).
108
122
 
109
123
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
110
124
 
125
+ ## Changes in 0.6.0
126
+
127
+ - **Breaking for scripts:** `db rm`, `agents destroy`, `sandboxes destroy`, `domains rm` and
128
+ `files delete` now require `--yes` when not running in a terminal (exit `2` otherwise).
129
+ - `--dotenv <path|->` for env vars and secrets; `easybits login -` / `--with-token` reads the key from stdin.
130
+ `login <api-key>` and `secrets set KEY=VALUE` still work but are no longer advertised.
131
+ - `easybits db tables <db>`: tables with row counts and columns (`--json`: `[{name, rows, columns:[{name,type,pk}]}]`).
132
+ - "Did you mean" for unknown commands and subcommands; `easybits help <command> <subcommand>`.
133
+
111
134
  ## License
112
135
 
113
136
  MIT
package/dist/index.js CHANGED
@@ -6,6 +6,9 @@ import { parseArgs } from "util";
6
6
  // src/commands/account.ts
7
7
  import { resolveBaseUrl as resolveBaseUrl3 } from "@easybits.cloud/sdk";
8
8
 
9
+ // src/args.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+
9
12
  // src/errors.ts
10
13
  import { EasybitsError } from "@easybits.cloud/sdk";
11
14
  var EXIT = { OK: 0, API: 1, USAGE: 2, AUTH: 3 };
@@ -34,24 +37,35 @@ function notLoggedIn() {
34
37
  return new CliError(
35
38
  "Not logged in.",
36
39
  EXIT.AUTH,
37
- "Run: easybits login (or: easybits login <api-key>, or set EASYBITS_API_KEY)",
40
+ "Run: easybits login (or: easybits login - < key-file, or set EASYBITS_API_KEY)",
38
41
  "not_logged_in"
39
42
  );
40
43
  }
41
44
  function apiMessage(body) {
42
45
  try {
43
46
  const j = JSON.parse(body);
47
+ if (typeof j.error === "string" && typeof j.message === "string") return { message: `${j.error}: ${j.message}`, kind: j.error };
48
+ if (typeof j.code === "string" && typeof j.error === "string") return { message: `${j.code}: ${j.error}`, kind: j.code };
44
49
  const msg = j.message ?? j.error ?? j.detail;
45
- if (typeof msg === "string") return msg;
46
- if (msg) return JSON.stringify(msg);
50
+ if (typeof msg === "string") return { message: msg };
51
+ if (msg) return { message: JSON.stringify(msg) };
47
52
  } catch {
48
53
  }
49
- return body.trim() || "empty response";
54
+ return { message: body.trim() || "empty response" };
50
55
  }
56
+ var KIND_HINTS = {
57
+ SandboxBusy: "A snapshot or fork is running on this sandbox. Wait a few minutes and retry (easybits sb get <id> shows `activity`).",
58
+ SandboxUnreachable: "The sandbox is not answering inside. Wait and retry; if it persists, destroy it.",
59
+ SandboxHostTimeout: "The operation may still be running. Check `easybits sb get <id>` and retry in a few minutes.",
60
+ SandboxHostError: "The sandbox host failed. Retry; if it persists, contact support.",
61
+ SQL_ERROR: "Fix the SQL statement.",
62
+ DATABASE_STORAGE_MISSING: "This database has no storage; its data is unavailable. Delete it (easybits db rm) and create a new one.",
63
+ DATABASE_BACKEND_ERROR: "The database backend failed. Retry in a moment."
64
+ };
51
65
  function toCliError(err) {
52
66
  if (err instanceof CliError) return err;
53
67
  if (err instanceof EasybitsError) {
54
- const message2 = apiMessage(err.body);
68
+ const { message: message2, kind } = apiMessage(err.body);
55
69
  if (err.status === 401) {
56
70
  return new CliError(
57
71
  `Credentials rejected (401): ${message2}`,
@@ -61,7 +75,7 @@ function toCliError(err) {
61
75
  401
62
76
  );
63
77
  }
64
- const hint = 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;
78
+ 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);
65
79
  return new CliError(`API error ${err.status}: ${message2}`, EXIT.API, hint, "api_error", err.status);
66
80
  }
67
81
  const message = err instanceof Error ? err.message : String(err);
@@ -106,6 +120,34 @@ async function readStdin() {
106
120
  for await (const c of process.stdin) chunks.push(c);
107
121
  return Buffer.concat(chunks);
108
122
  }
123
+ var ENV_FILE_OPTION = {
124
+ dotenv: { type: "string", value: "path", description: "KEY=VALUE lines (dotenv); - reads stdin. Use it for secrets" }
125
+ };
126
+ async function readEnvFile(path, usage2) {
127
+ let text;
128
+ if (path === "-") {
129
+ if (process.stdin.isTTY) throw usageError("--dotenv - expects KEY=VALUE lines on stdin.", usage2);
130
+ text = (await readStdin()).toString("utf8");
131
+ } else {
132
+ if (!existsSync(path)) throw usageError(`File not found: ${path}`, usage2);
133
+ text = readFileSync(path, "utf8");
134
+ }
135
+ const out = {};
136
+ for (const [n, raw] of text.split(/\r?\n/).entries()) {
137
+ const line = raw.trim();
138
+ if (!line || line.startsWith("#")) continue;
139
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line);
140
+ if (!m) throw usageError(`${path === "-" ? "stdin" : path}:${n + 1}: expected KEY=VALUE.`, usage2);
141
+ let v = m[2];
142
+ if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v.endsWith(v[0])) v = v.slice(1, -1);
143
+ out[m[1]] = v;
144
+ }
145
+ return out;
146
+ }
147
+ async function envFrom(ctx, usage2) {
148
+ const file = str(ctx, "dotenv");
149
+ return { ...file ? await readEnvFile(file, usage2) : {}, ...pairs(list(ctx, "env"), usage2) };
150
+ }
109
151
 
110
152
  // src/output.ts
111
153
  function emit(ctx, data, human) {
@@ -145,7 +187,7 @@ import { EasybitsClient, resolveBaseUrl as resolveBaseUrl2 } from "@easybits.clo
145
187
  import { createHash, randomBytes } from "crypto";
146
188
  import { createServer } from "http";
147
189
  import { spawn } from "child_process";
148
- import { chmodSync, readFileSync, writeFileSync } from "fs";
190
+ import { chmodSync, readFileSync as readFileSync2, writeFileSync } from "fs";
149
191
  import { homedir } from "os";
150
192
  import { join } from "path";
151
193
  import { resolveBaseUrl } from "@easybits.cloud/sdk";
@@ -154,7 +196,7 @@ var LOGIN_TIMEOUT_MS = 5 * 6e4;
154
196
  var RC_PATH = join(homedir(), ".easybitsrc");
155
197
  function readRc() {
156
198
  try {
157
- return JSON.parse(readFileSync(RC_PATH, "utf8"));
199
+ return JSON.parse(readFileSync2(RC_PATH, "utf8"));
158
200
  } catch {
159
201
  return {};
160
202
  }
@@ -339,20 +381,28 @@ var login = {
339
381
  name: "login",
340
382
  group: "Account",
341
383
  summary: "Sign in with your browser (or save an API key)",
342
- synopsis: "login [api-key]",
384
+ synopsis: "login [-]",
343
385
  leaf: {
344
386
  summary: "Sign in with your browser (OAuth2 + PKCE), or save an API key instead",
345
- usage: "easybits login [api-key] [--no-browser]",
387
+ usage: "easybits login [-|--with-token] [--no-browser]",
346
388
  options: {
347
- "no-browser": { type: "boolean", description: "Only print the sign-in URL (e.g. for an agent to relay it)" }
389
+ "no-browser": { type: "boolean", description: "Only print the sign-in URL (e.g. for an agent to relay it)" },
390
+ "with-token": { type: "boolean", description: "Read an API key from stdin instead of the browser (same as -)" }
348
391
  },
349
392
  examples: [
350
- "easybits login # opens the browser; waits for you",
351
- 'easybits login --json # agents: prints {"event":"login_url"} first',
352
- "easybits login eb_sk_live_xxxxxxxx # API key instead of the browser (validated first)"
393
+ "easybits login # opens the browser; waits for you",
394
+ 'easybits login --json # agents: prints {"event":"login_url"} first',
395
+ "easybits login - < key.txt # API key from stdin (validated first)",
396
+ "printenv EB_KEY | easybits login --with-token",
397
+ "EASYBITS_API_KEY=\u2026 easybits usage # CI/agents: the env var needs no login at all"
353
398
  ],
354
399
  async run(ctx) {
355
- const key = ctx.args[0];
400
+ let key = ctx.args[0];
401
+ if (key === "-" || bool(ctx, "with-token")) {
402
+ if (process.stdin.isTTY) throw usageError("Pipe the API key on stdin: easybits login - < key.txt", this.usage);
403
+ key = (await readStdin()).toString("utf8").trim();
404
+ if (!key) throw usageError("Empty API key on stdin.", this.usage);
405
+ }
356
406
  if (key) {
357
407
  const email2 = await fetchEmail(key);
358
408
  const { oauth: _drop, ...rest } = readRc();
@@ -517,8 +567,49 @@ var mcp = {
517
567
  };
518
568
 
519
569
  // src/commands/files.ts
520
- import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
570
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
521
571
  import { basename, extname } from "path";
572
+
573
+ // src/prompt.ts
574
+ import { createInterface } from "readline/promises";
575
+ var YES_OPTION = {
576
+ yes: { type: "boolean", short: "y", description: "Skip the confirmation (required without a terminal)" }
577
+ };
578
+ function sameCommand() {
579
+ const argv2 = process.argv.slice(2).map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
580
+ return `easybits ${argv2.join(" ")}`;
581
+ }
582
+ async function ask(question) {
583
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
584
+ try {
585
+ return (await rl.question(question)).trim();
586
+ } catch {
587
+ process.stderr.write("\n");
588
+ return "";
589
+ } finally {
590
+ rl.close();
591
+ }
592
+ }
593
+ function requireYesIfHeadless(ctx) {
594
+ if (bool(ctx, "yes") || interactive(ctx)) return;
595
+ throw new CliError("--yes required when not running interactively.", EXIT.USAGE, `Run: ${sameCommand()} --yes`, "usage");
596
+ }
597
+ async function confirm(ctx, action, opts = {}) {
598
+ if (bool(ctx, "yes")) return;
599
+ requireYesIfHeadless(ctx);
600
+ if (opts.typeName) {
601
+ process.stderr.write(`${action}
602
+ This cannot be undone.
603
+ `);
604
+ const answer2 = await ask(`Type "${opts.typeName}" to confirm: `);
605
+ if (answer2 !== opts.typeName) throw new CliError("Aborted: the name did not match.", EXIT.USAGE, void 0, "aborted");
606
+ return;
607
+ }
608
+ const answer = await ask(`${action} [y/N] `);
609
+ if (!/^y(es)?$/i.test(answer)) throw new CliError("Aborted.", EXIT.USAGE, void 0, "aborted");
610
+ }
611
+
612
+ // src/commands/files.ts
522
613
  var MIME = {
523
614
  pdf: "application/pdf",
524
615
  png: "image/png",
@@ -569,14 +660,14 @@ var files = {
569
660
  examples: ["easybits files upload ./report.pdf", "easybits files upload ./logo.png --json"],
570
661
  async run(ctx) {
571
662
  const path = need(ctx, 0, "path", this.usage);
572
- if (!existsSync(path)) throw usageError(`File not found: ${path}`, this.usage);
663
+ if (!existsSync2(path)) throw usageError(`File not found: ${path}`, this.usage);
573
664
  const size = statSync(path).size;
574
665
  const contentType = MIME[extname(path).slice(1).toLowerCase()] || "application/octet-stream";
575
666
  const eb = await getClient(ctx);
576
667
  const data = await eb.uploadFile({ fileName: basename(path), contentType, size });
577
668
  const res = await fetch(data.putUrl, {
578
669
  method: "PUT",
579
- body: readFileSync2(path),
670
+ body: readFileSync3(path),
580
671
  headers: { "Content-Type": contentType }
581
672
  });
582
673
  if (!res.ok) throw new CliError(`Upload failed: storage answered ${res.status}`, 1, void 0, "upload_failed", res.status);
@@ -586,10 +677,13 @@ var files = {
586
677
  delete: {
587
678
  aliases: ["rm"],
588
679
  summary: "Delete a file (soft delete, 7-day trash)",
589
- usage: "easybits files delete <file-id>",
590
- examples: ["easybits files delete 6650f0c2a1b2c3d4e5f60718"],
680
+ usage: "easybits files delete <file-id> [--yes]",
681
+ options: { ...YES_OPTION },
682
+ examples: ["easybits files delete 6650f0c2a1b2c3d4e5f60718", "easybits files rm 6650f0c2a1b2c3d4e5f60718 --yes"],
591
683
  async run(ctx) {
592
684
  const id = need(ctx, 0, "file-id", this.usage);
685
+ requireYesIfHeadless(ctx);
686
+ await confirm(ctx, `Delete file ${id}? (recoverable from trash for 7 days)`);
593
687
  const eb = await getClient(ctx);
594
688
  const r = await eb.deleteFile(id);
595
689
  emit(ctx, r, () => console.log("Deleted"));
@@ -599,7 +693,7 @@ var files = {
599
693
  };
600
694
 
601
695
  // src/commands/init.ts
602
- import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
696
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
603
697
  var init = {
604
698
  name: "init",
605
699
  group: "Hosting",
@@ -616,7 +710,7 @@ var init = {
616
710
  async run(ctx) {
617
711
  const appDir = str(ctx, "app-dir") ?? "/srv/app";
618
712
  const port = str(ctx, "port") ?? "3000";
619
- if (!existsSync2("package.json")) {
713
+ if (!existsSync3("package.json")) {
620
714
  throw usageError("No package.json here. Run this at the root of your app's repo.", this.usage);
621
715
  }
622
716
  const workflow = `# Despliegue a EasyBits en cada push a main.
@@ -793,7 +887,7 @@ From then on, every push to main deploys.`));
793
887
  // src/commands/ssh.ts
794
888
  import { homedir as homedir2 } from "os";
795
889
  import { join as join2 } from "path";
796
- import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
890
+ import { existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
797
891
  import { execFileSync } from "child_process";
798
892
  var SSH_CONFIG_SNIPPET = `Host *.ghosty
799
893
  ProxyCommand easybits ssh-proxy %h
@@ -811,14 +905,14 @@ var sshKey = {
811
905
  const dir = join2(homedir2(), ".ssh");
812
906
  const key = join2(dir, "easybits_ed25519");
813
907
  const pub = `${key}.pub`;
814
- if (!existsSync3(pub)) {
908
+ if (!existsSync4(pub)) {
815
909
  mkdirSync2(dir, { recursive: true, mode: 448 });
816
910
  execFileSync("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", key, "-C", "easybits"], {
817
911
  stdio: ["ignore", "ignore", "inherit"]
818
912
  });
819
913
  console.error(`easybits: key created at ${key}`);
820
914
  }
821
- const publicKey = readFileSync3(pub, "utf8").trim();
915
+ const publicKey = readFileSync4(pub, "utf8").trim();
822
916
  if (ctx.json) process.stdout.write(JSON.stringify({ publicKey, path: pub }, null, 2) + "\n");
823
917
  else process.stdout.write(publicKey + "\n");
824
918
  }
@@ -870,7 +964,7 @@ var sshProxy = {
870
964
  };
871
965
 
872
966
  // src/commands/sandboxes.ts
873
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
967
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
874
968
  function sandboxRecord(s) {
875
969
  return {
876
970
  sandboxId: s.sandboxId,
@@ -883,7 +977,8 @@ function sandboxRecord(s) {
883
977
  tier: s.tier,
884
978
  cpuMode: s.cpuMode,
885
979
  monthlyMxn: s.monthlyMxn,
886
- metadata: s.metadata
980
+ metadata: s.metadata,
981
+ activity: s.activity
887
982
  };
888
983
  }
889
984
  function printSandbox(s) {
@@ -891,6 +986,7 @@ function printSandbox(s) {
891
986
  if (s.name) console.log(`Name: ${s.name}`);
892
987
  console.log(`Template: ${s.template}`);
893
988
  console.log(`Status: ${s.status}`);
989
+ if (s.activity) console.log(`Activity: ${s.activity} (busy: exec/suspend/destroy answer 409 SandboxBusy until it ends)`);
894
990
  console.log(`Created: ${fmtDate(s.createdAt)}`);
895
991
  console.log(`Expires: ${s.persistent ? "never (permanent)" : fmtDate(s.expiresAt)}`);
896
992
  if (s.tier) console.log(`Tier: ${s.tier}${s.monthlyMxn != null ? ` ($${s.monthlyMxn} MXN/month)` : ""}`);
@@ -930,23 +1026,25 @@ var sandboxes = {
930
1026
  create: {
931
1027
  aliases: ["new"],
932
1028
  summary: "Create a sandbox and wait until it is running",
933
- usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--timeout <s>] [--size s|m|l|xl] [--env K=V]...",
1029
+ usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--timeout <s>] [--size s|m|l|xl] [--dotenv <path>] [--env K=V]...",
934
1030
  options: {
935
1031
  template: { type: "string", value: "name", description: "Base template (default ubuntu): ubuntu, python, node, code-interpreter\u2026" },
936
1032
  name: { type: "string", value: "name", description: "Human label (lets you `ssh <name>.ghosty`)" },
937
1033
  timeout: { type: "string", value: "seconds", description: "Time to live before auto-destroy" },
938
1034
  size: { type: "string", value: "s|m|l|xl", description: "VM size class (gated by plan)" },
939
- env: { type: "string", multiple: true, value: "K=V", description: "Environment variable (repeatable)" },
1035
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
1036
+ ...ENV_FILE_OPTION,
940
1037
  "no-wait": { type: "boolean", description: "Return right away instead of waiting for running" }
941
1038
  },
942
1039
  examples: [
943
1040
  "easybits sandboxes create --template node --name scratch",
944
- "ID=$(easybits sb create --template python --json | jq -r .sandboxId)"
1041
+ "ID=$(easybits sb create --template python --json | jq -r .sandboxId)",
1042
+ "easybits sb create --template node --dotenv .env"
945
1043
  ],
946
1044
  async run(ctx) {
947
1045
  const size = str(ctx, "size");
948
1046
  if (size && !["s", "m", "l", "xl"].includes(size)) throw usageError("--size must be s, m, l or xl.", this.usage);
949
- const env = pairs(list(ctx, "env"), this.usage);
1047
+ const env = await envFrom(ctx, this.usage);
950
1048
  const eb = await getClient(ctx);
951
1049
  const sb = await eb.sandboxes.create({
952
1050
  template: str(ctx, "template") ?? "ubuntu",
@@ -972,14 +1070,16 @@ var sandboxes = {
972
1070
  },
973
1071
  exec: {
974
1072
  summary: "Run a shell command inside a sandbox",
975
- usage: "easybits sandboxes exec <sandbox-id> [--cwd <dir>] [--timeout <s>] -- <command...>",
1073
+ usage: "easybits sandboxes exec <sandbox-id> [--cwd <dir>] [--timeout <s>] [--dotenv <path>] -- <command...>",
976
1074
  options: {
977
1075
  cwd: { type: "string", value: "dir", description: "Working directory" },
978
1076
  timeout: { type: "string", value: "seconds", description: "Kill the command after this long" },
979
- env: { type: "string", multiple: true, value: "K=V", description: "Environment variable (repeatable)" }
1077
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
1078
+ ...ENV_FILE_OPTION
980
1079
  },
981
1080
  examples: [
982
1081
  "easybits sandboxes exec sb_abc123 -- uname -a",
1082
+ "easybits sb exec sb_abc123 --dotenv .env -- node migrate.js",
983
1083
  "easybits sb exec sb_abc123 --cwd /data/work -- npm test",
984
1084
  "easybits sb exec sb_abc123 --json -- 'ls -la /' | jq .exitCode"
985
1085
  ],
@@ -987,7 +1087,7 @@ var sandboxes = {
987
1087
  need(ctx, 0, "sandbox-id", this.usage);
988
1088
  const command = ctx.args.slice(1).join(" ");
989
1089
  if (!command) throw usageError("Missing <command>.", this.usage);
990
- const env = pairs(list(ctx, "env"), this.usage);
1090
+ const env = await envFrom(ctx, this.usage);
991
1091
  const sb = await sandbox(ctx, this.usage);
992
1092
  const r = await sb.exec(command, {
993
1093
  cwd: str(ctx, "cwd"),
@@ -1073,8 +1173,8 @@ var sandboxes = {
1073
1173
  let buf;
1074
1174
  if (inline != null) buf = Buffer.from(inline, "utf8");
1075
1175
  else if (local) {
1076
- if (!existsSync4(local)) throw usageError(`File not found: ${local}`, FILES_USAGE);
1077
- buf = readFileSync4(local);
1176
+ if (!existsSync5(local)) throw usageError(`File not found: ${local}`, FILES_USAGE);
1177
+ buf = readFileSync5(local);
1078
1178
  } else if (!process.stdin.isTTY) buf = await readStdin();
1079
1179
  else throw usageError("Give a local file, --content, or pipe data on stdin.", FILES_USAGE);
1080
1180
  const r = await sb.files.write(path, buf.toString("base64"), { encoding: "base64" });
@@ -1095,11 +1195,14 @@ var sandboxes = {
1095
1195
  },
1096
1196
  resume: {
1097
1197
  summary: "Wake a suspended sandbox",
1098
- usage: "easybits sandboxes resume <sandbox-id> [--env K=V]...",
1099
- options: { env: { type: "string", multiple: true, value: "K=V", description: "Rewrite env on wake (repeatable)" } },
1198
+ usage: "easybits sandboxes resume <sandbox-id> [--dotenv <path>] [--env K=V]...",
1199
+ options: {
1200
+ env: { type: "string", multiple: true, value: "K=V", description: "Rewrite env on wake, non-secret (repeatable)" },
1201
+ ...ENV_FILE_OPTION
1202
+ },
1100
1203
  examples: ["easybits sandboxes resume sb_abc123"],
1101
1204
  async run(ctx) {
1102
- const env = pairs(list(ctx, "env"), this.usage);
1205
+ const env = await envFrom(ctx, this.usage);
1103
1206
  const r = await (await sandbox(ctx, this.usage)).resume(Object.keys(env).length ? { env } : {});
1104
1207
  emit(ctx, r, () => console.log(`Resumed ${r.sandboxId ?? ctx.args[0]} (${r.status ?? "running"})`));
1105
1208
  }
@@ -1107,10 +1210,19 @@ var sandboxes = {
1107
1210
  destroy: {
1108
1211
  aliases: ["rm", "delete"],
1109
1212
  summary: "Destroy a sandbox (irreversible)",
1110
- usage: "easybits sandboxes destroy <sandbox-id>",
1111
- examples: ["easybits sandboxes destroy sb_abc123"],
1213
+ usage: "easybits sandboxes destroy <sandbox-id> [--yes]",
1214
+ options: { ...YES_OPTION },
1215
+ examples: ["easybits sandboxes destroy sb_abc123", "easybits sb rm sb_abc123 --yes"],
1112
1216
  async run(ctx) {
1113
- const r = await (await sandbox(ctx, this.usage)).destroy();
1217
+ need(ctx, 0, "sandbox-id", this.usage);
1218
+ requireYesIfHeadless(ctx);
1219
+ const sb = await sandbox(ctx, this.usage);
1220
+ await confirm(
1221
+ ctx,
1222
+ sb.persistent ? `Destroy permanent sandbox ${sb.sandboxId}${sb.name ? ` (${sb.name})` : ""} and its disk?` : `Destroy sandbox ${sb.sandboxId}?`,
1223
+ sb.persistent ? { typeName: sb.sandboxId } : {}
1224
+ );
1225
+ const r = await sb.destroy();
1114
1226
  emit(ctx, { ...r, sandboxId: ctx.args[0] }, () => console.log(`Destroyed ${ctx.args[0]}`));
1115
1227
  }
1116
1228
  },
@@ -1128,7 +1240,7 @@ var sandboxes = {
1128
1240
  };
1129
1241
 
1130
1242
  // src/commands/machines.ts
1131
- var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine-id> [KEY=VALUE... | NAME]";
1243
+ var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine-id> [--dotenv <path> | NAME]";
1132
1244
  var machines = {
1133
1245
  name: "machines",
1134
1246
  aliases: ["deploy", "machine"],
@@ -1219,9 +1331,11 @@ var machines = {
1219
1331
  secrets: {
1220
1332
  summary: "App env vars stored encrypted in your vault",
1221
1333
  usage: SECRETS_USAGE,
1334
+ options: { ...ENV_FILE_OPTION },
1222
1335
  examples: [
1223
1336
  "easybits machines secrets ls sb_abc123",
1224
- "easybits machines secrets set sb_abc123 DATABASE_URL=postgres://\u2026 API_KEY=xyz",
1337
+ "easybits machines secrets set sb_abc123 --dotenv .env.production",
1338
+ `printf 'API_KEY=%s\\n' "$API_KEY" | easybits machines secrets set sb_abc123 --dotenv -`,
1225
1339
  "easybits machines secrets unset sb_abc123 API_KEY"
1226
1340
  ],
1227
1341
  async run(ctx) {
@@ -1235,8 +1349,9 @@ var machines = {
1235
1349
  console.log(`In vault: ${r.inVault.length ? r.inVault.join(", ") : "(none)"}`);
1236
1350
  });
1237
1351
  } else if (action === "set") {
1238
- const values = pairs(ctx.args.slice(2), SECRETS_USAGE);
1239
- if (!Object.keys(values).length) throw usageError("Give at least one KEY=VALUE.", SECRETS_USAGE);
1352
+ const file = str(ctx, "dotenv");
1353
+ const values = { ...file ? await readEnvFile(file, SECRETS_USAGE) : {}, ...pairs(ctx.args.slice(2), SECRETS_USAGE) };
1354
+ if (!Object.keys(values).length) throw usageError("Give --dotenv <path> (or - for stdin).", SECRETS_USAGE);
1240
1355
  const r = await eb.machines.setSecrets(id, values);
1241
1356
  emit(ctx, r, () => console.log(`Set ${Object.keys(values).join(", ")}. Injected now: ${r.secretNames.join(", ")}`));
1242
1357
  } else if (action === "unset") {
@@ -1308,10 +1423,14 @@ var domains = {
1308
1423
  rm: {
1309
1424
  aliases: ["remove", "delete"],
1310
1425
  summary: "Detach a domain",
1311
- usage: "easybits domains rm <sandbox-id> <domain>",
1312
- examples: ["easybits domains rm sb_abc123 shop.example.com"],
1426
+ usage: "easybits domains rm <sandbox-id> <domain> [--yes]",
1427
+ options: { ...YES_OPTION },
1428
+ examples: ["easybits domains rm sb_abc123 shop.example.com", "easybits domains rm sb_abc123 shop.example.com --yes"],
1313
1429
  async run(ctx) {
1430
+ need(ctx, 0, "sandbox-id", this.usage);
1314
1431
  const domain = need(ctx, 1, "domain", this.usage);
1432
+ requireYesIfHeadless(ctx);
1433
+ await confirm(ctx, `Detach ${domain} from ${ctx.args[0]}? It stops serving right away.`);
1315
1434
  const r = await (await sandbox2(ctx, this.usage)).removeDomain(domain);
1316
1435
  emit(ctx, r, () => console.log(`Removed ${domain}`));
1317
1436
  }
@@ -1367,16 +1486,55 @@ var db = {
1367
1486
  rm: {
1368
1487
  aliases: ["delete", "drop"],
1369
1488
  summary: "Delete a database (irreversible)",
1370
- usage: "easybits db rm <db-id|name>",
1371
- examples: ["easybits db rm leads"],
1489
+ usage: "easybits db rm <db-id|name> [--yes]",
1490
+ options: { ...YES_OPTION },
1491
+ examples: ["easybits db rm leads # asks you to type the name", "easybits db rm leads --yes # scripts and agents"],
1372
1492
  async run(ctx) {
1373
1493
  const ref = need(ctx, 0, "db-id|name", this.usage);
1494
+ requireYesIfHeadless(ctx);
1374
1495
  const eb = await getClient(ctx);
1375
1496
  const hit = await findDb(eb, ref);
1497
+ await confirm(ctx, `Delete database ${hit.name} (${hit.id}) and all its data?`, { typeName: hit.name });
1376
1498
  const r = await eb.deleteDatabase(hit.id);
1377
1499
  emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(`Deleted ${hit.name} (${hit.id})`));
1378
1500
  }
1379
1501
  },
1502
+ tables: {
1503
+ aliases: ["inspect", "schema"],
1504
+ summary: "List tables with row counts and columns",
1505
+ usage: "easybits db tables <db-id|name>",
1506
+ examples: ["easybits db tables leads", "easybits db tables leads --json"],
1507
+ // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
1508
+ async run(ctx) {
1509
+ const ref = need(ctx, 0, "db-id|name", this.usage);
1510
+ const eb = await getClient(ctx);
1511
+ const hit = await findDb(eb, ref);
1512
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
1513
+ const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name")).rows.map((r) => String(r[0]));
1514
+ const ident = (n) => `"${n.replace(/"/g, '""')}"`;
1515
+ const tables = [];
1516
+ for (const name of names) {
1517
+ const info2 = await q(`PRAGMA table_info(${ident(name)})`);
1518
+ const col = (k) => info2.cols.indexOf(k);
1519
+ const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
1520
+ const count = await q(`SELECT count(*) FROM ${ident(name)}`);
1521
+ tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns });
1522
+ }
1523
+ emit(
1524
+ ctx,
1525
+ tables,
1526
+ () => table(
1527
+ tables.map((t) => ({
1528
+ name: t.name,
1529
+ rows: t.rows,
1530
+ columns: t.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
1531
+ })),
1532
+ [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
1533
+ `No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`
1534
+ )
1535
+ );
1536
+ }
1537
+ },
1380
1538
  query: {
1381
1539
  aliases: ["sql"],
1382
1540
  summary: "Run one SQL statement (by database id or name)",
@@ -1439,25 +1597,28 @@ var agents = {
1439
1597
  },
1440
1598
  create: {
1441
1599
  summary: "Create an agent from a template",
1442
- usage: "easybits agents create --template <template> [--name <name>] [--env K=V]... [--timeout <s>]",
1600
+ usage: "easybits agents create --template <template> [--name <name>] [--dotenv <path>] [--env K=V]... [--timeout <s>]",
1443
1601
  options: {
1444
1602
  template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
1445
1603
  name: { type: "string", value: "name", description: "Label" },
1446
- env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent (repeatable)" },
1604
+ env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
1605
+ ...ENV_FILE_OPTION,
1447
1606
  timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" }
1448
1607
  },
1449
1608
  examples: [
1450
1609
  "easybits agents create --template goose --name helper",
1451
- "easybits agents create --template chat-anthropic --env ANTHROPIC_API_KEY=sk-ant-\u2026 --json"
1610
+ "easybits agents create --template chat-anthropic --dotenv .env --json",
1611
+ "op read op://vault/anthropic/env | easybits agents create --template chat-anthropic --dotenv -"
1452
1612
  ],
1453
1613
  async run(ctx) {
1454
1614
  const template = str(ctx, "template");
1455
1615
  if (!template) throw usageError("Missing --template.", this.usage);
1616
+ const env = await envFrom(ctx, this.usage);
1456
1617
  const eb = await getClient(ctx);
1457
1618
  const a = await eb.createAgent({
1458
1619
  template,
1459
1620
  name: str(ctx, "name"),
1460
- env: pairs(list(ctx, "env"), this.usage),
1621
+ env,
1461
1622
  timeoutSeconds: int(ctx, "timeout", this.usage)
1462
1623
  });
1463
1624
  emit(ctx, a, () => {
@@ -1492,10 +1653,13 @@ var agents = {
1492
1653
  destroy: {
1493
1654
  aliases: ["rm", "delete"],
1494
1655
  summary: "Destroy an agent and its sandbox",
1495
- usage: "easybits agents destroy <agent-id>",
1496
- examples: ["easybits agents destroy ag_123"],
1656
+ usage: "easybits agents destroy <agent-id> [--yes]",
1657
+ options: { ...YES_OPTION },
1658
+ examples: ["easybits agents destroy ag_123 # asks you to type the id", "easybits agents destroy ag_123 --yes"],
1497
1659
  async run(ctx) {
1498
1660
  const id = need(ctx, 0, "agent-id", this.usage);
1661
+ requireYesIfHeadless(ctx);
1662
+ await confirm(ctx, `Destroy agent ${id} and its sandbox (including /data)?`, { typeName: id });
1499
1663
  const eb = await getClient(ctx);
1500
1664
  const r = await eb.destroyAgent(id);
1501
1665
  emit(ctx, { ...r, agentId: id }, () => console.log(`Destroyed ${id}`));
@@ -1574,10 +1738,36 @@ function findCommand(name) {
1574
1738
  return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
1575
1739
  }
1576
1740
 
1741
+ // src/suggest.ts
1742
+ function levenshtein(a, b) {
1743
+ if (a === b) return 0;
1744
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
1745
+ for (let i = 1; i <= a.length; i++) {
1746
+ const cur = [i];
1747
+ for (let j = 1; j <= b.length; j++) {
1748
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1749
+ }
1750
+ prev = cur;
1751
+ }
1752
+ return prev[b.length];
1753
+ }
1754
+ function suggest(input, candidates, max = 2) {
1755
+ const needle = input.toLowerCase();
1756
+ const scored = [...new Set(candidates)].map((c) => ({ c, d: levenshtein(needle, c.toLowerCase()) })).filter(({ c, d }) => d <= max || needle.length >= 2 && c.toLowerCase().startsWith(needle)).sort((x, y) => x.d - y.d || x.c.localeCompare(y.c));
1757
+ return scored.map(({ c }) => c);
1758
+ }
1759
+ function didYouMean(input, candidates, prefix, canonical = (n) => n) {
1760
+ const hits = [...new Set(suggest(input, candidates).map(canonical))].slice(0, 3);
1761
+ if (!hits.length) return void 0;
1762
+ return `Did you mean this?
1763
+ ${hits.map((h) => ` ${prefix}${h}`).join("\n")}`;
1764
+ }
1765
+
1577
1766
  // src/help.ts
1578
1767
  var GLOBAL_FLAGS = `Global flags:
1579
1768
  --json Machine output: JSON on stdout, errors included
1580
- --token <key> API key for this call (EASYBITS_API_KEY wins if set)
1769
+ --token <key> API key for this call (prefer EASYBITS_API_KEY: argv leaks to ps)
1770
+ -y, --yes On rm/destroy: skip the confirmation (required without a TTY)
1581
1771
  -h, --help Help (also: easybits <command> --help)
1582
1772
  -v, --version Print the CLI version
1583
1773
 
@@ -1636,7 +1826,7 @@ var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;
1636
1826
 
1637
1827
  // src/index.ts
1638
1828
  import { EasybitsError as EasybitsError2 } from "@easybits.cloud/sdk";
1639
- var VERSION = true ? "0.5.0" : "dev";
1829
+ var VERSION = true ? "0.6.0" : "dev";
1640
1830
  var GLOBAL_OPTIONS = {
1641
1831
  json: { type: "boolean" },
1642
1832
  token: { type: "string" },
@@ -1679,16 +1869,44 @@ function parse(argv2, skip, leaf) {
1679
1869
  throw usageError(e.message.split("\n")[0].split(". To specify")[0].replace(/\.?$/, "."), leaf?.usage);
1680
1870
  }
1681
1871
  }
1872
+ function findSub(cmd, name) {
1873
+ return Object.entries(cmd.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
1874
+ }
1875
+ function unknownCommand(name) {
1876
+ const e = usageError(`Unknown command "${name}".`);
1877
+ const names = COMMANDS.flatMap((c) => [c.name, ...c.aliases ?? []]);
1878
+ const hint = didYouMean(name, names, "easybits ", (n) => findCommand(n)?.name ?? n);
1879
+ if (hint) e.hint = `${hint}
1880
+ Run: easybits --help`;
1881
+ return e;
1882
+ }
1883
+ function unknownSub(cmd, name) {
1884
+ const e = usageError(`Unknown subcommand "${cmd.name} ${name}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs ?? {}).join("|")}>`);
1885
+ const names = Object.entries(cmd.subs ?? {}).flatMap(([n, l]) => [n, ...l.aliases ?? []]);
1886
+ const hint = didYouMean(name, names, `easybits ${cmd.name} `, (n) => findSub(cmd, n)?.[0] ?? n);
1887
+ if (hint) e.hint = `${hint}
1888
+ ${e.hint}`;
1889
+ return e;
1890
+ }
1682
1891
  async function main(argv2) {
1683
1892
  const pos = positionalIndexes(argv2);
1684
1893
  const cmdName = pos.length ? argv2[pos[0]] : void 0;
1685
1894
  if (!cmdName || cmdName === "help") {
1686
- const topic = cmdName === "help" && pos[1] != null ? findCommand(argv2[pos[1]]) : void 0;
1687
- const { values: values2 } = parse(argv2, pos.slice(0, 2), void 0);
1895
+ const topicName = cmdName === "help" && pos[1] != null ? argv2[pos[1]] : void 0;
1896
+ const topic = topicName ? findCommand(topicName) : void 0;
1897
+ if (topicName && !topic) throw unknownCommand(topicName);
1898
+ const subName = topic?.subs && pos[2] != null ? argv2[pos[2]] : void 0;
1899
+ const sub = topic && subName ? findSub(topic, subName) : void 0;
1900
+ if (topic && subName && !sub) throw unknownSub(topic, subName);
1901
+ const { values: values2 } = parse(argv2, pos.slice(0, sub ? 3 : 2), void 0);
1688
1902
  if (values2.version) {
1689
1903
  console.log(VERSION);
1690
1904
  return;
1691
1905
  }
1906
+ if (sub) {
1907
+ console.log(leafHelp(sub[1]));
1908
+ return;
1909
+ }
1692
1910
  if (topic) {
1693
1911
  console.log(commandHelp(topic));
1694
1912
  return;
@@ -1698,17 +1916,17 @@ async function main(argv2) {
1698
1916
  return;
1699
1917
  }
1700
1918
  const cmd = findCommand(cmdName);
1701
- if (!cmd) throw usageError(`Unknown command "${cmdName}".`);
1919
+ if (!cmd) throw unknownCommand(cmdName);
1702
1920
  let leaf = cmd.leaf;
1703
1921
  const skip = [pos[0]];
1704
1922
  if (cmd.subs) {
1705
1923
  const subName = pos[1] != null ? argv2[pos[1]] : void 0;
1706
- const entry = subName ? Object.entries(cmd.subs).find(([n, l]) => n === subName || l.aliases?.includes(subName)) : void 0;
1924
+ const entry = subName ? findSub(cmd, subName) : void 0;
1707
1925
  if (entry) {
1708
1926
  leaf = entry[1];
1709
1927
  skip.push(pos[1]);
1710
1928
  } else if (subName) {
1711
- throw usageError(`Unknown subcommand "${cmd.name} ${subName}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs).join("|")}>`);
1929
+ throw unknownSub(cmd, subName);
1712
1930
  } else if (cmd.defaultSub && !argv2.includes("--help") && !argv2.includes("-h")) {
1713
1931
  leaf = cmd.subs[cmd.defaultSub];
1714
1932
  } else if (!argv2.includes("--help") && !argv2.includes("-h")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,13 +29,14 @@
29
29
  "build": "tsup",
30
30
  "dev": "tsup --watch",
31
31
  "prepublishOnly": "npm run build",
32
- "typecheck": "tsc --noEmit"
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "node --experimental-strip-types --no-warnings --test src/*.test.ts"
33
34
  },
34
35
  "publishConfig": {
35
36
  "access": "public"
36
37
  },
37
38
  "dependencies": {
38
- "@easybits.cloud/sdk": "^0.35.2"
39
+ "@easybits.cloud/sdk": "^0.36.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^22.0.0",