@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.
- package/README.md +49 -13
- package/dist/index.js +268 -59
- 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
|
|
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
|
|
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
|
|
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 <
|
|
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(
|
|
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 [
|
|
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 [
|
|
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
|
|
362
|
-
'easybits login --json
|
|
363
|
-
"easybits login
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
1090
|
-
buf =
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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> [
|
|
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
|
|
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
|
|
1252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
1700
|
-
const
|
|
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
|
|
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 ?
|
|
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
|
|
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.
|
|
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.
|
|
39
|
+
"@easybits.cloud/sdk": "^0.36.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22.0.0",
|