@renbostudios/edge-ota 0.2.3 → 0.2.5

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 +89 -96
  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,61 +314,57 @@ 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,
354
351
  platform,
355
352
  bundleHash,
356
353
  timestamp: Date.now(),
357
- assetCount: assets.length
354
+ assetCount: assets.length,
355
+ publicKey: config.publicKey
358
356
  };
359
357
  const payloadStr = JSON.stringify(payloadObj);
360
- console.log(`🔏 Signing payload with ECDSA P-256...`);
358
+ console.log(` signing ECDSA P-256`);
361
359
  const signature = await signPayload(payloadStr, privateKey);
362
- console.log(` Signature: ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
360
+ console.log(` sig ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
363
361
  if (options.dryRun) {
364
- console.log(`${colors.bold}${colors.yellow}🏳️ Dry run — skipping upload.${colors.reset}`);
365
- console.log(" Payload:", payloadObj);
362
+ console.log(` ${colors.yellow}dry-run${colors.reset} upload skipped`);
363
+ console.log(" payload ", payloadObj);
366
364
  continue;
367
365
  }
368
- // ── Step 4: Upload ─────────────────────────────────────────────────────
369
366
  const zipPath = path.resolve(cwd, `update-bundle-${platform}.zip`);
370
- console.log(`🗜️ Zipping bundle...`);
367
+ console.log(` packing bundle → zip`);
371
368
  await zipDirectory(distDir, zipPath);
372
369
  const zipBuffer = fs.readFileSync(zipPath);
373
370
  const form = new FormData();
@@ -375,7 +372,7 @@ program
375
372
  form.append("payload", payloadStr);
376
373
  form.append("signature", signature);
377
374
  form.append("platform", platform);
378
- console.log(`📡 Uploading to ${colors.cyan}${uploadUrl}${colors.reset} ...`);
375
+ console.log(` uploading ${colors.dim}${uploadUrl}${colors.reset}`);
379
376
  const response = await fetch(uploadUrl, {
380
377
  method: "POST",
381
378
  headers: {
@@ -384,27 +381,25 @@ program
384
381
  },
385
382
  body: form
386
383
  });
387
- // Clean up zip
388
384
  fs.unlinkSync(zipPath);
389
385
  if (response.ok) {
390
386
  const body = await response.json().catch(() => ({}));
391
387
  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}`);
388
+ console.log(` ${colors.green}✓${colors.reset} deployed`);
389
+ console.log(` id ${colors.dim}${updateId}${colors.reset}`);
390
+ console.log(` channel ${options.channel}`);
391
+ console.log(` runtime ${options.runtime}`);
392
+ console.log(` bundle ${colors.dim}${bundleHash.slice(0, 16)}…${colors.reset}`);
397
393
  }
398
394
  else {
399
395
  const text = await response.text();
400
- console.error(`\n${colors.bold}${colors.red}❌ Upload failed (HTTP ${response.status}): ${text}${colors.reset}`);
396
+ console.error(` ${colors.red}error${colors.reset} upload failed (HTTP ${response.status}): ${text}`);
401
397
  process.exit(1);
402
398
  }
403
399
  }
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`);
400
+ console.log(`\n${colors.dim}${"-".repeat(48)}${colors.reset}`);
401
+ console.log(` ${colors.green}✓${colors.reset} ${colors.bold}done${colors.reset} update will be applied on next OTA sync`);
402
+ console.log(`${colors.dim}${"-".repeat(48)}${colors.reset}\n`);
408
403
  });
409
404
  // ──────────────────────────────────────────────────────
410
405
  // edge-ota status
@@ -421,23 +416,20 @@ program
421
416
  const token = options.token || process.env.EDGE_OTA_TOKEN;
422
417
  const projectId = options.project || config.projectId;
423
418
  if (!token) {
424
- console.error("❌ Auth token required. Use -t <token> or set EDGE_OTA_TOKEN.");
419
+ console.error(` ${colors.red}error${colors.reset} token required — set EDGE_OTA_TOKEN in .env or use -t`);
425
420
  process.exit(1);
426
421
  }
427
422
  const headers = { Authorization: `Bearer ${token}` };
428
- if (projectId) {
423
+ if (projectId)
429
424
  headers["x-project-id"] = projectId;
430
- }
431
- const res = await fetch(`${config.serverUrl}/api/releases`, {
432
- headers
433
- });
425
+ const res = await fetch(`${config.serverUrl}/api/releases`, { headers });
434
426
  if (!res.ok) {
435
- console.error(`❌ Failed to fetch releases (HTTP ${res.status})`);
427
+ console.error(` ${colors.red}error${colors.reset} failed to fetch releases (HTTP ${res.status})`);
436
428
  process.exit(1);
437
429
  }
438
430
  const releases = (await res.json());
439
431
  if (!releases.length) {
440
- console.log("No releases found.");
432
+ console.log(` ${colors.dim}no releases found${colors.reset}`);
441
433
  return;
442
434
  }
443
435
  const rows = releases.slice(0, parseInt(options.limit)).map((r) => ({
@@ -446,11 +438,9 @@ program
446
438
  Runtime: r.runtime,
447
439
  Status: r.status,
448
440
  Published: r.published,
449
- "By": r.publisher
441
+ By: r.publisher
450
442
  }));
451
443
  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
444
  });
455
445
  // ──────────────────────────────────────────────────────
456
446
  // edge-ota keygen
@@ -460,11 +450,14 @@ program
460
450
  .description("Generate a fresh ECDSA key pair (prints to stdout, does not write files)")
461
451
  .action(async () => {
462
452
  const keys = await generateECDSAKeyPair();
463
- console.log("--- PRIVATE KEY (keep secret) ---");
453
+ const sep = `${colors.dim}${"-".repeat(48)}${colors.reset}`;
454
+ console.log(`\n${sep}`);
455
+ console.log(` ${colors.bold}private key${colors.reset} ${colors.dim}keep secret — never commit${colors.reset}`);
456
+ console.log(sep);
464
457
  console.log(keys.privateKey);
465
- console.log("\n--- PUBLIC KEY (paste into dashboard) ---");
458
+ console.log(`\n${sep}`);
459
+ console.log(` ${colors.bold}public key${colors.reset} ${colors.dim}paste into dashboard → Settings → General${colors.reset}`);
460
+ console.log(sep);
466
461
  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
462
  });
470
463
  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.5",
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",