@renbostudios/edge-ota 0.2.0 → 0.2.2

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 (3) hide show
  1. package/README.md +9 -6
  2. package/dist/index.js +97 -47
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # `@renbostudios/edge-ota`
1
+ # `@edge-ota/cli`
2
2
 
3
3
  > Zero-SDK, self-hostable OTA update CLI for Expo apps.
4
4
  > Push cryptographically signed JS bundles to any EdgeOTA server — no Expo account required.
@@ -6,9 +6,9 @@
6
6
  ## Install
7
7
 
8
8
  ```bash
9
- npm install -g @renbostudios/edge-ota
9
+ npm install -g @edge-ota/cli
10
10
  # or use without installing:
11
- npx @renbostudios/edge-ota <command>
11
+ npx @edge-ota/cli <command>
12
12
  ```
13
13
 
14
14
  ## Quick Start
@@ -32,16 +32,17 @@ edge-ota status
32
32
 
33
33
  Generates an ECDSA P-256 key pair, writes `edge-ota.config.json` and `.edge-ota.private.key`, auto-adds the private key to `.gitignore`, and automatically updates your Expo `app.json` configuration file with the new updates URL.
34
34
 
35
- If `-s` or `--server` is not specified, you will be prompted interactively to enter the URL of your hosted or local EdgeOTA server.
35
+ If `--server` is not specified, you will be prompted interactively to enter the URL of your hosted or local EdgeOTA server and your optional Project ID.
36
36
 
37
37
  | Option | Default | Description |
38
38
  |--------|---------|-------------|
39
39
  | `-s, --server <url>` | — | Your EdgeOTA server URL (prompts if omitted) |
40
+ | `--project <id>` | — | Your EdgeOTA Project ID (prompts if omitted, optional) |
40
41
 
41
42
  **Output:**
42
- - `edge-ota.config.json` — server URL + public key (safe to commit)
43
+ - `edge-ota.config.json` — server URL, public key, and optional project ID (safe to commit)
43
44
  - `.edge-ota.private.key` — private signing key (**never commit this**)
44
- - **Auto-configured:** `app.json` is modified to point `expo.updates.url` to your server's `/api/updates` endpoint.
45
+ - **Auto-configured:** `app.json` is modified to point `expo.updates.url` to your server's updates endpoint. If a Project ID is configured, it points to `/api/projects/:projectId/updates`; otherwise, it falls back to the global `/api/updates` endpoint.
45
46
 
46
47
  ---
47
48
 
@@ -54,6 +55,7 @@ Runs `expo export`, hashes every asset with SHA-256, signs the payload with your
54
55
  | `-c, --channel <name>` | `production` | Deployment channel |
55
56
  | `-r, --runtime <version>` | `1.0.0` | Native runtime version (must match `runtimeVersion` in app.json) |
56
57
  | `-p, --platform <platform>` | `all` | `ios`, `android`, or `all` |
58
+ | `--project <id>` | — | Project ID (overrides value in `edge-ota.config.json`) |
57
59
  | `--skip-export` | — | Skip `expo export` — use existing `./dist` |
58
60
  | `--dry-run` | — | Sign the payload but don't upload |
59
61
 
@@ -75,6 +77,7 @@ Lists recent releases from the server as a table.
75
77
  |--------|---------|-------------|
76
78
  | `-t, --token <token>` | `$EDGE_OTA_TOKEN` | Auth token from the dashboard |
77
79
  | `-n, --limit <n>` | `10` | Number of releases to show |
80
+ | `--project <id>` | — | Project ID to query (overrides config) |
78
81
 
79
82
  ```bash
80
83
  export EDGE_OTA_TOKEN="your-dashboard-token"
package/dist/index.js CHANGED
@@ -32,6 +32,35 @@ 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
+ // ─── ANSI Styling Helpers ───────────────────────────────────────────────────
36
+ const colors = {
37
+ reset: "\x1b[0m",
38
+ bold: "\x1b[1m",
39
+ dim: "\x1b[2m",
40
+ underline: "\x1b[4m",
41
+ // Foreground Colors
42
+ black: "\x1b[30m",
43
+ red: "\x1b[31m",
44
+ green: "\x1b[32m",
45
+ yellow: "\x1b[33m",
46
+ blue: "\x1b[34m",
47
+ magenta: "\x1b[35m",
48
+ cyan: "\x1b[36m",
49
+ white: "\x1b[37m",
50
+ gray: "\x1b[90m",
51
+ };
52
+ 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`);
63
+ }
35
64
  function askQuestion(query) {
36
65
  const rl = readline.createInterface({
37
66
  input: process.stdin,
@@ -44,7 +73,7 @@ function askQuestion(query) {
44
73
  });
45
74
  });
46
75
  }
47
- function updateAppJson(cwd, serverUrl) {
76
+ function updateAppJson(cwd, serverUrl, projectId) {
48
77
  const appJsonPath = path.resolve(cwd, "app.json");
49
78
  if (!fs.existsSync(appJsonPath)) {
50
79
  console.log("⚠️ app.json not found in this directory. Skipping auto-update.");
@@ -59,7 +88,9 @@ function updateAppJson(cwd, serverUrl) {
59
88
  if (!data.expo.updates) {
60
89
  data.expo.updates = {};
61
90
  }
62
- const updateUrl = `${serverUrl.replace(/\/$/, "")}/api/updates`;
91
+ const updateUrl = projectId
92
+ ? `${serverUrl.replace(/\/$/, "")}/api/projects/${projectId}/updates`
93
+ : `${serverUrl.replace(/\/$/, "")}/api/updates`;
63
94
  data.expo.updates.url = updateUrl;
64
95
  fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
65
96
  console.log(`✅ Updated app.json: expo.updates.url set to "${updateUrl}"`);
@@ -176,16 +207,19 @@ program
176
207
  .command("init")
177
208
  .description("Generate an ECDSA P-256 key pair and write edge-ota.config.json")
178
209
  .option("-s, --server <url>", "EdgeOTA server URL")
210
+ .option("--project <id>", "EdgeOTA Project ID (optional)")
179
211
  .action(async (options) => {
212
+ printBanner();
180
213
  let serverUrl = options.server;
214
+ let projectId = options.project;
181
215
  if (!serverUrl) {
182
- console.log("ℹ️ Server URL not provided via -s/--server.");
216
+ console.log(`${colors.bold}${colors.cyan}ℹ️ Server URL not provided via -s/--server.${colors.reset}`);
183
217
  const answer = await askQuestion("Enter your EdgeOTA server URL [http://localhost:3000]: ");
184
218
  if (!answer) {
185
219
  serverUrl = "http://localhost:3000";
186
220
  }
187
221
  else if (answer.toLowerCase().includes("host")) {
188
- console.log("\nℹ️ Please set the server URL to your hosted EdgeOTA instance (e.g. https://ota.renbostudios.com)");
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}`);
189
223
  const hostedAnswer = await askQuestion("Hosted server URL: ");
190
224
  serverUrl = hostedAnswer.trim() || "http://localhost:3000";
191
225
  }
@@ -193,14 +227,24 @@ program
193
227
  serverUrl = answer;
194
228
  }
195
229
  }
230
+ 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()) {
234
+ projectId = pAnswer.trim();
235
+ }
236
+ }
196
237
  // Normalise URL (remove trailing slashes)
197
238
  serverUrl = serverUrl.replace(/\/$/, "");
198
- console.log("⚙️ Generating ECDSA P-256 key pair...");
239
+ console.log(`⚙️ Generating ECDSA P-256 key pair...`);
199
240
  const keys = await generateECDSAKeyPair();
200
241
  const config = {
201
242
  serverUrl: serverUrl,
202
243
  publicKey: keys.publicKey
203
244
  };
245
+ if (projectId) {
246
+ config.projectId = projectId;
247
+ }
204
248
  const cwd = process.cwd();
205
249
  fs.writeFileSync("edge-ota.config.json", JSON.stringify(config, null, 2));
206
250
  fs.writeFileSync(".edge-ota.private.key", keys.privateKey, { mode: 0o600 });
@@ -218,19 +262,22 @@ program
218
262
  fs.writeFileSync(gitignorePath, `${gitignoreEntry}\n`);
219
263
  }
220
264
  // Auto-update app.json updates.url
221
- updateAppJson(cwd, serverUrl);
222
- console.log("\n✅ EdgeOTA initialised.");
223
- console.log(" Config : edge-ota.config.json");
224
- console.log(" Key : .edge-ota.private.key (keep this secret!)");
225
- console.log("\n📋 Public key to paste into your dashboard → Settings → Infrastructure:\n");
226
- console.log(keys.publicKey);
227
- console.log("\n💡 Your app.json updates configuration has been configured to:\n" +
228
- ' "updates": {\n' +
229
- ' "url": "' + serverUrl + '/api/updates"\n' +
230
- " }\n" +
231
- "\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
232
- "\n🚀 Need scalable app development or custom Cloudflare setup?" +
233
- "\n👉 Partner with us at https://renbostudios.com to launch your next project!");
265
+ updateAppJson(cwd, serverUrl, projectId);
266
+ const targetUrlStr = projectId
267
+ ? `${serverUrl}/api/projects/${projectId}/updates`
268
+ : `${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`);
234
281
  });
235
282
  // ──────────────────────────────────────────────────────
236
283
  // edge-ota push
@@ -241,6 +288,7 @@ program
241
288
  .option("-c, --channel <channel>", "Deployment channel", "production")
242
289
  .option("-r, --runtime <version>", "React Native runtime version (semver)", "1.0.0")
243
290
  .option("-p, --platform <platform>", "Target platform: ios | android | all", "all")
291
+ .option("--project <id>", "Project ID (optional, overrides config)")
244
292
  .option("--skip-export", "Skip expo export (use existing ./dist directory)")
245
293
  .option("--dry-run", "Build and sign the payload but do NOT upload")
246
294
  .action(async (options) => {
@@ -248,35 +296,36 @@ program
248
296
  const config = loadConfig(cwd);
249
297
  const privateKey = loadPrivateKey(cwd);
250
298
  // ── Step 1: Build ────────────────────────────────────────────────────────
299
+ printBanner();
251
300
  const distDir = path.resolve(cwd, "dist");
252
301
  if (!options.skipExport) {
253
- console.log("📦 Running expo export...");
302
+ console.log(`${colors.bold}${colors.cyan}📦 Running expo export...${colors.reset}`);
254
303
  try {
255
304
  execSync("npx expo export", { stdio: "inherit", cwd });
256
305
  }
257
306
  catch {
258
- console.error("❌ expo export failed. Aborting.");
307
+ console.error(`${colors.bold}${colors.red}❌ expo export failed. Aborting.${colors.reset}`);
259
308
  process.exit(1);
260
309
  }
261
310
  }
262
311
  else {
263
- console.log("⏭️ Skipping expo export (--skip-export)");
312
+ console.log(`${colors.bold}${colors.yellow}⏭️ Skipping expo export (--skip-export)${colors.reset}`);
264
313
  }
265
314
  if (!fs.existsSync(distDir)) {
266
- console.error("❌ ./dist directory not found after export. Aborting.");
315
+ console.error(`${colors.bold}${colors.red}❌ ./dist directory not found after export. Aborting.${colors.reset}`);
267
316
  process.exit(1);
268
317
  }
269
318
  // ── Step 2: Collect assets & hashes ─────────────────────────────────────
270
- console.log("🔍 Collecting assets and computing SHA-256 hashes...");
319
+ console.log(`\n${colors.bold}${colors.cyan}🔍 Collecting assets and computing SHA-256 hashes...${colors.reset}`);
271
320
  const assets = await collectAssets(distDir);
272
- console.log(` Found ${assets.length} asset(s)`);
321
+ console.log(` Found ${colors.bold}${colors.white}${assets.length}${colors.reset} asset(s)`);
273
322
  const platforms = options.platform === "all" ? ["ios", "android"] : [options.platform];
274
323
  for (const platform of platforms) {
275
- console.log(`\n🚀 Processing platform: ${platform.toUpperCase()}`);
324
+ console.log(`\n${colors.bold}${colors.magenta}🚀 Processing platform: ${platform.toUpperCase()}${colors.reset}`);
276
325
  // Find the JS bundle
277
326
  const bundlePath = findBundle(distDir, platform);
278
327
  if (!bundlePath) {
279
- console.warn(`⚠️ No bundle found for ${platform}. Skipping.`);
328
+ console.warn(`${colors.bold}${colors.yellow}⚠️ No bundle found for ${platform}. Skipping.${colors.reset}`);
280
329
  continue;
281
330
  }
282
331
  const bundleHash = await hashFile(bundlePath);
@@ -291,55 +340,50 @@ program
291
340
  assetCount: assets.length
292
341
  };
293
342
  const payloadStr = JSON.stringify(payloadObj);
294
- console.log("🔏 Signing payload with ECDSA P-256...");
343
+ console.log(`🔏 Signing payload with ECDSA P-256...`);
295
344
  const signature = await signPayload(payloadStr, privateKey);
296
- console.log(` Signature: ${signature.slice(0, 24)}…`);
345
+ console.log(` Signature: ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
297
346
  if (options.dryRun) {
298
- console.log("🏳️ Dry run — skipping upload.");
347
+ console.log(`${colors.bold}${colors.yellow}🏳️ Dry run — skipping upload.${colors.reset}`);
299
348
  console.log(" Payload:", payloadObj);
300
349
  continue;
301
350
  }
302
351
  // ── Step 4: Upload ─────────────────────────────────────────────────────
303
- // Pack the whole dist directory into a zip for server-side extraction
304
352
  const zipPath = path.resolve(cwd, `update-bundle-${platform}.zip`);
305
- console.log("🗜️ Zipping bundle...");
353
+ console.log(`🗜️ Zipping bundle...`);
306
354
  await zipDirectory(distDir, zipPath);
307
355
  const zipBuffer = fs.readFileSync(zipPath);
308
- // Build multipart/form-data manually (Node 18+ has FormData but
309
- // Blob.arrayBuffer is not always available; use Buffer directly).
310
356
  const form = new FormData();
311
357
  form.append("bundle", new Blob([zipBuffer], { type: "application/zip" }), `bundle-${platform}.zip`);
312
358
  form.append("payload", payloadStr);
313
359
  form.append("signature", signature);
314
360
  form.append("platform", platform);
315
- console.log(`📡 Uploading to ${config.serverUrl}/api/updates ...`);
361
+ console.log(`📡 Uploading to ${colors.cyan}${config.serverUrl}/api/updates${colors.reset} ...`);
316
362
  const response = await fetch(`${config.serverUrl}/api/updates`, {
317
363
  method: "POST",
318
364
  body: form
319
- // Note: do NOT set Content-Type; fetch sets it automatically
320
- // with the correct multipart boundary.
321
365
  });
322
366
  // Clean up zip
323
367
  fs.unlinkSync(zipPath);
324
368
  if (response.ok) {
325
369
  const body = await response.json().catch(() => ({}));
326
370
  const updateId = body.updateId || "unknown";
327
- console.log(`\n✅ ${platform.toUpperCase()} update deployed!`);
328
- console.log(` Update ID : ${updateId}`);
329
- console.log(` Channel : ${options.channel}`);
330
- console.log(` Runtime : ${options.runtime}`);
331
- console.log(` Bundle : ${bundleHash.slice(0, 16)}…`);
371
+ console.log(`\n${colors.bold}${colors.green}✔ ${platform.toUpperCase()} update deployed successfully!${colors.reset}`);
372
+ console.log(` ${colors.bold}Update ID :${colors.reset} ${colors.cyan}${updateId}${colors.reset}`);
373
+ console.log(` ${colors.bold}Channel :${colors.reset} ${colors.white}${options.channel}${colors.reset}`);
374
+ console.log(` ${colors.bold}Runtime :${colors.reset} ${colors.white}${options.runtime}${colors.reset}`);
375
+ console.log(` ${colors.bold}Bundle :${colors.reset} ${colors.gray}${bundleHash.slice(0, 16)}…${colors.reset}`);
332
376
  }
333
377
  else {
334
378
  const text = await response.text();
335
- console.error(`\n❌ Upload failed (HTTP ${response.status}): ${text}`);
379
+ console.error(`\n${colors.bold}${colors.red}❌ Upload failed (HTTP ${response.status}): ${text}${colors.reset}`);
336
380
  process.exit(1);
337
381
  }
338
382
  }
339
- console.log("\n🎉 Done. Your app will receive this update on the next OTA sync.\n" +
340
- "\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
341
- "\n🚀 Need scalable app development or custom Cloudflare setup?" +
342
- "\n👉 Partner with us at https://renbostudios.com to launch your next project!");
383
+ console.log(`\n${colors.bold}${colors.green}🎉 Done. Your app will receive this update on the next OTA sync.${colors.reset}\n` +
384
+ `\n✨ ${colors.bold}${colors.white}Powered by EdgeOTA${colors.reset} — A product of ${colors.magenta}Renbo Studios${colors.reset}` +
385
+ `\n🚀 Need scalable app development or custom Cloudflare setup?` +
386
+ `\n👉 Partner with us at ${colors.underline}${colors.cyan}https://renbostudios.com${colors.reset} to launch your next project!\n`);
343
387
  });
344
388
  // ──────────────────────────────────────────────────────
345
389
  // edge-ota status
@@ -349,16 +393,22 @@ program
349
393
  .description("List recent releases from the EdgeOTA server")
350
394
  .option("-t, --token <token>", "Auth token (or set EDGE_OTA_TOKEN env var)")
351
395
  .option("-n, --limit <n>", "Number of releases to show", "10")
396
+ .option("--project <id>", "Project ID (optional)")
352
397
  .action(async (options) => {
353
398
  const cwd = process.cwd();
354
399
  const config = loadConfig(cwd);
355
400
  const token = options.token || process.env.EDGE_OTA_TOKEN;
401
+ const projectId = options.project || config.projectId;
356
402
  if (!token) {
357
403
  console.error("❌ Auth token required. Use -t <token> or set EDGE_OTA_TOKEN.");
358
404
  process.exit(1);
359
405
  }
406
+ const headers = { Authorization: `Bearer ${token}` };
407
+ if (projectId) {
408
+ headers["x-project-id"] = projectId;
409
+ }
360
410
  const res = await fetch(`${config.serverUrl}/api/releases`, {
361
- headers: { Authorization: `Bearer ${token}` }
411
+ headers
362
412
  });
363
413
  if (!res.ok) {
364
414
  console.error(`❌ Failed to fetch releases (HTTP ${res.status})`);
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
7
7
  "author": "EdgeOTA Contributors",
8
- "homepage": "https://github.com/Renbo-Studios/edge-ota",
8
+ "homepage": "https://github.com/renbostudios/edge-ota",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/Renbo-Studios/edge-ota.git"
11
+ "url": "https://github.com/renbostudios/edge-ota.git",
12
+ "directory": "apps/cli"
12
13
  },
13
14
  "bugs": {
14
- "url": "https://github.com/Renbo-Studios/edge-ota/issues"
15
+ "url": "https://github.com/renbostudios/edge-ota/issues"
15
16
  },
16
17
  "keywords": ["expo", "ota", "updates", "react-native", "cli", "edge", "cloudflare"],
17
18
  "type": "module",