@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.
- package/dist/index.js +89 -96
- 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,61 +314,57 @@ 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,
|
|
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(
|
|
358
|
+
console.log(` signing ECDSA P-256`);
|
|
361
359
|
const signature = await signPayload(payloadStr, privateKey);
|
|
362
|
-
console.log(`
|
|
360
|
+
console.log(` sig ${colors.dim}${signature.slice(0, 24)}…${colors.reset}`);
|
|
363
361
|
if (options.dryRun) {
|
|
364
|
-
console.log(
|
|
365
|
-
console.log("
|
|
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(
|
|
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(
|
|
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(
|
|
393
|
-
console.log(`
|
|
394
|
-
console.log(`
|
|
395
|
-
console.log(`
|
|
396
|
-
console.log(`
|
|
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(
|
|
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.
|
|
405
|
-
|
|
406
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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