@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.
Files changed (2) hide show
  1. package/dist/index.js +221 -2
  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.1.0",
4
- "description": "Official CLI for Easybits — manage files, storage, and MCP from your terminal",
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.1.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
+ }