@renbostudios/edge-ota 0.2.1 → 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.
- package/README.md +3 -3
- package/dist/index.js +70 -44
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `@
|
|
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 @
|
|
9
|
+
npm install -g @edge-ota/cli
|
|
10
10
|
# or use without installing:
|
|
11
|
-
npx @
|
|
11
|
+
npx @edge-ota/cli <command>
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
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,
|
|
@@ -180,16 +209,17 @@ program
|
|
|
180
209
|
.option("-s, --server <url>", "EdgeOTA server URL")
|
|
181
210
|
.option("--project <id>", "EdgeOTA Project ID (optional)")
|
|
182
211
|
.action(async (options) => {
|
|
212
|
+
printBanner();
|
|
183
213
|
let serverUrl = options.server;
|
|
184
214
|
let projectId = options.project;
|
|
185
215
|
if (!serverUrl) {
|
|
186
|
-
console.log(
|
|
216
|
+
console.log(`${colors.bold}${colors.cyan}ℹ️ Server URL not provided via -s/--server.${colors.reset}`);
|
|
187
217
|
const answer = await askQuestion("Enter your EdgeOTA server URL [http://localhost:3000]: ");
|
|
188
218
|
if (!answer) {
|
|
189
219
|
serverUrl = "http://localhost:3000";
|
|
190
220
|
}
|
|
191
221
|
else if (answer.toLowerCase().includes("host")) {
|
|
192
|
-
console.log(
|
|
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}`);
|
|
193
223
|
const hostedAnswer = await askQuestion("Hosted server URL: ");
|
|
194
224
|
serverUrl = hostedAnswer.trim() || "http://localhost:3000";
|
|
195
225
|
}
|
|
@@ -198,7 +228,7 @@ program
|
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
230
|
if (!projectId && !options.server) {
|
|
201
|
-
console.log(
|
|
231
|
+
console.log(`\n${colors.bold}${colors.cyan}ℹ️ Project ID not provided via --project.${colors.reset}`);
|
|
202
232
|
const pAnswer = await askQuestion("Enter your EdgeOTA Project ID (optional, press Enter for single-tenant self-hosted): ");
|
|
203
233
|
if (pAnswer.trim()) {
|
|
204
234
|
projectId = pAnswer.trim();
|
|
@@ -206,7 +236,7 @@ program
|
|
|
206
236
|
}
|
|
207
237
|
// Normalise URL (remove trailing slashes)
|
|
208
238
|
serverUrl = serverUrl.replace(/\/$/, "");
|
|
209
|
-
console.log(
|
|
239
|
+
console.log(`⚙️ Generating ECDSA P-256 key pair...`);
|
|
210
240
|
const keys = await generateECDSAKeyPair();
|
|
211
241
|
const config = {
|
|
212
242
|
serverUrl: serverUrl,
|
|
@@ -236,18 +266,18 @@ program
|
|
|
236
266
|
const targetUrlStr = projectId
|
|
237
267
|
? `${serverUrl}/api/projects/${projectId}/updates`
|
|
238
268
|
: `${serverUrl}/api/updates`;
|
|
239
|
-
console.log(
|
|
240
|
-
console.log(
|
|
241
|
-
console.log(
|
|
242
|
-
console.log(
|
|
243
|
-
console.log(
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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`);
|
|
251
281
|
});
|
|
252
282
|
// ──────────────────────────────────────────────────────
|
|
253
283
|
// edge-ota push
|
|
@@ -266,35 +296,36 @@ program
|
|
|
266
296
|
const config = loadConfig(cwd);
|
|
267
297
|
const privateKey = loadPrivateKey(cwd);
|
|
268
298
|
// ── Step 1: Build ────────────────────────────────────────────────────────
|
|
299
|
+
printBanner();
|
|
269
300
|
const distDir = path.resolve(cwd, "dist");
|
|
270
301
|
if (!options.skipExport) {
|
|
271
|
-
console.log(
|
|
302
|
+
console.log(`${colors.bold}${colors.cyan}📦 Running expo export...${colors.reset}`);
|
|
272
303
|
try {
|
|
273
304
|
execSync("npx expo export", { stdio: "inherit", cwd });
|
|
274
305
|
}
|
|
275
306
|
catch {
|
|
276
|
-
console.error(
|
|
307
|
+
console.error(`${colors.bold}${colors.red}❌ expo export failed. Aborting.${colors.reset}`);
|
|
277
308
|
process.exit(1);
|
|
278
309
|
}
|
|
279
310
|
}
|
|
280
311
|
else {
|
|
281
|
-
console.log(
|
|
312
|
+
console.log(`${colors.bold}${colors.yellow}⏭️ Skipping expo export (--skip-export)${colors.reset}`);
|
|
282
313
|
}
|
|
283
314
|
if (!fs.existsSync(distDir)) {
|
|
284
|
-
console.error(
|
|
315
|
+
console.error(`${colors.bold}${colors.red}❌ ./dist directory not found after export. Aborting.${colors.reset}`);
|
|
285
316
|
process.exit(1);
|
|
286
317
|
}
|
|
287
318
|
// ── Step 2: Collect assets & hashes ─────────────────────────────────────
|
|
288
|
-
console.log(
|
|
319
|
+
console.log(`\n${colors.bold}${colors.cyan}🔍 Collecting assets and computing SHA-256 hashes...${colors.reset}`);
|
|
289
320
|
const assets = await collectAssets(distDir);
|
|
290
|
-
console.log(` Found ${assets.length} asset(s)`);
|
|
321
|
+
console.log(` Found ${colors.bold}${colors.white}${assets.length}${colors.reset} asset(s)`);
|
|
291
322
|
const platforms = options.platform === "all" ? ["ios", "android"] : [options.platform];
|
|
292
323
|
for (const platform of platforms) {
|
|
293
|
-
console.log(`\n🚀 Processing platform: ${platform.toUpperCase()}`);
|
|
324
|
+
console.log(`\n${colors.bold}${colors.magenta}🚀 Processing platform: ${platform.toUpperCase()}${colors.reset}`);
|
|
294
325
|
// Find the JS bundle
|
|
295
326
|
const bundlePath = findBundle(distDir, platform);
|
|
296
327
|
if (!bundlePath) {
|
|
297
|
-
console.warn(
|
|
328
|
+
console.warn(`${colors.bold}${colors.yellow}⚠️ No bundle found for ${platform}. Skipping.${colors.reset}`);
|
|
298
329
|
continue;
|
|
299
330
|
}
|
|
300
331
|
const bundleHash = await hashFile(bundlePath);
|
|
@@ -309,55 +340,50 @@ program
|
|
|
309
340
|
assetCount: assets.length
|
|
310
341
|
};
|
|
311
342
|
const payloadStr = JSON.stringify(payloadObj);
|
|
312
|
-
console.log(
|
|
343
|
+
console.log(`🔏 Signing payload with ECDSA P-256...`);
|
|
313
344
|
const signature = await signPayload(payloadStr, privateKey);
|
|
314
|
-
console.log(` Signature: ${signature.slice(0, 24)}
|
|
345
|
+
console.log(` Signature: ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
|
|
315
346
|
if (options.dryRun) {
|
|
316
|
-
console.log(
|
|
347
|
+
console.log(`${colors.bold}${colors.yellow}🏳️ Dry run — skipping upload.${colors.reset}`);
|
|
317
348
|
console.log(" Payload:", payloadObj);
|
|
318
349
|
continue;
|
|
319
350
|
}
|
|
320
351
|
// ── Step 4: Upload ─────────────────────────────────────────────────────
|
|
321
|
-
// Pack the whole dist directory into a zip for server-side extraction
|
|
322
352
|
const zipPath = path.resolve(cwd, `update-bundle-${platform}.zip`);
|
|
323
|
-
console.log(
|
|
353
|
+
console.log(`🗜️ Zipping bundle...`);
|
|
324
354
|
await zipDirectory(distDir, zipPath);
|
|
325
355
|
const zipBuffer = fs.readFileSync(zipPath);
|
|
326
|
-
// Build multipart/form-data manually (Node 18+ has FormData but
|
|
327
|
-
// Blob.arrayBuffer is not always available; use Buffer directly).
|
|
328
356
|
const form = new FormData();
|
|
329
357
|
form.append("bundle", new Blob([zipBuffer], { type: "application/zip" }), `bundle-${platform}.zip`);
|
|
330
358
|
form.append("payload", payloadStr);
|
|
331
359
|
form.append("signature", signature);
|
|
332
360
|
form.append("platform", platform);
|
|
333
|
-
console.log(`📡 Uploading to ${config.serverUrl}/api/updates ...`);
|
|
361
|
+
console.log(`📡 Uploading to ${colors.cyan}${config.serverUrl}/api/updates${colors.reset} ...`);
|
|
334
362
|
const response = await fetch(`${config.serverUrl}/api/updates`, {
|
|
335
363
|
method: "POST",
|
|
336
364
|
body: form
|
|
337
|
-
// Note: do NOT set Content-Type; fetch sets it automatically
|
|
338
|
-
// with the correct multipart boundary.
|
|
339
365
|
});
|
|
340
366
|
// Clean up zip
|
|
341
367
|
fs.unlinkSync(zipPath);
|
|
342
368
|
if (response.ok) {
|
|
343
369
|
const body = await response.json().catch(() => ({}));
|
|
344
370
|
const updateId = body.updateId || "unknown";
|
|
345
|
-
console.log(`\n
|
|
346
|
-
console.log(` Update ID
|
|
347
|
-
console.log(` Channel
|
|
348
|
-
console.log(` Runtime
|
|
349
|
-
console.log(` Bundle
|
|
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}`);
|
|
350
376
|
}
|
|
351
377
|
else {
|
|
352
378
|
const text = await response.text();
|
|
353
|
-
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}`);
|
|
354
380
|
process.exit(1);
|
|
355
381
|
}
|
|
356
382
|
}
|
|
357
|
-
console.log(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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`);
|
|
361
387
|
});
|
|
362
388
|
// ──────────────────────────────────────────────────────
|
|
363
389
|
// edge-ota status
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renbostudios/edge-ota",
|
|
3
|
-
"version": "0.2.
|
|
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/
|
|
8
|
+
"homepage": "https://github.com/renbostudios/edge-ota",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/renbostudios/edge-ota.git",
|
|
12
|
+
"directory": "apps/cli"
|
|
12
13
|
},
|
|
13
14
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
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",
|