@easybits.cloud/cli 0.1.0 → 0.3.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/dist/index.js +221 -2
- package/package.json +11 -6
package/dist/index.js
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
resolveBaseUrl,
|
|
9
9
|
createClientFromEnv
|
|
10
10
|
} from "@easybits.cloud/sdk";
|
|
11
|
-
import { readFileSync, existsSync, statSync } from "fs";
|
|
11
|
+
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync } from "fs";
|
|
12
|
+
function flag(name) {
|
|
13
|
+
const i = process.argv.indexOf(name);
|
|
14
|
+
return i > -1 ? process.argv[i + 1] : void 0;
|
|
15
|
+
}
|
|
12
16
|
async function getClient() {
|
|
13
17
|
try {
|
|
14
18
|
return await createClientFromEnv();
|
|
@@ -151,6 +155,207 @@ function printMcpStdioConfig() {
|
|
|
151
155
|
};
|
|
152
156
|
console.log(JSON.stringify(config, null, 2));
|
|
153
157
|
}
|
|
158
|
+
async function init() {
|
|
159
|
+
const appDir = flag("--app-dir") ?? "/srv/app";
|
|
160
|
+
const port = flag("--port") ?? "3000";
|
|
161
|
+
if (!existsSync("package.json")) {
|
|
162
|
+
console.error(
|
|
163
|
+
"Aqu\xED no hay package.json. Corre esto en la ra\xEDz del repo de tu app."
|
|
164
|
+
);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const workflow = `# Despliegue a EasyBits en cada push a main.
|
|
168
|
+
#
|
|
169
|
+
# El build ocurre AQU\xCD, no dentro de la m\xE1quina: as\xED la caja s\xF3lo descarga y
|
|
170
|
+
# arranca, y el sitio cabe en un tier peque\xF1o. Si el build falla, no llega a
|
|
171
|
+
# producci\xF3n y el sitio sigue en pie.
|
|
172
|
+
#
|
|
173
|
+
# Secretos que necesita el repo (Settings \u2192 Secrets and variables \u2192 Actions):
|
|
174
|
+
# EASYBITS_API_KEY tu key de easybits.cloud/dash/developer
|
|
175
|
+
# EASYBITS_SANDBOX_ID el id que devolvi\xF3 machines/launch al crear la m\xE1quina
|
|
176
|
+
#
|
|
177
|
+
# Las variables de la app (DATABASE_URL, etc.) NO van aqu\xED: se cargan una vez
|
|
178
|
+
# con PUT /machines/:id/secrets y viven cifradas en tu b\xF3veda.
|
|
179
|
+
name: Deploy a EasyBits
|
|
180
|
+
|
|
181
|
+
on:
|
|
182
|
+
push:
|
|
183
|
+
branches: [main]
|
|
184
|
+
workflow_dispatch:
|
|
185
|
+
|
|
186
|
+
concurrency: deploy-produccion
|
|
187
|
+
|
|
188
|
+
jobs:
|
|
189
|
+
deploy:
|
|
190
|
+
runs-on: ubuntu-latest
|
|
191
|
+
steps:
|
|
192
|
+
- uses: actions/checkout@v4
|
|
193
|
+
- uses: actions/setup-node@v4
|
|
194
|
+
with:
|
|
195
|
+
node-version: 24
|
|
196
|
+
cache: npm
|
|
197
|
+
- run: npm ci
|
|
198
|
+
- run: npm run build
|
|
199
|
+
|
|
200
|
+
- name: Empaquetar y desplegar
|
|
201
|
+
env:
|
|
202
|
+
EASYBITS_API_KEY: \${{ secrets.EASYBITS_API_KEY }}
|
|
203
|
+
EASYBITS_SANDBOX_ID: \${{ secrets.EASYBITS_SANDBOX_ID }}
|
|
204
|
+
EASYBITS_APP_DIR: ${appDir}
|
|
205
|
+
EASYBITS_PORT: "${port}"
|
|
206
|
+
run: node .github/scripts/easybits-deploy.mjs
|
|
207
|
+
`;
|
|
208
|
+
const script = [
|
|
209
|
+
"// Empaqueta lo que hace falta para servir y lo manda a la m\xE1quina.",
|
|
210
|
+
"// Lo genera 'easybits init'; aj\xFAstalo si tu app necesita otros archivos.",
|
|
211
|
+
"",
|
|
212
|
+
'import { createReadStream, statSync } from "node:fs";',
|
|
213
|
+
'import { execFileSync } from "node:child_process";',
|
|
214
|
+
"",
|
|
215
|
+
'const API = process.env.EASYBITS_API || "https://www.easybits.cloud/api/v2";',
|
|
216
|
+
"const KEY = process.env.EASYBITS_API_KEY;",
|
|
217
|
+
"const MACHINE = process.env.EASYBITS_SANDBOX_ID;",
|
|
218
|
+
'const APP_DIR = process.env.EASYBITS_APP_DIR || "' + appDir + '";',
|
|
219
|
+
"const PORT = Number(process.env.EASYBITS_PORT || " + port + ");",
|
|
220
|
+
'const TARBALL = "/tmp/easybits-build.tgz";',
|
|
221
|
+
'const SHA = (process.env.GITHUB_SHA || "manual").slice(0, 7);',
|
|
222
|
+
"",
|
|
223
|
+
"if (!KEY || !MACHINE) {",
|
|
224
|
+
' console.error("Faltan EASYBITS_API_KEY o EASYBITS_SANDBOX_ID en los secretos del repo.");',
|
|
225
|
+
" process.exit(1);",
|
|
226
|
+
"}",
|
|
227
|
+
"",
|
|
228
|
+
"async function api(path, body) {",
|
|
229
|
+
" const res = await fetch(API + path, {",
|
|
230
|
+
' method: "POST",',
|
|
231
|
+
' headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json" },',
|
|
232
|
+
" body: JSON.stringify(body),",
|
|
233
|
+
" });",
|
|
234
|
+
" const text = await res.text();",
|
|
235
|
+
" let data;",
|
|
236
|
+
" try { data = JSON.parse(text); } catch { data = {}; }",
|
|
237
|
+
" if (!res.ok) {",
|
|
238
|
+
' console.error("x " + path + " respondio " + res.status);',
|
|
239
|
+
" console.error(data.message || data.error || text);",
|
|
240
|
+
" process.exit(1);",
|
|
241
|
+
" }",
|
|
242
|
+
" return data;",
|
|
243
|
+
"}",
|
|
244
|
+
"",
|
|
245
|
+
"// Servir no necesita vite ni los compiladores: podarlos es la diferencia",
|
|
246
|
+
"// entre mandar 170 MB por deploy y mandar lo que la maquina ejecuta.",
|
|
247
|
+
'console.log("-> podando dependencias de desarrollo");',
|
|
248
|
+
'execFileSync("npm", ["prune", "--omit=dev"], { stdio: "inherit" });',
|
|
249
|
+
"",
|
|
250
|
+
'console.log("-> empaquetando");',
|
|
251
|
+
'execFileSync("tar", [',
|
|
252
|
+
' "czf", TARBALL, "--exclude=.git", "--exclude=node_modules/.cache",',
|
|
253
|
+
' "build", "node_modules", "package.json", "package-lock.json",',
|
|
254
|
+
"]);",
|
|
255
|
+
"const size = statSync(TARBALL).size;",
|
|
256
|
+
'console.log(" " + (size / 1048576).toFixed(1) + " MB");',
|
|
257
|
+
"",
|
|
258
|
+
"// Publico y de vida corta: la caja lo baja con curl, sin credenciales. No",
|
|
259
|
+
"// lleva secretos: esos los inyecta EasyBits desde la boveda, ya dentro.",
|
|
260
|
+
'console.log("-> subiendo");',
|
|
261
|
+
'const up = await api("/files", {',
|
|
262
|
+
' fileName: "build-" + SHA + ".tgz",',
|
|
263
|
+
' contentType: "application/gzip",',
|
|
264
|
+
" size,",
|
|
265
|
+
' access: "public",',
|
|
266
|
+
"});",
|
|
267
|
+
"const put = await fetch(up.putUrl, {",
|
|
268
|
+
' method: "PUT",',
|
|
269
|
+
' headers: { "Content-Type": "application/gzip", "Content-Length": String(size) },',
|
|
270
|
+
" body: createReadStream(TARBALL),",
|
|
271
|
+
' duplex: "half",',
|
|
272
|
+
"});",
|
|
273
|
+
"if (!put.ok) {",
|
|
274
|
+
' console.error("x la subida devolvio " + put.status);',
|
|
275
|
+
" process.exit(1);",
|
|
276
|
+
"}",
|
|
277
|
+
"",
|
|
278
|
+
"// prebuilt: la caja no reconstruye nada, solo descomprime y arranca.",
|
|
279
|
+
'console.log("-> desplegando");',
|
|
280
|
+
'const out = await api("/machines/launch", {',
|
|
281
|
+
" sandboxId: MACHINE,",
|
|
282
|
+
" archiveUrl: up.file.url,",
|
|
283
|
+
" prebuilt: true,",
|
|
284
|
+
" appDir: APP_DIR,",
|
|
285
|
+
" port: PORT,",
|
|
286
|
+
' message: "deploy " + SHA,',
|
|
287
|
+
"});",
|
|
288
|
+
"",
|
|
289
|
+
"if (out.exitCode !== 0) {",
|
|
290
|
+
' console.error("x termino con codigo " + out.exitCode);',
|
|
291
|
+
' console.error(out.buildOutput || out.startOutput || "");',
|
|
292
|
+
" process.exit(1);",
|
|
293
|
+
"}",
|
|
294
|
+
'console.log("OK desplegado - version " + out.version);',
|
|
295
|
+
'console.log(" " + out.url);',
|
|
296
|
+
""
|
|
297
|
+
].join("\n");
|
|
298
|
+
mkdirSync(".github/workflows", { recursive: true });
|
|
299
|
+
mkdirSync(".github/scripts", { recursive: true });
|
|
300
|
+
writeFileSync(".github/workflows/easybits-deploy.yml", workflow);
|
|
301
|
+
writeFileSync(".github/scripts/easybits-deploy.mjs", script);
|
|
302
|
+
console.log(`Listo. Escrib\xED:
|
|
303
|
+
.github/workflows/easybits-deploy.yml
|
|
304
|
+
.github/scripts/easybits-deploy.mjs
|
|
305
|
+
|
|
306
|
+
Falta, una sola vez:
|
|
307
|
+
|
|
308
|
+
1. Crea la m\xE1quina (si a\xFAn no la tienes):
|
|
309
|
+
|
|
310
|
+
curl -X POST https://www.easybits.cloud/api/v2/machines/launch \\
|
|
311
|
+
-H "Authorization: Bearer $EASYBITS_API_KEY" \\
|
|
312
|
+
-H "Content-Type: application/json" \\
|
|
313
|
+
-d '{"repo":"https://github.com/TU/REPO.git","branch":"main",
|
|
314
|
+
"tier":"micro","template":"node","appDir":"${appDir}","port":${port}}'
|
|
315
|
+
|
|
316
|
+
Un repo privado necesita un token en la URL:
|
|
317
|
+
https://x-access-token:TOKEN@github.com/TU/REPO.git
|
|
318
|
+
|
|
319
|
+
2. Guarda en el repo (Settings \u2192 Secrets and variables \u2192 Actions):
|
|
320
|
+
EASYBITS_API_KEY tu key
|
|
321
|
+
EASYBITS_SANDBOX_ID el id que devolvi\xF3 el paso 1
|
|
322
|
+
|
|
323
|
+
3. Si tu app usa variables secretas:
|
|
324
|
+
|
|
325
|
+
curl -X PUT https://www.easybits.cloud/api/v2/machines/SANDBOX_ID/secrets \\
|
|
326
|
+
-H "Authorization: Bearer $EASYBITS_API_KEY" \\
|
|
327
|
+
-H "Content-Type: application/json" \\
|
|
328
|
+
-d '{"DATABASE_URL":"..."}'
|
|
329
|
+
|
|
330
|
+
Desde ah\xED, cada push a main despliega.`);
|
|
331
|
+
}
|
|
332
|
+
async function sshProxy() {
|
|
333
|
+
const target = process.argv[3];
|
|
334
|
+
if (!target) {
|
|
335
|
+
console.error("uso: easybits ssh-proxy <sandboxId|caja.ghosty>");
|
|
336
|
+
process.exit(2);
|
|
337
|
+
}
|
|
338
|
+
const sandboxId = target.split(".")[0];
|
|
339
|
+
const eb = await createClientFromEnv();
|
|
340
|
+
const sb = await eb.sandboxes.get(sandboxId);
|
|
341
|
+
const { url } = await sb.sshTicket();
|
|
342
|
+
const ws = new WebSocket(url);
|
|
343
|
+
ws.binaryType = "arraybuffer";
|
|
344
|
+
ws.onerror = () => {
|
|
345
|
+
console.error("easybits ssh-proxy: no se pudo abrir el t\xFAnel");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
};
|
|
348
|
+
ws.onclose = () => process.exit(0);
|
|
349
|
+
ws.onmessage = (ev) => {
|
|
350
|
+
const data = ev.data;
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
typeof data === "string" ? Buffer.from(data) : Buffer.from(data)
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
await new Promise((resolve) => ws.onopen = () => resolve());
|
|
356
|
+
process.stdin.on("data", (chunk) => ws.send(chunk));
|
|
357
|
+
process.stdin.on("end", () => ws.close());
|
|
358
|
+
}
|
|
154
359
|
var [cmd, sub] = [process.argv[2], process.argv[3]];
|
|
155
360
|
switch (cmd) {
|
|
156
361
|
case "login":
|
|
@@ -165,24 +370,38 @@ switch (cmd) {
|
|
|
165
370
|
case "providers":
|
|
166
371
|
providersList();
|
|
167
372
|
break;
|
|
373
|
+
case "init":
|
|
374
|
+
init();
|
|
375
|
+
break;
|
|
168
376
|
case "config":
|
|
169
377
|
printMcpConfig();
|
|
170
378
|
break;
|
|
171
379
|
case "mcp":
|
|
172
380
|
printMcpStdioConfig();
|
|
173
381
|
break;
|
|
382
|
+
case "ssh-proxy":
|
|
383
|
+
sshProxy();
|
|
384
|
+
break;
|
|
174
385
|
case "help":
|
|
175
386
|
case void 0:
|
|
176
387
|
console.log(`easybits CLI \u2014 @easybits.cloud/cli
|
|
177
388
|
|
|
178
389
|
Commands:
|
|
179
390
|
login <key> Save API key
|
|
391
|
+
init Write the GitHub Actions deploy workflow
|
|
392
|
+
(--app-dir /srv/app, --port 3000)
|
|
180
393
|
files list List your files
|
|
181
394
|
files upload <f> Upload a file
|
|
182
395
|
files delete <id> Delete a file
|
|
183
396
|
providers list Show storage providers
|
|
184
397
|
config Print MCP config JSON (streamable HTTP)
|
|
185
|
-
mcp Print MCP stdio config JSON
|
|
398
|
+
mcp Print MCP stdio config JSON
|
|
399
|
+
ssh-proxy <id> SSH tunnel over 443 (use as ssh ProxyCommand)
|
|
400
|
+
|
|
401
|
+
SSH to a box \u2014 add to ~/.ssh/config:
|
|
402
|
+
Host *.ghosty
|
|
403
|
+
ProxyCommand easybits ssh-proxy %h
|
|
404
|
+
User root`);
|
|
186
405
|
break;
|
|
187
406
|
default:
|
|
188
407
|
console.error(`Unknown command: ${cmd}. Run 'easybits help'`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easybits.cloud/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Official CLI for Easybits
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Official CLI for Easybits \u2014 manage files, storage, and MCP from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"easybits": "./dist/index.js"
|
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
27
|
-
"dev": "tsup src/index.ts --format esm --watch"
|
|
27
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@easybits.cloud/sdk": "^0.
|
|
34
|
+
"@easybits.cloud/sdk": "^0.30.0"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
37
|
"tsup": "^8.0.0",
|
|
34
|
-
"typescript": "^5.7.0"
|
|
38
|
+
"typescript": "^5.7.0",
|
|
39
|
+
"@types/node": "^22.0.0"
|
|
35
40
|
}
|
|
36
|
-
}
|
|
41
|
+
}
|