@easybits.cloud/cli 0.5.1 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +49 -13
  2. package/dist/index.js +268 -59
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -17,7 +17,9 @@ 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 --no-browser # only print the sign-in URL
21
+ easybits login - < key.txt # or save an API key, read from stdin (never in argv)
22
+ printenv EB_KEY | easybits login --with-token # same as -
21
23
  easybits usage
22
24
  easybits logout
23
25
  ```
@@ -25,32 +27,35 @@ easybits logout
25
27
  Running a command without a session in a terminal starts the login automatically;
26
28
  without a terminal (agents, CI) it exits `3`. Precedence: `EASYBITS_API_KEY` env var >
27
29
  `--token <key>` > browser session > saved API key. Keys: https://www.easybits.cloud/dash/developer.
30
+ In CI and agents, `EASYBITS_API_KEY` needs no login at all. `EASYBITS_URL` points the CLI at
31
+ another server (default `https://www.easybits.cloud`; `baseUrl` in `~/.easybitsrc` works too).
28
32
 
29
33
  ## Commands
30
34
 
31
35
  ```bash
32
36
  # Sandboxes (alias: sb)
33
37
  easybits sandboxes ls
34
- easybits sandboxes create --template node --name scratch
38
+ easybits sandboxes create --template node --name scratch --dotenv .env
39
+ easybits sandboxes create --size m --timeout 1800 --no-wait # size s|m|l|xl (plan-gated)
35
40
  easybits sandboxes get sb_abc123
36
41
  easybits sandboxes exec sb_abc123 -- npm test
37
- easybits sandboxes logs sb_abc123 --unit myapp --lines 100
42
+ easybits sandboxes logs sb_abc123 --unit myapp --lines 100 --since "10 min ago" --grep ERROR
38
43
  easybits sandboxes files ls sb_abc123 /data/work
39
44
  easybits sandboxes files read sb_abc123 /etc/os-release
40
45
  easybits sandboxes files write sb_abc123 /data/work/app.js ./app.js
41
46
  easybits sandboxes suspend sb_abc123
42
47
  easybits sandboxes resume sb_abc123
43
48
  easybits sandboxes snapshot sb_abc123 --name before-upgrade
44
- easybits sandboxes destroy sb_abc123
49
+ easybits sandboxes destroy sb_abc123 --yes
45
50
 
46
51
  # Hosting: permanent machines (alias: deploy)
47
52
  easybits machines ls
48
53
  easybits machines deploy sb_abc123 -m "v1.2"
49
- easybits machines releases sb_abc123
50
- easybits machines logs sb_abc123 --grep ERROR
54
+ easybits machines releases sb_abc123 --limit 5
55
+ easybits machines logs sb_abc123 --lines 100 --grep ERROR
51
56
  easybits machines rollback sb_abc123 rel_789
52
57
  easybits machines secrets ls sb_abc123
53
- easybits machines secrets set sb_abc123 DATABASE_URL=postgres://...
58
+ easybits machines secrets set sb_abc123 --dotenv .env.production # - reads stdin
54
59
  easybits machines secrets unset sb_abc123 DATABASE_URL
55
60
  easybits init --port 3000 # GitHub Actions workflow that deploys on every push
56
61
 
@@ -58,25 +63,28 @@ easybits init --port 3000 # GitHub Actions workflow that deploys on e
58
63
  easybits domains ls sb_abc123
59
64
  easybits domains add sb_abc123 shop.example.com --port 3000
60
65
  easybits domains verify sb_abc123 shop.example.com
61
- easybits domains rm sb_abc123 shop.example.com
66
+ easybits domains rm sb_abc123 shop.example.com --yes
62
67
 
63
68
  # Databases
64
69
  easybits db ls
65
70
  easybits db create leads
71
+ easybits db tables leads # tables, row counts, columns
66
72
  easybits db query leads "SELECT * FROM leads" --json
67
- easybits db rm leads
73
+ easybits db rm leads --yes
68
74
 
69
75
  # Agents
70
76
  easybits agents ls
71
- easybits agents create --template goose --name helper
77
+ easybits agents create --template goose --name helper --dotenv .env
72
78
  easybits agents message ag_123 "hello"
73
- easybits agents destroy ag_123
79
+ easybits agents message ag_123 "and now?" --session ses_456 # continue a conversation
80
+ easybits agents destroy ag_123 --yes
74
81
 
75
82
  # Files, websites, account
76
83
  easybits files ls
77
84
  easybits files upload ./report.pdf
78
- easybits files delete FILE_ID
85
+ easybits files delete FILE_ID --yes
79
86
  easybits websites ls
87
+ easybits providers # storage provider (Tigris by default)
80
88
  easybits usage
81
89
 
82
90
  # MCP, SSH, docs
@@ -84,9 +92,21 @@ easybits config # MCP config (streamable HTTP)
84
92
  easybits mcp # MCP config (stdio)
85
93
  easybits ssh-key # public key for sandbox SSH
86
94
  easybits docs cli # a docs section as markdown (--en for English)
95
+ easybits docs --open # open the docs in your browser
87
96
  ```
88
97
 
89
- Every command has help with examples: `easybits <command> <subcommand> --help`.
98
+ Every command has help with examples: `easybits <command> <subcommand> --help`
99
+ (or `easybits help <command> <subcommand>`). A typo gets a "Did you mean" suggestion.
100
+
101
+ **Destructive commands** (`db rm`, `agents destroy`, `sandboxes destroy`, `domains rm`,
102
+ `files delete`) ask before acting in a terminal: `[y/N]`, or typing the name/id for
103
+ irreversible ones (a database, an agent, a permanent sandbox). Without a terminal they
104
+ never prompt — and neither do they with `--json`: pass `--yes` / `-y`, or they exit `2`.
105
+
106
+ **Secrets** go in a dotenv file or on stdin, never in argv (visible in `ps` and shell
107
+ history): `--dotenv <path>` (`KEY=VALUE` lines, `-` = stdin) on `sandboxes create|exec|resume`,
108
+ `agents create` and `machines secrets set`. `--env K=V` stays for non-secret values and wins
109
+ over the file. (It is not called `--env-file` because Node 22 intercepts that flag name.)
90
110
 
91
111
  ## SSH over 443
92
112
 
@@ -103,11 +123,27 @@ Host *.ghosty
103
123
  - `--json`: stdout is JSON only, errors included: `{"error":"…","code":3,"hint":"…"}` (`code` = exit code).
104
124
  - Exit codes: `0` ok · `1` API error · `2` usage error · `3` no session, expired or rejected.
105
125
  - Login for a person: `easybits login --json` prints `{"event":"login_url","url":…}` first (show it to them), then `{"event":"logged_in","email":…}`.
126
+ - Destructive commands need `--yes`, also with `--json` (no TTY or `--json` means no prompt: exit `2` with the exact command to rerun).
106
127
  - `sandboxes exec` without `--json` exits with the remote command's code; with `--json` it exits 0 and reports `exitCode`.
107
128
  - Skill: `npx skills add https://easybits.cloud` (includes `easybits-cli`).
108
129
 
109
130
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
110
131
 
132
+ ## Changes in 0.6.1
133
+
134
+ - Help: `--yes` is no longer listed as a global flag; it lives on each destructive command
135
+ (and the global help says it is required without a terminal or with `--json`).
136
+ - `easybits init` suggests `machines secrets set <id> --dotenv …` instead of `KEY=VALUE` in argv.
137
+
138
+ ## Changes in 0.6.0
139
+
140
+ - **Breaking for scripts:** `db rm`, `agents destroy`, `sandboxes destroy`, `domains rm` and
141
+ `files delete` now require `--yes` when not running in a terminal (exit `2` otherwise).
142
+ - `--dotenv <path|->` for env vars and secrets; `easybits login -` / `--with-token` reads the key from stdin.
143
+ `login <api-key>` and `secrets set KEY=VALUE` still work but are no longer advertised.
144
+ - `easybits db tables <db>`: tables with row counts and columns (`--json`: `[{name, rows, columns:[{name,type,pk}]}]`).
145
+ - "Did you mean" for unknown commands and subcommands; `easybits help <command> <subcommand>`.
146
+
111
147
  ## License
112
148
 
113
149
  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,7 +37,7 @@ 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
  }
@@ -56,7 +59,7 @@ var KIND_HINTS = {
56
59
  SandboxHostTimeout: "The operation may still be running. Check `easybits sb get <id>` and retry in a few minutes.",
57
60
  SandboxHostError: "The sandbox host failed. Retry; if it persists, contact support.",
58
61
  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.",
62
+ DATABASE_STORAGE_MISSING: "This database has no storage; its data is unavailable. Delete it (easybits db rm <db> --yes) and create a new one.",
60
63
  DATABASE_BACKEND_ERROR: "The database backend failed. Retry in a moment."
61
64
  };
62
65
  function toCliError(err) {
@@ -117,6 +120,34 @@ async function readStdin() {
117
120
  for await (const c of process.stdin) chunks.push(c);
118
121
  return Buffer.concat(chunks);
119
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
+ }
120
151
 
121
152
  // src/output.ts
122
153
  function emit(ctx, data, human) {
@@ -156,7 +187,7 @@ import { EasybitsClient, resolveBaseUrl as resolveBaseUrl2 } from "@easybits.clo
156
187
  import { createHash, randomBytes } from "crypto";
157
188
  import { createServer } from "http";
158
189
  import { spawn } from "child_process";
159
- import { chmodSync, readFileSync, writeFileSync } from "fs";
190
+ import { chmodSync, readFileSync as readFileSync2, writeFileSync } from "fs";
160
191
  import { homedir } from "os";
161
192
  import { join } from "path";
162
193
  import { resolveBaseUrl } from "@easybits.cloud/sdk";
@@ -165,7 +196,7 @@ var LOGIN_TIMEOUT_MS = 5 * 6e4;
165
196
  var RC_PATH = join(homedir(), ".easybitsrc");
166
197
  function readRc() {
167
198
  try {
168
- return JSON.parse(readFileSync(RC_PATH, "utf8"));
199
+ return JSON.parse(readFileSync2(RC_PATH, "utf8"));
169
200
  } catch {
170
201
  return {};
171
202
  }
@@ -350,20 +381,28 @@ var login = {
350
381
  name: "login",
351
382
  group: "Account",
352
383
  summary: "Sign in with your browser (or save an API key)",
353
- synopsis: "login [api-key]",
384
+ synopsis: "login [-]",
354
385
  leaf: {
355
386
  summary: "Sign in with your browser (OAuth2 + PKCE), or save an API key instead",
356
- usage: "easybits login [api-key] [--no-browser]",
387
+ usage: "easybits login [-|--with-token] [--no-browser]",
357
388
  options: {
358
- "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 -)" }
359
391
  },
360
392
  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)"
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"
364
398
  ],
365
399
  async run(ctx) {
366
- 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
+ }
367
406
  if (key) {
368
407
  const email2 = await fetchEmail(key);
369
408
  const { oauth: _drop, ...rest } = readRc();
@@ -528,8 +567,49 @@ var mcp = {
528
567
  };
529
568
 
530
569
  // src/commands/files.ts
531
- import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
570
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
532
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 or with --json)" }
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
533
613
  var MIME = {
534
614
  pdf: "application/pdf",
535
615
  png: "image/png",
@@ -580,14 +660,14 @@ var files = {
580
660
  examples: ["easybits files upload ./report.pdf", "easybits files upload ./logo.png --json"],
581
661
  async run(ctx) {
582
662
  const path = need(ctx, 0, "path", this.usage);
583
- if (!existsSync(path)) throw usageError(`File not found: ${path}`, this.usage);
663
+ if (!existsSync2(path)) throw usageError(`File not found: ${path}`, this.usage);
584
664
  const size = statSync(path).size;
585
665
  const contentType = MIME[extname(path).slice(1).toLowerCase()] || "application/octet-stream";
586
666
  const eb = await getClient(ctx);
587
667
  const data = await eb.uploadFile({ fileName: basename(path), contentType, size });
588
668
  const res = await fetch(data.putUrl, {
589
669
  method: "PUT",
590
- body: readFileSync2(path),
670
+ body: readFileSync3(path),
591
671
  headers: { "Content-Type": contentType }
592
672
  });
593
673
  if (!res.ok) throw new CliError(`Upload failed: storage answered ${res.status}`, 1, void 0, "upload_failed", res.status);
@@ -597,10 +677,13 @@ var files = {
597
677
  delete: {
598
678
  aliases: ["rm"],
599
679
  summary: "Delete a file (soft delete, 7-day trash)",
600
- usage: "easybits files delete <file-id>",
601
- 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"],
602
683
  async run(ctx) {
603
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)`);
604
687
  const eb = await getClient(ctx);
605
688
  const r = await eb.deleteFile(id);
606
689
  emit(ctx, r, () => console.log("Deleted"));
@@ -610,7 +693,7 @@ var files = {
610
693
  };
611
694
 
612
695
  // src/commands/init.ts
613
- import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
696
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
614
697
  var init = {
615
698
  name: "init",
616
699
  group: "Hosting",
@@ -627,7 +710,7 @@ var init = {
627
710
  async run(ctx) {
628
711
  const appDir = str(ctx, "app-dir") ?? "/srv/app";
629
712
  const port = str(ctx, "port") ?? "3000";
630
- if (!existsSync2("package.json")) {
713
+ if (!existsSync3("package.json")) {
631
714
  throw usageError("No package.json here. Run this at the root of your app's repo.", this.usage);
632
715
  }
633
716
  const workflow = `# Despliegue a EasyBits en cada push a main.
@@ -794,7 +877,7 @@ One-time setup left:
794
877
 
795
878
  3. If your app uses secret env vars:
796
879
 
797
- easybits machines secrets set SANDBOX_ID DATABASE_URL=...
880
+ easybits machines secrets set SANDBOX_ID --dotenv .env.production
798
881
 
799
882
  From then on, every push to main deploys.`));
800
883
  }
@@ -804,7 +887,7 @@ From then on, every push to main deploys.`));
804
887
  // src/commands/ssh.ts
805
888
  import { homedir as homedir2 } from "os";
806
889
  import { join as join2 } from "path";
807
- 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";
808
891
  import { execFileSync } from "child_process";
809
892
  var SSH_CONFIG_SNIPPET = `Host *.ghosty
810
893
  ProxyCommand easybits ssh-proxy %h
@@ -822,14 +905,14 @@ var sshKey = {
822
905
  const dir = join2(homedir2(), ".ssh");
823
906
  const key = join2(dir, "easybits_ed25519");
824
907
  const pub = `${key}.pub`;
825
- if (!existsSync3(pub)) {
908
+ if (!existsSync4(pub)) {
826
909
  mkdirSync2(dir, { recursive: true, mode: 448 });
827
910
  execFileSync("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", key, "-C", "easybits"], {
828
911
  stdio: ["ignore", "ignore", "inherit"]
829
912
  });
830
913
  console.error(`easybits: key created at ${key}`);
831
914
  }
832
- const publicKey = readFileSync3(pub, "utf8").trim();
915
+ const publicKey = readFileSync4(pub, "utf8").trim();
833
916
  if (ctx.json) process.stdout.write(JSON.stringify({ publicKey, path: pub }, null, 2) + "\n");
834
917
  else process.stdout.write(publicKey + "\n");
835
918
  }
@@ -881,7 +964,7 @@ var sshProxy = {
881
964
  };
882
965
 
883
966
  // src/commands/sandboxes.ts
884
- 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";
885
968
  function sandboxRecord(s) {
886
969
  return {
887
970
  sandboxId: s.sandboxId,
@@ -943,23 +1026,25 @@ var sandboxes = {
943
1026
  create: {
944
1027
  aliases: ["new"],
945
1028
  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]...",
1029
+ usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--timeout <s>] [--size s|m|l|xl] [--dotenv <path>] [--env K=V]...",
947
1030
  options: {
948
1031
  template: { type: "string", value: "name", description: "Base template (default ubuntu): ubuntu, python, node, code-interpreter\u2026" },
949
1032
  name: { type: "string", value: "name", description: "Human label (lets you `ssh <name>.ghosty`)" },
950
1033
  timeout: { type: "string", value: "seconds", description: "Time to live before auto-destroy" },
951
1034
  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)" },
1035
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
1036
+ ...ENV_FILE_OPTION,
953
1037
  "no-wait": { type: "boolean", description: "Return right away instead of waiting for running" }
954
1038
  },
955
1039
  examples: [
956
1040
  "easybits sandboxes create --template node --name scratch",
957
- "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"
958
1043
  ],
959
1044
  async run(ctx) {
960
1045
  const size = str(ctx, "size");
961
1046
  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);
1047
+ const env = await envFrom(ctx, this.usage);
963
1048
  const eb = await getClient(ctx);
964
1049
  const sb = await eb.sandboxes.create({
965
1050
  template: str(ctx, "template") ?? "ubuntu",
@@ -985,14 +1070,16 @@ var sandboxes = {
985
1070
  },
986
1071
  exec: {
987
1072
  summary: "Run a shell command inside a sandbox",
988
- 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...>",
989
1074
  options: {
990
1075
  cwd: { type: "string", value: "dir", description: "Working directory" },
991
1076
  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)" }
1077
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
1078
+ ...ENV_FILE_OPTION
993
1079
  },
994
1080
  examples: [
995
1081
  "easybits sandboxes exec sb_abc123 -- uname -a",
1082
+ "easybits sb exec sb_abc123 --dotenv .env -- node migrate.js",
996
1083
  "easybits sb exec sb_abc123 --cwd /data/work -- npm test",
997
1084
  "easybits sb exec sb_abc123 --json -- 'ls -la /' | jq .exitCode"
998
1085
  ],
@@ -1000,7 +1087,7 @@ var sandboxes = {
1000
1087
  need(ctx, 0, "sandbox-id", this.usage);
1001
1088
  const command = ctx.args.slice(1).join(" ");
1002
1089
  if (!command) throw usageError("Missing <command>.", this.usage);
1003
- const env = pairs(list(ctx, "env"), this.usage);
1090
+ const env = await envFrom(ctx, this.usage);
1004
1091
  const sb = await sandbox(ctx, this.usage);
1005
1092
  const r = await sb.exec(command, {
1006
1093
  cwd: str(ctx, "cwd"),
@@ -1086,8 +1173,8 @@ var sandboxes = {
1086
1173
  let buf;
1087
1174
  if (inline != null) buf = Buffer.from(inline, "utf8");
1088
1175
  else if (local) {
1089
- if (!existsSync4(local)) throw usageError(`File not found: ${local}`, FILES_USAGE);
1090
- buf = readFileSync4(local);
1176
+ if (!existsSync5(local)) throw usageError(`File not found: ${local}`, FILES_USAGE);
1177
+ buf = readFileSync5(local);
1091
1178
  } else if (!process.stdin.isTTY) buf = await readStdin();
1092
1179
  else throw usageError("Give a local file, --content, or pipe data on stdin.", FILES_USAGE);
1093
1180
  const r = await sb.files.write(path, buf.toString("base64"), { encoding: "base64" });
@@ -1108,11 +1195,14 @@ var sandboxes = {
1108
1195
  },
1109
1196
  resume: {
1110
1197
  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)" } },
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
+ },
1113
1203
  examples: ["easybits sandboxes resume sb_abc123"],
1114
1204
  async run(ctx) {
1115
- const env = pairs(list(ctx, "env"), this.usage);
1205
+ const env = await envFrom(ctx, this.usage);
1116
1206
  const r = await (await sandbox(ctx, this.usage)).resume(Object.keys(env).length ? { env } : {});
1117
1207
  emit(ctx, r, () => console.log(`Resumed ${r.sandboxId ?? ctx.args[0]} (${r.status ?? "running"})`));
1118
1208
  }
@@ -1120,10 +1210,19 @@ var sandboxes = {
1120
1210
  destroy: {
1121
1211
  aliases: ["rm", "delete"],
1122
1212
  summary: "Destroy a sandbox (irreversible)",
1123
- usage: "easybits sandboxes destroy <sandbox-id>",
1124
- 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"],
1125
1216
  async run(ctx) {
1126
- 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();
1127
1226
  emit(ctx, { ...r, sandboxId: ctx.args[0] }, () => console.log(`Destroyed ${ctx.args[0]}`));
1128
1227
  }
1129
1228
  },
@@ -1141,7 +1240,7 @@ var sandboxes = {
1141
1240
  };
1142
1241
 
1143
1242
  // src/commands/machines.ts
1144
- 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]";
1145
1244
  var machines = {
1146
1245
  name: "machines",
1147
1246
  aliases: ["deploy", "machine"],
@@ -1232,9 +1331,11 @@ var machines = {
1232
1331
  secrets: {
1233
1332
  summary: "App env vars stored encrypted in your vault",
1234
1333
  usage: SECRETS_USAGE,
1334
+ options: { ...ENV_FILE_OPTION },
1235
1335
  examples: [
1236
1336
  "easybits machines secrets ls sb_abc123",
1237
- "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 -`,
1238
1339
  "easybits machines secrets unset sb_abc123 API_KEY"
1239
1340
  ],
1240
1341
  async run(ctx) {
@@ -1248,8 +1349,9 @@ var machines = {
1248
1349
  console.log(`In vault: ${r.inVault.length ? r.inVault.join(", ") : "(none)"}`);
1249
1350
  });
1250
1351
  } 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);
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);
1253
1355
  const r = await eb.machines.setSecrets(id, values);
1254
1356
  emit(ctx, r, () => console.log(`Set ${Object.keys(values).join(", ")}. Injected now: ${r.secretNames.join(", ")}`));
1255
1357
  } else if (action === "unset") {
@@ -1321,10 +1423,14 @@ var domains = {
1321
1423
  rm: {
1322
1424
  aliases: ["remove", "delete"],
1323
1425
  summary: "Detach a domain",
1324
- usage: "easybits domains rm <sandbox-id> <domain>",
1325
- 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"],
1326
1429
  async run(ctx) {
1430
+ need(ctx, 0, "sandbox-id", this.usage);
1327
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.`);
1328
1434
  const r = await (await sandbox2(ctx, this.usage)).removeDomain(domain);
1329
1435
  emit(ctx, r, () => console.log(`Removed ${domain}`));
1330
1436
  }
@@ -1380,16 +1486,55 @@ var db = {
1380
1486
  rm: {
1381
1487
  aliases: ["delete", "drop"],
1382
1488
  summary: "Delete a database (irreversible)",
1383
- usage: "easybits db rm <db-id|name>",
1384
- 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"],
1385
1492
  async run(ctx) {
1386
1493
  const ref = need(ctx, 0, "db-id|name", this.usage);
1494
+ requireYesIfHeadless(ctx);
1387
1495
  const eb = await getClient(ctx);
1388
1496
  const hit = await findDb(eb, ref);
1497
+ await confirm(ctx, `Delete database ${hit.name} (${hit.id}) and all its data?`, { typeName: hit.name });
1389
1498
  const r = await eb.deleteDatabase(hit.id);
1390
1499
  emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(`Deleted ${hit.name} (${hit.id})`));
1391
1500
  }
1392
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
+ },
1393
1538
  query: {
1394
1539
  aliases: ["sql"],
1395
1540
  summary: "Run one SQL statement (by database id or name)",
@@ -1452,25 +1597,28 @@ var agents = {
1452
1597
  },
1453
1598
  create: {
1454
1599
  summary: "Create an agent from a template",
1455
- 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>]",
1456
1601
  options: {
1457
1602
  template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
1458
1603
  name: { type: "string", value: "name", description: "Label" },
1459
- 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,
1460
1606
  timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" }
1461
1607
  },
1462
1608
  examples: [
1463
1609
  "easybits agents create --template goose --name helper",
1464
- "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 -"
1465
1612
  ],
1466
1613
  async run(ctx) {
1467
1614
  const template = str(ctx, "template");
1468
1615
  if (!template) throw usageError("Missing --template.", this.usage);
1616
+ const env = await envFrom(ctx, this.usage);
1469
1617
  const eb = await getClient(ctx);
1470
1618
  const a = await eb.createAgent({
1471
1619
  template,
1472
1620
  name: str(ctx, "name"),
1473
- env: pairs(list(ctx, "env"), this.usage),
1621
+ env,
1474
1622
  timeoutSeconds: int(ctx, "timeout", this.usage)
1475
1623
  });
1476
1624
  emit(ctx, a, () => {
@@ -1505,10 +1653,13 @@ var agents = {
1505
1653
  destroy: {
1506
1654
  aliases: ["rm", "delete"],
1507
1655
  summary: "Destroy an agent and its sandbox",
1508
- usage: "easybits agents destroy <agent-id>",
1509
- 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"],
1510
1659
  async run(ctx) {
1511
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 });
1512
1663
  const eb = await getClient(ctx);
1513
1664
  const r = await eb.destroyAgent(id);
1514
1665
  emit(ctx, { ...r, agentId: id }, () => console.log(`Destroyed ${id}`));
@@ -1587,13 +1738,43 @@ function findCommand(name) {
1587
1738
  return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
1588
1739
  }
1589
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
+
1590
1766
  // src/help.ts
1591
1767
  var GLOBAL_FLAGS = `Global flags:
1592
1768
  --json Machine output: JSON on stdout, errors included
1593
- --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)
1594
1770
  -h, --help Help (also: easybits <command> --help)
1595
1771
  -v, --version Print the CLI version
1596
1772
 
1773
+ Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files delete) ask first;
1774
+ without a terminal or with --json pass -y/--yes, or they exit 2.
1775
+
1776
+ Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud)
1777
+
1597
1778
  Exit codes: 0 ok, 1 API error, 2 usage error, 3 not logged in or bad key`;
1598
1779
  var COL = 17;
1599
1780
  function globalHelp(version) {
@@ -1649,7 +1830,7 @@ var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;
1649
1830
 
1650
1831
  // src/index.ts
1651
1832
  import { EasybitsError as EasybitsError2 } from "@easybits.cloud/sdk";
1652
- var VERSION = true ? "0.5.1" : "dev";
1833
+ var VERSION = true ? "0.6.1" : "dev";
1653
1834
  var GLOBAL_OPTIONS = {
1654
1835
  json: { type: "boolean" },
1655
1836
  token: { type: "string" },
@@ -1692,16 +1873,44 @@ function parse(argv2, skip, leaf) {
1692
1873
  throw usageError(e.message.split("\n")[0].split(". To specify")[0].replace(/\.?$/, "."), leaf?.usage);
1693
1874
  }
1694
1875
  }
1876
+ function findSub(cmd, name) {
1877
+ return Object.entries(cmd.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
1878
+ }
1879
+ function unknownCommand(name) {
1880
+ const e = usageError(`Unknown command "${name}".`);
1881
+ const names = COMMANDS.flatMap((c) => [c.name, ...c.aliases ?? []]);
1882
+ const hint = didYouMean(name, names, "easybits ", (n) => findCommand(n)?.name ?? n);
1883
+ if (hint) e.hint = `${hint}
1884
+ Run: easybits --help`;
1885
+ return e;
1886
+ }
1887
+ function unknownSub(cmd, name) {
1888
+ const e = usageError(`Unknown subcommand "${cmd.name} ${name}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs ?? {}).join("|")}>`);
1889
+ const names = Object.entries(cmd.subs ?? {}).flatMap(([n, l]) => [n, ...l.aliases ?? []]);
1890
+ const hint = didYouMean(name, names, `easybits ${cmd.name} `, (n) => findSub(cmd, n)?.[0] ?? n);
1891
+ if (hint) e.hint = `${hint}
1892
+ ${e.hint}`;
1893
+ return e;
1894
+ }
1695
1895
  async function main(argv2) {
1696
1896
  const pos = positionalIndexes(argv2);
1697
1897
  const cmdName = pos.length ? argv2[pos[0]] : void 0;
1698
1898
  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);
1899
+ const topicName = cmdName === "help" && pos[1] != null ? argv2[pos[1]] : void 0;
1900
+ const topic = topicName ? findCommand(topicName) : void 0;
1901
+ if (topicName && !topic) throw unknownCommand(topicName);
1902
+ const subName = topic?.subs && pos[2] != null ? argv2[pos[2]] : void 0;
1903
+ const sub = topic && subName ? findSub(topic, subName) : void 0;
1904
+ if (topic && subName && !sub) throw unknownSub(topic, subName);
1905
+ const { values: values2 } = parse(argv2, pos.slice(0, sub ? 3 : 2), void 0);
1701
1906
  if (values2.version) {
1702
1907
  console.log(VERSION);
1703
1908
  return;
1704
1909
  }
1910
+ if (sub) {
1911
+ console.log(leafHelp(sub[1]));
1912
+ return;
1913
+ }
1705
1914
  if (topic) {
1706
1915
  console.log(commandHelp(topic));
1707
1916
  return;
@@ -1711,17 +1920,17 @@ async function main(argv2) {
1711
1920
  return;
1712
1921
  }
1713
1922
  const cmd = findCommand(cmdName);
1714
- if (!cmd) throw usageError(`Unknown command "${cmdName}".`);
1923
+ if (!cmd) throw unknownCommand(cmdName);
1715
1924
  let leaf = cmd.leaf;
1716
1925
  const skip = [pos[0]];
1717
1926
  if (cmd.subs) {
1718
1927
  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;
1928
+ const entry = subName ? findSub(cmd, subName) : void 0;
1720
1929
  if (entry) {
1721
1930
  leaf = entry[1];
1722
1931
  skip.push(pos[1]);
1723
1932
  } else if (subName) {
1724
- throw usageError(`Unknown subcommand "${cmd.name} ${subName}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs).join("|")}>`);
1933
+ throw unknownSub(cmd, subName);
1725
1934
  } else if (cmd.defaultSub && !argv2.includes("--help") && !argv2.includes("-h")) {
1726
1935
  leaf = cmd.subs[cmd.defaultSub];
1727
1936
  } 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.1",
3
+ "version": "0.6.1",
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.3"
39
+ "@easybits.cloud/sdk": "^0.36.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^22.0.0",