@renbostudios/edge-ota 0.2.1 → 0.2.3
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 +92 -45
- package/package.json +13 -5
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
|
|
@@ -259,42 +289,60 @@ program
|
|
|
259
289
|
.option("-r, --runtime <version>", "React Native runtime version (semver)", "1.0.0")
|
|
260
290
|
.option("-p, --platform <platform>", "Target platform: ios | android | all", "all")
|
|
261
291
|
.option("--project <id>", "Project ID (optional, overrides config)")
|
|
292
|
+
.option("-t, --token <token>", "Deploy key token (or set EDGE_OTA_TOKEN env var)")
|
|
262
293
|
.option("--skip-export", "Skip expo export (use existing ./dist directory)")
|
|
263
294
|
.option("--dry-run", "Build and sign the payload but do NOT upload")
|
|
264
295
|
.action(async (options) => {
|
|
265
296
|
const cwd = process.cwd();
|
|
266
297
|
const config = loadConfig(cwd);
|
|
267
298
|
const privateKey = loadPrivateKey(cwd);
|
|
299
|
+
// Resolve token — CLI flag > env var
|
|
300
|
+
const token = options.token || process.env.EDGE_OTA_TOKEN;
|
|
301
|
+
if (!token) {
|
|
302
|
+
console.error(`${colors.bold}${colors.red}❌ Auth token required. Use --token <token> or set EDGE_OTA_TOKEN env var.${colors.reset}`);
|
|
303
|
+
console.error(` Generate a deploy key from your dashboard → Keys page.`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
// Resolve project ID — CLI flag > config file
|
|
307
|
+
const projectId = options.project || config.projectId;
|
|
308
|
+
// Build the correct upload URL
|
|
309
|
+
const uploadUrl = projectId
|
|
310
|
+
? `${config.serverUrl}/api/projects/${projectId}/updates`
|
|
311
|
+
: `${config.serverUrl}/api/updates`;
|
|
268
312
|
// ── Step 1: Build ────────────────────────────────────────────────────────
|
|
313
|
+
printBanner();
|
|
269
314
|
const distDir = path.resolve(cwd, "dist");
|
|
270
315
|
if (!options.skipExport) {
|
|
271
|
-
console.log(
|
|
316
|
+
console.log(`${colors.bold}${colors.cyan}📦 Running expo export...${colors.reset}`);
|
|
272
317
|
try {
|
|
273
318
|
execSync("npx expo export", { stdio: "inherit", cwd });
|
|
274
319
|
}
|
|
275
320
|
catch {
|
|
276
|
-
console.error(
|
|
321
|
+
console.error(`${colors.bold}${colors.red}❌ expo export failed. Aborting.${colors.reset}`);
|
|
277
322
|
process.exit(1);
|
|
278
323
|
}
|
|
279
324
|
}
|
|
280
325
|
else {
|
|
281
|
-
console.log(
|
|
326
|
+
console.log(`${colors.bold}${colors.yellow}⏭️ Skipping expo export (--skip-export)${colors.reset}`);
|
|
282
327
|
}
|
|
283
328
|
if (!fs.existsSync(distDir)) {
|
|
284
|
-
console.error(
|
|
329
|
+
console.error(`${colors.bold}${colors.red}❌ ./dist directory not found after export. Aborting.${colors.reset}`);
|
|
285
330
|
process.exit(1);
|
|
286
331
|
}
|
|
287
332
|
// ── Step 2: Collect assets & hashes ─────────────────────────────────────
|
|
288
|
-
console.log(
|
|
333
|
+
console.log(`\n${colors.bold}${colors.cyan}🔍 Collecting assets and computing SHA-256 hashes...${colors.reset}`);
|
|
289
334
|
const assets = await collectAssets(distDir);
|
|
290
|
-
console.log(` Found ${assets.length} asset(s)`);
|
|
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
|
+
}
|
|
291
339
|
const platforms = options.platform === "all" ? ["ios", "android"] : [options.platform];
|
|
292
340
|
for (const platform of platforms) {
|
|
293
|
-
console.log(`\n🚀 Processing platform: ${platform.toUpperCase()}`);
|
|
341
|
+
console.log(`\n${colors.bold}${colors.magenta}🚀 Processing platform: ${platform.toUpperCase()}${colors.reset}`);
|
|
294
342
|
// Find the JS bundle
|
|
295
343
|
const bundlePath = findBundle(distDir, platform);
|
|
296
344
|
if (!bundlePath) {
|
|
297
|
-
console.warn(
|
|
345
|
+
console.warn(`${colors.bold}${colors.yellow}⚠️ No bundle found for ${platform}. Skipping.${colors.reset}`);
|
|
298
346
|
continue;
|
|
299
347
|
}
|
|
300
348
|
const bundleHash = await hashFile(bundlePath);
|
|
@@ -309,55 +357,54 @@ program
|
|
|
309
357
|
assetCount: assets.length
|
|
310
358
|
};
|
|
311
359
|
const payloadStr = JSON.stringify(payloadObj);
|
|
312
|
-
console.log(
|
|
360
|
+
console.log(`🔏 Signing payload with ECDSA P-256...`);
|
|
313
361
|
const signature = await signPayload(payloadStr, privateKey);
|
|
314
|
-
console.log(` Signature: ${signature.slice(0, 24)}
|
|
362
|
+
console.log(` Signature: ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
|
|
315
363
|
if (options.dryRun) {
|
|
316
|
-
console.log(
|
|
364
|
+
console.log(`${colors.bold}${colors.yellow}🏳️ Dry run — skipping upload.${colors.reset}`);
|
|
317
365
|
console.log(" Payload:", payloadObj);
|
|
318
366
|
continue;
|
|
319
367
|
}
|
|
320
368
|
// ── Step 4: Upload ─────────────────────────────────────────────────────
|
|
321
|
-
// Pack the whole dist directory into a zip for server-side extraction
|
|
322
369
|
const zipPath = path.resolve(cwd, `update-bundle-${platform}.zip`);
|
|
323
|
-
console.log(
|
|
370
|
+
console.log(`🗜️ Zipping bundle...`);
|
|
324
371
|
await zipDirectory(distDir, zipPath);
|
|
325
372
|
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
373
|
const form = new FormData();
|
|
329
374
|
form.append("bundle", new Blob([zipBuffer], { type: "application/zip" }), `bundle-${platform}.zip`);
|
|
330
375
|
form.append("payload", payloadStr);
|
|
331
376
|
form.append("signature", signature);
|
|
332
377
|
form.append("platform", platform);
|
|
333
|
-
console.log(`📡 Uploading to ${
|
|
334
|
-
const response = await fetch(
|
|
378
|
+
console.log(`📡 Uploading to ${colors.cyan}${uploadUrl}${colors.reset} ...`);
|
|
379
|
+
const response = await fetch(uploadUrl, {
|
|
335
380
|
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
"Authorization": `Bearer ${token}`,
|
|
383
|
+
...(projectId ? { "X-Project-Id": projectId } : {})
|
|
384
|
+
},
|
|
336
385
|
body: form
|
|
337
|
-
// Note: do NOT set Content-Type; fetch sets it automatically
|
|
338
|
-
// with the correct multipart boundary.
|
|
339
386
|
});
|
|
340
387
|
// Clean up zip
|
|
341
388
|
fs.unlinkSync(zipPath);
|
|
342
389
|
if (response.ok) {
|
|
343
390
|
const body = await response.json().catch(() => ({}));
|
|
344
391
|
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
|
|
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}`);
|
|
350
397
|
}
|
|
351
398
|
else {
|
|
352
399
|
const text = await response.text();
|
|
353
|
-
console.error(`\n❌ Upload failed (HTTP ${response.status}): ${text}`);
|
|
400
|
+
console.error(`\n${colors.bold}${colors.red}❌ Upload failed (HTTP ${response.status}): ${text}${colors.reset}`);
|
|
354
401
|
process.exit(1);
|
|
355
402
|
}
|
|
356
403
|
}
|
|
357
|
-
console.log(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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`);
|
|
361
408
|
});
|
|
362
409
|
// ──────────────────────────────────────────────────────
|
|
363
410
|
// edge-ota status
|
package/package.json
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renbostudios/edge-ota",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/renbostudios/edge-ota/issues"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"expo",
|
|
18
|
+
"ota",
|
|
19
|
+
"updates",
|
|
20
|
+
"react-native",
|
|
21
|
+
"cli",
|
|
22
|
+
"edge",
|
|
23
|
+
"cloudflare"
|
|
24
|
+
],
|
|
17
25
|
"type": "module",
|
|
18
26
|
"bin": {
|
|
19
27
|
"edge-ota": "./dist/index.js"
|