@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.
- package/README.md +33 -10
- package/dist/index.js +281 -63
- 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
|
|
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
|
|
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 <
|
|
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(
|
|
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 [
|
|
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 [
|
|
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
|
|
351
|
-
'easybits login --json
|
|
352
|
-
"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"
|
|
353
398
|
],
|
|
354
399
|
async run(ctx) {
|
|
355
|
-
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
1077
|
-
buf =
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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> [
|
|
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
|
|
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
|
|
1239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
1687
|
-
const
|
|
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
|
|
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 ?
|
|
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
|
|
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.
|
|
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.
|
|
39
|
+
"@easybits.cloud/sdk": "^0.36.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22.0.0",
|