@renbostudios/edge-ota 0.2.3 → 0.2.4

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 +87 -95
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,6 +32,27 @@ import { createWriteStream } from "fs";
32
32
  import archiver from "archiver";
33
33
  import readline from "readline";
34
34
  import { generateECDSAKeyPair, signPayload, sha256Hex } from "./core/index.js";
35
+ // ─── Auto-load .env from the project root ────────────────────────────────────
36
+ // Lets users store EDGE_OTA_TOKEN in a .env file without shell exports.
37
+ try {
38
+ const envPath = path.resolve(process.cwd(), ".env");
39
+ if (fs.existsSync(envPath)) {
40
+ const lines = fs.readFileSync(envPath, "utf8").split("\n");
41
+ for (const line of lines) {
42
+ const trimmed = line.trim();
43
+ if (!trimmed || trimmed.startsWith("#"))
44
+ continue;
45
+ const eqIdx = trimmed.indexOf("=");
46
+ if (eqIdx === -1)
47
+ continue;
48
+ const key = trimmed.slice(0, eqIdx).trim();
49
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
50
+ if (!(key in process.env))
51
+ process.env[key] = val;
52
+ }
53
+ }
54
+ }
55
+ catch { /* silently skip if .env is unreadable */ }
35
56
  // ─── ANSI Styling Helpers ───────────────────────────────────────────────────
36
57
  const colors = {
37
58
  reset: "\x1b[0m",
@@ -50,16 +71,11 @@ const colors = {
50
71
  gray: "\x1b[90m",
51
72
  };
52
73
  function printBanner() {
53
- console.log(`\n${colors.bold}${colors.cyan} ┌──────────────────────────────────────────────────┐`);
54
- console.log(` │ ${colors.magenta}__ _ _ _ _ _ _ _ _ _ _ _ _ ${colors.cyan}│`);
55
- console.log(` │${colors.bold}${colors.white} ███████╗██████╗ ██████╗ ███████╗ ██████╗ ████████╗ █████╗ ${colors.cyan}│`);
56
- console.log(` │${colors.bold}${colors.white} ██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗╚══██╔══╝██╔══██╗ ${colors.cyan}│`);
57
- console.log(` │${colors.bold}${colors.white} █████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║ ██║ ███████║ ${colors.cyan}│`);
58
- console.log(` │${colors.bold}${colors.white} ██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║ ${colors.cyan}│`);
59
- console.log(` │${colors.bold}${colors.white} ███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝ ██║ ██║ ██║ ${colors.cyan}│`);
60
- console.log(` │${colors.bold}${colors.white} ╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ${colors.cyan}│`);
61
- console.log(` │ ${colors.gray}Zero-SDK OTA Update Platform — By Renbo Studios ${colors.cyan}│`);
62
- console.log(` └──────────────────────────────────────────────────┘${colors.reset}\n`);
74
+ const line = `${colors.dim}${"-".repeat(48)}${colors.reset}`;
75
+ console.log(`\n${line}`);
76
+ console.log(` ${colors.bold}${colors.white}edge-ota${colors.reset} ${colors.dim}v0.2.4 · Zero-SDK OTA for Expo${colors.reset}`);
77
+ console.log(` ${colors.dim}by Renbo Studios · renbostudios.com${colors.reset}`);
78
+ console.log(`${line}\n`);
63
79
  }
64
80
  function askQuestion(query) {
65
81
  const rl = readline.createInterface({
@@ -82,21 +98,19 @@ function updateAppJson(cwd, serverUrl, projectId) {
82
98
  try {
83
99
  const raw = fs.readFileSync(appJsonPath, "utf-8");
84
100
  const data = JSON.parse(raw);
85
- if (!data.expo) {
101
+ if (!data.expo)
86
102
  data.expo = {};
87
- }
88
- if (!data.expo.updates) {
103
+ if (!data.expo.updates)
89
104
  data.expo.updates = {};
90
- }
91
105
  const updateUrl = projectId
92
106
  ? `${serverUrl.replace(/\/$/, "")}/api/projects/${projectId}/updates`
93
107
  : `${serverUrl.replace(/\/$/, "")}/api/updates`;
94
108
  data.expo.updates.url = updateUrl;
95
109
  fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
96
- console.log(`✅ Updated app.json: expo.updates.url set to "${updateUrl}"`);
110
+ console.log(` ${colors.dim}app.json${colors.reset} updates.url set`);
97
111
  }
98
112
  catch (error) {
99
- console.error(`❌ Failed to update app.json: ${error.message}`);
113
+ console.error(` ${colors.red}error${colors.reset} Failed to update app.json: ${error.message}`);
100
114
  }
101
115
  }
102
116
  function loadConfig(cwd) {
@@ -213,14 +227,14 @@ program
213
227
  let serverUrl = options.server;
214
228
  let projectId = options.project;
215
229
  if (!serverUrl) {
216
- console.log(`${colors.bold}${colors.cyan}ℹ️ Server URL not provided via -s/--server.${colors.reset}`);
217
- const answer = await askQuestion("Enter your EdgeOTA server URL [http://localhost:3000]: ");
230
+ console.log(` ${colors.dim}no --server flag provided${colors.reset}`);
231
+ const answer = await askQuestion(` server url [http://localhost:3000]: `);
218
232
  if (!answer) {
219
233
  serverUrl = "http://localhost:3000";
220
234
  }
221
235
  else if (answer.toLowerCase().includes("host")) {
222
- console.log(`\n${colors.bold}${colors.yellow}ℹ️ Please set the server URL to your hosted EdgeOTA instance (e.g. https://api.edge-ota.renbo.site)${colors.reset}`);
223
- const hostedAnswer = await askQuestion("Hosted server URL: ");
236
+ console.log(` ${colors.yellow}hint${colors.reset} use your hosted EdgeOTA instance (e.g. https://api.edge-ota.renbo.site)`);
237
+ const hostedAnswer = await askQuestion(` hosted server url: `);
224
238
  serverUrl = hostedAnswer.trim() || "http://localhost:3000";
225
239
  }
226
240
  else {
@@ -228,56 +242,43 @@ program
228
242
  }
229
243
  }
230
244
  if (!projectId && !options.server) {
231
- console.log(`\n${colors.bold}${colors.cyan}ℹ️ Project ID not provided via --project.${colors.reset}`);
232
- const pAnswer = await askQuestion("Enter your EdgeOTA Project ID (optional, press Enter for single-tenant self-hosted): ");
233
- if (pAnswer.trim()) {
245
+ const pAnswer = await askQuestion(` project id (optional, press Enter to skip): `);
246
+ if (pAnswer.trim())
234
247
  projectId = pAnswer.trim();
235
- }
236
248
  }
237
- // Normalise URL (remove trailing slashes)
238
249
  serverUrl = serverUrl.replace(/\/$/, "");
239
- console.log(`⚙️ Generating ECDSA P-256 key pair...`);
250
+ console.log(` ${colors.dim}generating ECDSA P-256 key pair...${colors.reset}`);
240
251
  const keys = await generateECDSAKeyPair();
241
- const config = {
242
- serverUrl: serverUrl,
243
- publicKey: keys.publicKey
244
- };
245
- if (projectId) {
252
+ const config = { serverUrl, publicKey: keys.publicKey };
253
+ if (projectId)
246
254
  config.projectId = projectId;
247
- }
248
255
  const cwd = process.cwd();
249
256
  fs.writeFileSync("edge-ota.config.json", JSON.stringify(config, null, 2));
250
257
  fs.writeFileSync(".edge-ota.private.key", keys.privateKey, { mode: 0o600 });
251
- // Add the private key to .gitignore if it isn't already
252
258
  const gitignorePath = ".gitignore";
253
259
  const gitignoreEntry = ".edge-ota.private.key";
254
260
  if (fs.existsSync(gitignorePath)) {
255
261
  const current = fs.readFileSync(gitignorePath, "utf-8");
256
262
  if (!current.includes(gitignoreEntry)) {
257
263
  fs.appendFileSync(gitignorePath, `\n${gitignoreEntry}\n`);
258
- console.log("✅ Added .edge-ota.private.key to .gitignore");
259
264
  }
260
265
  }
261
266
  else {
262
267
  fs.writeFileSync(gitignorePath, `${gitignoreEntry}\n`);
263
268
  }
264
- // Auto-update app.json updates.url
265
269
  updateAppJson(cwd, serverUrl, projectId);
266
270
  const targetUrlStr = projectId
267
271
  ? `${serverUrl}/api/projects/${projectId}/updates`
268
272
  : `${serverUrl}/api/updates`;
269
- console.log(`\n${colors.bold}${colors.green}✔ EdgeOTA initialised successfully!${colors.reset}`);
270
- console.log(`${colors.gray}──────────────────────────────────────────────────${colors.reset}`);
271
- console.log(`📂 ${colors.bold}Config :${colors.reset} ${colors.cyan}edge-ota.config.json${colors.reset}`);
272
- console.log(`🔑 ${colors.bold}Key :${colors.reset} ${colors.yellow}.edge-ota.private.key${colors.reset} ${colors.dim}(keep secret!)${colors.reset}`);
273
- console.log(`\n${colors.bold}${colors.white}📋 Public key to paste into dashboard → Settings → General:${colors.reset}\n`);
274
- console.log(`${colors.gray}${keys.publicKey}${colors.reset}`);
275
- console.log(`\n${colors.bold}${colors.white}💡 Auto-configured app.json updates endpoint:${colors.reset}`);
276
- console.log(` ${colors.dim}"updates": { "url": "${colors.reset}${colors.cyan}${targetUrlStr}${colors.reset}${colors.dim}" }${colors.reset}\n`);
277
- console.log(`${colors.gray}──────────────────────────────────────────────────${colors.reset}`);
278
- console.log(`✨ ${colors.bold}${colors.white}Powered by EdgeOTA${colors.reset} — A product of ${colors.magenta}Renbo Studios${colors.reset}`);
279
- console.log(`🚀 Need scalable app development or custom Cloudflare setup?`);
280
- console.log(`👉 Partner with us at ${colors.underline}${colors.cyan}https://renbostudios.com${colors.reset} to launch your next project!\n`);
273
+ const sep = `${colors.dim}${"-".repeat(48)}${colors.reset}`;
274
+ console.log(`\n ${colors.green}✓${colors.reset} ${colors.bold}initialised${colors.reset}`);
275
+ console.log(sep);
276
+ console.log(` config ${colors.cyan}edge-ota.config.json${colors.reset}`);
277
+ console.log(` key ${colors.yellow}.edge-ota.private.key${colors.reset} ${colors.dim}(keep secret)${colors.reset}`);
278
+ console.log(` endpoint ${colors.dim}${targetUrlStr}${colors.reset}`);
279
+ console.log(sep);
280
+ console.log(`\n ${colors.bold}public key${colors.reset} ${colors.dim}paste into dashboard → Settings → General${colors.reset}\n`);
281
+ console.log(`${colors.dim}${keys.publicKey}${colors.reset}\n`);
281
282
  });
282
283
  // ──────────────────────────────────────────────────────
283
284
  // edge-ota push
@@ -313,41 +314,37 @@ program
313
314
  printBanner();
314
315
  const distDir = path.resolve(cwd, "dist");
315
316
  if (!options.skipExport) {
316
- console.log(`${colors.bold}${colors.cyan}📦 Running expo export...${colors.reset}`);
317
+ console.log(` ${colors.dim}running expo export...${colors.reset}`);
317
318
  try {
318
319
  execSync("npx expo export", { stdio: "inherit", cwd });
319
320
  }
320
321
  catch {
321
- console.error(`${colors.bold}${colors.red}❌ expo export failed. Aborting.${colors.reset}`);
322
+ console.error(` ${colors.red}error${colors.reset} expo export failed`);
322
323
  process.exit(1);
323
324
  }
324
325
  }
325
326
  else {
326
- console.log(`${colors.bold}${colors.yellow}⏭️ Skipping expo export (--skip-export)${colors.reset}`);
327
+ console.log(` ${colors.dim}skipping expo export (--skip-export)${colors.reset}`);
327
328
  }
328
329
  if (!fs.existsSync(distDir)) {
329
- console.error(`${colors.bold}${colors.red}❌ ./dist directory not found after export. Aborting.${colors.reset}`);
330
+ console.error(` ${colors.red}error${colors.reset} ./dist not found after export`);
330
331
  process.exit(1);
331
332
  }
332
- // ── Step 2: Collect assets & hashes ─────────────────────────────────────
333
- console.log(`\n${colors.bold}${colors.cyan}🔍 Collecting assets and computing SHA-256 hashes...${colors.reset}`);
333
+ console.log(` ${colors.dim}collecting assets...${colors.reset}`);
334
334
  const assets = await collectAssets(distDir);
335
- console.log(` Found ${colors.bold}${colors.white}${assets.length}${colors.reset} asset(s)`);
336
- if (projectId) {
337
- console.log(` Project : ${colors.bold}${colors.cyan}${projectId}${colors.reset}`);
338
- }
335
+ console.log(` ${colors.dim}found ${assets.length} asset(s)${projectId ? ` · project ${projectId}` : ""}${colors.reset}`);
339
336
  const platforms = options.platform === "all" ? ["ios", "android"] : [options.platform];
337
+ const sep = `${colors.dim}${"-".repeat(48)}${colors.reset}`;
340
338
  for (const platform of platforms) {
341
- console.log(`\n${colors.bold}${colors.magenta}🚀 Processing platform: ${platform.toUpperCase()}${colors.reset}`);
342
- // Find the JS bundle
339
+ console.log(`\n${sep}`);
340
+ console.log(` ${colors.bold}${platform}${colors.reset}`);
341
+ console.log(sep);
343
342
  const bundlePath = findBundle(distDir, platform);
344
343
  if (!bundlePath) {
345
- console.warn(`${colors.bold}${colors.yellow}⚠️ No bundle found for ${platform}. Skipping.${colors.reset}`);
344
+ console.warn(` ${colors.yellow}warn${colors.reset} no bundle found for ${platform}, skipping`);
346
345
  continue;
347
346
  }
348
347
  const bundleHash = await hashFile(bundlePath);
349
- const bundleAsset = assets.find(a => a.localPath === bundlePath);
350
- // ── Step 3: Build the signed payload ──────────────────────────────────
351
348
  const payloadObj = {
352
349
  channel: options.channel,
353
350
  runtimeVersion: options.runtime,
@@ -357,17 +354,16 @@ program
357
354
  assetCount: assets.length
358
355
  };
359
356
  const payloadStr = JSON.stringify(payloadObj);
360
- console.log(`🔏 Signing payload with ECDSA P-256...`);
357
+ console.log(` signing ECDSA P-256`);
361
358
  const signature = await signPayload(payloadStr, privateKey);
362
- console.log(` Signature: ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
359
+ console.log(` sig ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
363
360
  if (options.dryRun) {
364
- console.log(`${colors.bold}${colors.yellow}🏳️ Dry run — skipping upload.${colors.reset}`);
365
- console.log(" Payload:", payloadObj);
361
+ console.log(` ${colors.yellow}dry-run${colors.reset} upload skipped`);
362
+ console.log(" payload ", payloadObj);
366
363
  continue;
367
364
  }
368
- // ── Step 4: Upload ─────────────────────────────────────────────────────
369
365
  const zipPath = path.resolve(cwd, `update-bundle-${platform}.zip`);
370
- console.log(`🗜️ Zipping bundle...`);
366
+ console.log(` packing bundle → zip`);
371
367
  await zipDirectory(distDir, zipPath);
372
368
  const zipBuffer = fs.readFileSync(zipPath);
373
369
  const form = new FormData();
@@ -375,7 +371,7 @@ program
375
371
  form.append("payload", payloadStr);
376
372
  form.append("signature", signature);
377
373
  form.append("platform", platform);
378
- console.log(`📡 Uploading to ${colors.cyan}${uploadUrl}${colors.reset} ...`);
374
+ console.log(` uploading ${colors.dim}${uploadUrl}${colors.reset}`);
379
375
  const response = await fetch(uploadUrl, {
380
376
  method: "POST",
381
377
  headers: {
@@ -384,27 +380,25 @@ program
384
380
  },
385
381
  body: form
386
382
  });
387
- // Clean up zip
388
383
  fs.unlinkSync(zipPath);
389
384
  if (response.ok) {
390
385
  const body = await response.json().catch(() => ({}));
391
386
  const updateId = body.updateId || "unknown";
392
- console.log(`\n${colors.bold}${colors.green}✔ ${platform.toUpperCase()} update deployed successfully!${colors.reset}`);
393
- console.log(` ${colors.bold}Update ID :${colors.reset} ${colors.cyan}${updateId}${colors.reset}`);
394
- console.log(` ${colors.bold}Channel :${colors.reset} ${colors.white}${options.channel}${colors.reset}`);
395
- console.log(` ${colors.bold}Runtime :${colors.reset} ${colors.white}${options.runtime}${colors.reset}`);
396
- console.log(` ${colors.bold}Bundle :${colors.reset} ${colors.gray}${bundleHash.slice(0, 16)}…${colors.reset}`);
387
+ console.log(` ${colors.green}✓${colors.reset} deployed`);
388
+ console.log(` id ${colors.dim}${updateId}${colors.reset}`);
389
+ console.log(` channel ${options.channel}`);
390
+ console.log(` runtime ${options.runtime}`);
391
+ console.log(` bundle ${colors.dim}${bundleHash.slice(0, 16)}…${colors.reset}`);
397
392
  }
398
393
  else {
399
394
  const text = await response.text();
400
- console.error(`\n${colors.bold}${colors.red}❌ Upload failed (HTTP ${response.status}): ${text}${colors.reset}`);
395
+ console.error(` ${colors.red}error${colors.reset} upload failed (HTTP ${response.status}): ${text}`);
401
396
  process.exit(1);
402
397
  }
403
398
  }
404
- console.log(`\n${colors.bold}${colors.green}🎉 Done. Your app will receive this update on the next OTA sync.${colors.reset}\n` +
405
- `\n✨ ${colors.bold}${colors.white}Powered by EdgeOTA${colors.reset} — A product of ${colors.magenta}Renbo Studios${colors.reset}` +
406
- `\n🚀 Need scalable app development or custom Cloudflare setup?` +
407
- `\n👉 Partner with us at ${colors.underline}${colors.cyan}https://renbostudios.com${colors.reset} to launch your next project!\n`);
399
+ console.log(`\n${colors.dim}${"-".repeat(48)}${colors.reset}`);
400
+ console.log(` ${colors.green}✓${colors.reset} ${colors.bold}done${colors.reset} update will be applied on next OTA sync`);
401
+ console.log(`${colors.dim}${"-".repeat(48)}${colors.reset}\n`);
408
402
  });
409
403
  // ──────────────────────────────────────────────────────
410
404
  // edge-ota status
@@ -421,23 +415,20 @@ program
421
415
  const token = options.token || process.env.EDGE_OTA_TOKEN;
422
416
  const projectId = options.project || config.projectId;
423
417
  if (!token) {
424
- console.error("❌ Auth token required. Use -t <token> or set EDGE_OTA_TOKEN.");
418
+ console.error(` ${colors.red}error${colors.reset} token required — set EDGE_OTA_TOKEN in .env or use -t`);
425
419
  process.exit(1);
426
420
  }
427
421
  const headers = { Authorization: `Bearer ${token}` };
428
- if (projectId) {
422
+ if (projectId)
429
423
  headers["x-project-id"] = projectId;
430
- }
431
- const res = await fetch(`${config.serverUrl}/api/releases`, {
432
- headers
433
- });
424
+ const res = await fetch(`${config.serverUrl}/api/releases`, { headers });
434
425
  if (!res.ok) {
435
- console.error(`❌ Failed to fetch releases (HTTP ${res.status})`);
426
+ console.error(` ${colors.red}error${colors.reset} failed to fetch releases (HTTP ${res.status})`);
436
427
  process.exit(1);
437
428
  }
438
429
  const releases = (await res.json());
439
430
  if (!releases.length) {
440
- console.log("No releases found.");
431
+ console.log(` ${colors.dim}no releases found${colors.reset}`);
441
432
  return;
442
433
  }
443
434
  const rows = releases.slice(0, parseInt(options.limit)).map((r) => ({
@@ -446,11 +437,9 @@ program
446
437
  Runtime: r.runtime,
447
438
  Status: r.status,
448
439
  Published: r.published,
449
- "By": r.publisher
440
+ By: r.publisher
450
441
  }));
451
442
  console.table(rows);
452
- console.log("\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
453
- "\n👉 Need scalable app development or cloud setup? Visit us at https://renbostudios.com");
454
443
  });
455
444
  // ──────────────────────────────────────────────────────
456
445
  // edge-ota keygen
@@ -460,11 +449,14 @@ program
460
449
  .description("Generate a fresh ECDSA key pair (prints to stdout, does not write files)")
461
450
  .action(async () => {
462
451
  const keys = await generateECDSAKeyPair();
463
- console.log("--- PRIVATE KEY (keep secret) ---");
452
+ const sep = `${colors.dim}${"-".repeat(48)}${colors.reset}`;
453
+ console.log(`\n${sep}`);
454
+ console.log(` ${colors.bold}private key${colors.reset} ${colors.dim}keep secret — never commit${colors.reset}`);
455
+ console.log(sep);
464
456
  console.log(keys.privateKey);
465
- console.log("\n--- PUBLIC KEY (paste into dashboard) ---");
457
+ console.log(`\n${sep}`);
458
+ console.log(` ${colors.bold}public key${colors.reset} ${colors.dim}paste into dashboard → Settings → General${colors.reset}`);
459
+ console.log(sep);
466
460
  console.log(keys.publicKey);
467
- console.log("\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
468
- "\n👉 Need scalable app development or cloud setup? Visit us at https://renbostudios.com");
469
461
  });
470
462
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "preferGlobal": true,
5
5
  "description": "Zero-SDK, self-hostable OTA update CLI for Expo — push signed JS bundles to any EdgeOTA server",
6
6
  "license": "MIT",