@renbostudios/edge-ota 0.2.8 → 0.3.0
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 +189 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -146,7 +146,7 @@ const spin = (() => {
|
|
|
146
146
|
};
|
|
147
147
|
})();
|
|
148
148
|
// ─── Package version ─────────────────────────────────────────────────────────
|
|
149
|
-
let packageVersion = "0.
|
|
149
|
+
let packageVersion = "0.3.0";
|
|
150
150
|
try {
|
|
151
151
|
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
152
152
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -163,6 +163,8 @@ function printBanner() {
|
|
|
163
163
|
console.log(` ${c.dim}renbostudios.com${c.reset}`);
|
|
164
164
|
console.log(`${sep}\n`);
|
|
165
165
|
}
|
|
166
|
+
// ─── app.json helpers ────────────────────────────────────────────────────────
|
|
167
|
+
const DEFAULT_SERVER = "https://api.ota.renbo.site";
|
|
166
168
|
function readAppJson(cwd) {
|
|
167
169
|
const appJsonPath = path.resolve(cwd, "app.json");
|
|
168
170
|
if (!fs.existsSync(appJsonPath)) {
|
|
@@ -193,7 +195,7 @@ function readAppJson(cwd) {
|
|
|
193
195
|
console.error(` Run ${c.cyan}edge-ota init${c.reset} to set this up.\n`);
|
|
194
196
|
process.exit(1);
|
|
195
197
|
}
|
|
196
|
-
// Parse serverUrl and projectId from the updates URL
|
|
198
|
+
// Parse serverUrl and projectId from the updates URL.
|
|
197
199
|
// Expected format: https://<host>/api/projects/<uuid>/updates
|
|
198
200
|
// or: https://<host>/api/updates
|
|
199
201
|
let serverUrl;
|
|
@@ -213,7 +215,14 @@ function readAppJson(cwd) {
|
|
|
213
215
|
console.error(` ${c.red}✗${c.reset} Could not parse ${c.dim}expo.updates.url${c.reset} in app.json`);
|
|
214
216
|
process.exit(1);
|
|
215
217
|
}
|
|
216
|
-
|
|
218
|
+
// ── Project-level server override ──
|
|
219
|
+
// expo.extra.edgeOtaServer lets users point a specific project at a
|
|
220
|
+
// different (self-hosted) EdgeOTA instance without re-logging in.
|
|
221
|
+
const projectServer = expo?.extra?.edgeOtaServer;
|
|
222
|
+
if (projectServer) {
|
|
223
|
+
serverUrl = projectServer.replace(/\/$/, "");
|
|
224
|
+
}
|
|
225
|
+
return { serverUrl, projectId, runtimeVersion, publicKey: expo?.extra?.edgeOtaPublicKey };
|
|
217
226
|
}
|
|
218
227
|
function updateAppJson(cwd, serverUrl, projectId, publicKey) {
|
|
219
228
|
const appJsonPath = path.resolve(cwd, "app.json");
|
|
@@ -225,6 +234,8 @@ function updateAppJson(cwd, serverUrl, projectId, publicKey) {
|
|
|
225
234
|
data.expo = {};
|
|
226
235
|
if (!data.expo.updates)
|
|
227
236
|
data.expo.updates = {};
|
|
237
|
+
if (!data.expo.extra)
|
|
238
|
+
data.expo.extra = {};
|
|
228
239
|
const cleanUrl = serverUrl.replace(/\/$/, "");
|
|
229
240
|
data.expo.updates.url = projectId
|
|
230
241
|
? `${cleanUrl}/api/projects/${projectId}/updates`
|
|
@@ -232,13 +243,15 @@ function updateAppJson(cwd, serverUrl, projectId, publicKey) {
|
|
|
232
243
|
data.expo.updates.checkAutomatically = data.expo.updates.checkAutomatically ?? "ON_LOAD";
|
|
233
244
|
data.expo.updates.fallbackToCacheTimeout = data.expo.updates.fallbackToCacheTimeout ?? 30000;
|
|
234
245
|
data.expo.updates.requestHeaders = data.expo.updates.requestHeaders ?? { "expo-channel-name": "production" };
|
|
246
|
+
// Store the server URL in expo.extra.edgeOtaServer so users can easily
|
|
247
|
+
// override which EdgeOTA instance this project points to without re-running
|
|
248
|
+
// `edge-ota login`. They can edit this value directly in app.json.
|
|
249
|
+
data.expo.extra.edgeOtaServer = cleanUrl;
|
|
235
250
|
if (publicKey) {
|
|
236
|
-
data.expo.updates.codeSigningCertificate = undefined; // clear legacy
|
|
237
|
-
data.expo.extra = data.expo.extra ?? {};
|
|
238
251
|
data.expo.extra.edgeOtaPublicKey = publicKey;
|
|
239
252
|
}
|
|
240
253
|
fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
241
|
-
console.log(` ${c.dim}app.json${c.reset} updated ${c.dim}expo.updates.url${c.reset}`);
|
|
254
|
+
console.log(` ${c.dim}app.json${c.reset} updated ${c.dim}expo.updates.url${c.reset} + ${c.dim}expo.extra.edgeOtaServer${c.reset}`);
|
|
242
255
|
}
|
|
243
256
|
catch (e) {
|
|
244
257
|
console.error(` ${c.yellow}⚠${c.reset} Could not update app.json: ${e.message}`);
|
|
@@ -284,6 +297,75 @@ function askSecret(query) {
|
|
|
284
297
|
}
|
|
285
298
|
});
|
|
286
299
|
}
|
|
300
|
+
function selectOption(question, options) {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
if (!process.stdin.isTTY) {
|
|
303
|
+
console.log(` ${c.bold}${question}${c.reset}`);
|
|
304
|
+
options.forEach((opt, idx) => {
|
|
305
|
+
console.log(` [${idx + 1}] ${opt.label}`);
|
|
306
|
+
});
|
|
307
|
+
ask(` selection (1-${options.length}): `).then((choice) => {
|
|
308
|
+
const idx = parseInt(choice) - 1;
|
|
309
|
+
if (!isNaN(idx) && idx >= 0 && idx < options.length) {
|
|
310
|
+
resolve(options[idx].value);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
resolve(options[0].value);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
let cursor = 0;
|
|
319
|
+
const hideCursor = "\u001b[?25l";
|
|
320
|
+
const showCursor = "\u001b[?25h";
|
|
321
|
+
let hasRendered = false;
|
|
322
|
+
console.log(` ${c.bold}${question}${c.reset}`);
|
|
323
|
+
function render() {
|
|
324
|
+
if (hasRendered) {
|
|
325
|
+
for (let i = 0; i < options.length; i++) {
|
|
326
|
+
process.stdout.write("\r\u001b[K\u001b[A");
|
|
327
|
+
}
|
|
328
|
+
process.stdout.write("\r\u001b[K");
|
|
329
|
+
}
|
|
330
|
+
hasRendered = true;
|
|
331
|
+
options.forEach((opt, idx) => {
|
|
332
|
+
const isSelected = idx === cursor;
|
|
333
|
+
const marker = isSelected ? `${c.cyan}❯${c.reset}` : " ";
|
|
334
|
+
const text = isSelected ? `${c.bold}${c.cyan}${opt.label}${c.reset}` : `${c.dim}${opt.label}${c.reset}`;
|
|
335
|
+
process.stdout.write(` ${marker} ${text}\n`);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
readline.emitKeypressEvents(process.stdin);
|
|
339
|
+
process.stdin.setRawMode(true);
|
|
340
|
+
process.stdin.resume();
|
|
341
|
+
process.stdout.write(hideCursor);
|
|
342
|
+
render();
|
|
343
|
+
function onKeypress(str, key) {
|
|
344
|
+
if (key.ctrl && key.name === "c") {
|
|
345
|
+
process.stdout.write(showCursor);
|
|
346
|
+
process.stdin.setRawMode(false);
|
|
347
|
+
process.stdin.pause();
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
if (key.name === "up" || key.name === "k") {
|
|
351
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
352
|
+
render();
|
|
353
|
+
}
|
|
354
|
+
else if (key.name === "down" || key.name === "j") {
|
|
355
|
+
cursor = (cursor + 1) % options.length;
|
|
356
|
+
render();
|
|
357
|
+
}
|
|
358
|
+
else if (key.name === "return" || key.name === "enter") {
|
|
359
|
+
process.stdout.write(showCursor);
|
|
360
|
+
process.stdin.setRawMode(false);
|
|
361
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
362
|
+
process.stdin.pause();
|
|
363
|
+
resolve(options[cursor].value);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
process.stdin.on("keypress", onKeypress);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
287
369
|
const CONTENT_TYPES = {
|
|
288
370
|
".js": "application/javascript",
|
|
289
371
|
".hbc": "application/javascript",
|
|
@@ -393,11 +475,16 @@ program.action(() => {
|
|
|
393
475
|
program
|
|
394
476
|
.command("login")
|
|
395
477
|
.description("Authenticate with your EdgeOTA account")
|
|
396
|
-
.option("-s, --server <url>",
|
|
478
|
+
.option("-s, --server <url>", `EdgeOTA server URL (default: ${DEFAULT_SERVER})`)
|
|
397
479
|
.action(async (options) => {
|
|
398
480
|
printBanner();
|
|
399
|
-
|
|
400
|
-
|
|
481
|
+
const usingCustomServer = options.server && options.server !== DEFAULT_SERVER;
|
|
482
|
+
console.log(` ${c.bold}Sign in to EdgeOTA${c.reset}`);
|
|
483
|
+
if (usingCustomServer) {
|
|
484
|
+
console.log(` ${c.dim}server: ${options.server}${c.reset}`);
|
|
485
|
+
}
|
|
486
|
+
console.log();
|
|
487
|
+
const serverUrl = (options.server || DEFAULT_SERVER).replace(/\/$/, "");
|
|
401
488
|
const email = await ask(` ${c.dim}email:${c.reset} `);
|
|
402
489
|
const password = await askSecret(` ${c.dim}password:${c.reset} `);
|
|
403
490
|
spin.start("authenticating");
|
|
@@ -458,53 +545,108 @@ program
|
|
|
458
545
|
process.exit(1);
|
|
459
546
|
}
|
|
460
547
|
const globalCfg = loadGlobalConfig();
|
|
461
|
-
const serverUrl = (options.server || globalCfg?.serverUrl ||
|
|
548
|
+
const serverUrl = (options.server || globalCfg?.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
|
|
462
549
|
const cwd = process.cwd();
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const data = JSON.parse(fs.readFileSync(path.resolve(cwd, "app.json"), "utf-8"));
|
|
467
|
-
suggestedName = data?.expo?.name || suggestedName;
|
|
468
|
-
}
|
|
469
|
-
catch { /* ignore */ }
|
|
470
|
-
const projectName = (await ask(` ${c.dim}project name [${suggestedName}]:${c.reset} `)) || suggestedName;
|
|
471
|
-
spin.start("generating ECDSA P-256 key pair");
|
|
472
|
-
const keys = await generateECDSAKeyPair();
|
|
473
|
-
spin.stop("key pair generated");
|
|
474
|
-
spin.start("registering project on server");
|
|
475
|
-
let projectId;
|
|
550
|
+
// Fetch existing projects
|
|
551
|
+
spin.start("fetching existing projects from server");
|
|
552
|
+
let projects = [];
|
|
476
553
|
try {
|
|
477
554
|
const res = await fetch(`${serverUrl}/api/projects`, {
|
|
478
|
-
|
|
479
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
480
|
-
body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
|
|
555
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
481
556
|
});
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
spin.fail(`Failed to register project: ${text}`);
|
|
485
|
-
process.exit(1);
|
|
557
|
+
if (res.ok) {
|
|
558
|
+
projects = await res.json();
|
|
486
559
|
}
|
|
487
|
-
|
|
488
|
-
projectId = data.id;
|
|
489
|
-
spin.stop("project registered");
|
|
560
|
+
spin.stop();
|
|
490
561
|
}
|
|
491
562
|
catch (e) {
|
|
492
|
-
spin.fail(`
|
|
493
|
-
|
|
563
|
+
spin.fail(`Failed to fetch projects: ${e.message}`);
|
|
564
|
+
// Continue with creation if list fails
|
|
565
|
+
}
|
|
566
|
+
let projectId = null;
|
|
567
|
+
let projectName = "";
|
|
568
|
+
if (projects.length > 0) {
|
|
569
|
+
const options = projects.map(p => ({
|
|
570
|
+
label: `${p.name} ${c.dim}(id: ${p.id.slice(0, 8)}...)${c.reset}`,
|
|
571
|
+
value: p
|
|
572
|
+
}));
|
|
573
|
+
options.push({
|
|
574
|
+
label: `${c.green}Create a new project...${c.reset}`,
|
|
575
|
+
value: null
|
|
576
|
+
});
|
|
577
|
+
const selected = await selectOption("Select a project to associate with this app:", options);
|
|
578
|
+
if (selected) {
|
|
579
|
+
projectId = selected.id;
|
|
580
|
+
projectName = selected.name;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const keys = await generateECDSAKeyPair();
|
|
584
|
+
if (projectId) {
|
|
585
|
+
// Re-initialize existing project: update public key on the server
|
|
586
|
+
spin.start(`associating project "${projectName}"`);
|
|
587
|
+
try {
|
|
588
|
+
const res = await fetch(`${serverUrl}/api/projects/${projectId}`, {
|
|
589
|
+
method: "PUT",
|
|
590
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
591
|
+
body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
|
|
592
|
+
});
|
|
593
|
+
if (!res.ok) {
|
|
594
|
+
const text = await res.text();
|
|
595
|
+
spin.fail(`Failed to update project public key on server: ${text}`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
spin.stop(`associated project "${projectName}"`);
|
|
599
|
+
}
|
|
600
|
+
catch (e) {
|
|
601
|
+
spin.fail(`Connection error: ${e.message}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
// Create new project
|
|
607
|
+
let suggestedName = path.basename(cwd);
|
|
608
|
+
try {
|
|
609
|
+
const data = JSON.parse(fs.readFileSync(path.resolve(cwd, "app.json"), "utf-8"));
|
|
610
|
+
suggestedName = data?.expo?.name || suggestedName;
|
|
611
|
+
}
|
|
612
|
+
catch { /* ignore */ }
|
|
613
|
+
projectName = (await ask(` ${c.dim}project name [${suggestedName}]:${c.reset} `)) || suggestedName;
|
|
614
|
+
spin.start("registering project on server");
|
|
615
|
+
try {
|
|
616
|
+
const res = await fetch(`${serverUrl}/api/projects`, {
|
|
617
|
+
method: "POST",
|
|
618
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
619
|
+
body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
|
|
620
|
+
});
|
|
621
|
+
if (!res.ok) {
|
|
622
|
+
const text = await res.text();
|
|
623
|
+
spin.fail(`Failed to register project: ${text}`);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
const data = await res.json();
|
|
627
|
+
projectId = data.id;
|
|
628
|
+
spin.stop("project registered");
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
spin.fail(`Connection error: ${e.message}`);
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
494
634
|
}
|
|
495
635
|
// Save private key globally
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
636
|
+
if (projectId) {
|
|
637
|
+
savePrivateKey(projectId, keys.privateKey);
|
|
638
|
+
// Update app.json
|
|
639
|
+
updateAppJson(cwd, serverUrl, projectId, keys.publicKey);
|
|
640
|
+
const sep = `${c.dim}${"─".repeat(52)}${c.reset}`;
|
|
641
|
+
console.log(`\n${sep}`);
|
|
642
|
+
console.log(` ${c.green}✓${c.reset} ${c.bold}Initialised${c.reset}`);
|
|
643
|
+
console.log(sep);
|
|
644
|
+
console.log(` project ${c.dim}${projectId}${c.reset}`);
|
|
645
|
+
console.log(` server ${c.dim}${serverUrl}${c.reset}`);
|
|
646
|
+
console.log(` key file ${c.dim}${path.join(GLOBAL_KEYS_DIR, `${projectId}.key`)}${c.reset}`);
|
|
647
|
+
console.log(sep + "\n");
|
|
648
|
+
console.log(` ${c.dim}Next step:${c.reset} run ${c.cyan}edge-ota push${c.reset} to publish your first update.\n`);
|
|
649
|
+
}
|
|
508
650
|
});
|
|
509
651
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
510
652
|
// edge-ota push
|
package/package.json
CHANGED