@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.
- package/dist/index.js +87 -95
- 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
|
-
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(`
|
|
56
|
-
console.log(`
|
|
57
|
-
console.log(
|
|
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(
|
|
110
|
+
console.log(` ${colors.dim}app.json${colors.reset} updates.url set`);
|
|
97
111
|
}
|
|
98
112
|
catch (error) {
|
|
99
|
-
console.error(
|
|
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(
|
|
217
|
-
const answer = await askQuestion(
|
|
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(
|
|
223
|
-
const hostedAnswer = await askQuestion(
|
|
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
|
-
|
|
232
|
-
|
|
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(
|
|
250
|
+
console.log(` ${colors.dim}generating ECDSA P-256 key pair...${colors.reset}`);
|
|
240
251
|
const keys = await generateECDSAKeyPair();
|
|
241
|
-
const config = {
|
|
242
|
-
|
|
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
|
-
|
|
270
|
-
console.log(
|
|
271
|
-
console.log(
|
|
272
|
-
console.log(
|
|
273
|
-
console.log(
|
|
274
|
-
console.log(
|
|
275
|
-
console.log(
|
|
276
|
-
console.log(
|
|
277
|
-
console.log(`${colors.
|
|
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(
|
|
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(
|
|
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(
|
|
327
|
+
console.log(` ${colors.dim}skipping expo export (--skip-export)${colors.reset}`);
|
|
327
328
|
}
|
|
328
329
|
if (!fs.existsSync(distDir)) {
|
|
329
|
-
console.error(
|
|
330
|
+
console.error(` ${colors.red}error${colors.reset} ./dist not found after export`);
|
|
330
331
|
process.exit(1);
|
|
331
332
|
}
|
|
332
|
-
|
|
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(`
|
|
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${
|
|
342
|
-
|
|
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(
|
|
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(
|
|
357
|
+
console.log(` signing ECDSA P-256`);
|
|
361
358
|
const signature = await signPayload(payloadStr, privateKey);
|
|
362
|
-
console.log(`
|
|
359
|
+
console.log(` sig ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
|
|
363
360
|
if (options.dryRun) {
|
|
364
|
-
console.log(
|
|
365
|
-
console.log("
|
|
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(
|
|
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(
|
|
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(
|
|
393
|
-
console.log(`
|
|
394
|
-
console.log(`
|
|
395
|
-
console.log(`
|
|
396
|
-
console.log(`
|
|
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(
|
|
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.
|
|
405
|
-
|
|
406
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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