@renbostudios/edge-ota 0.2.8 → 0.2.9

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.
Files changed (2) hide show
  1. package/dist/index.js +162 -38
  2. 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.2.8";
149
+ let packageVersion = "0.2.9";
150
150
  try {
151
151
  const pkgPath = path.join(__dirname, "..", "package.json");
152
152
  if (fs.existsSync(pkgPath)) {
@@ -284,6 +284,75 @@ function askSecret(query) {
284
284
  }
285
285
  });
286
286
  }
287
+ function selectOption(question, options) {
288
+ return new Promise((resolve) => {
289
+ if (!process.stdin.isTTY) {
290
+ console.log(` ${c.bold}${question}${c.reset}`);
291
+ options.forEach((opt, idx) => {
292
+ console.log(` [${idx + 1}] ${opt.label}`);
293
+ });
294
+ ask(` selection (1-${options.length}): `).then((choice) => {
295
+ const idx = parseInt(choice) - 1;
296
+ if (!isNaN(idx) && idx >= 0 && idx < options.length) {
297
+ resolve(options[idx].value);
298
+ }
299
+ else {
300
+ resolve(options[0].value);
301
+ }
302
+ });
303
+ return;
304
+ }
305
+ let cursor = 0;
306
+ const hideCursor = "\u001b[?25l";
307
+ const showCursor = "\u001b[?25h";
308
+ let hasRendered = false;
309
+ console.log(` ${c.bold}${question}${c.reset}`);
310
+ function render() {
311
+ if (hasRendered) {
312
+ for (let i = 0; i < options.length; i++) {
313
+ process.stdout.write("\r\u001b[K\u001b[A");
314
+ }
315
+ process.stdout.write("\r\u001b[K");
316
+ }
317
+ hasRendered = true;
318
+ options.forEach((opt, idx) => {
319
+ const isSelected = idx === cursor;
320
+ const marker = isSelected ? `${c.cyan}❯${c.reset}` : " ";
321
+ const text = isSelected ? `${c.bold}${c.cyan}${opt.label}${c.reset}` : `${c.dim}${opt.label}${c.reset}`;
322
+ process.stdout.write(` ${marker} ${text}\n`);
323
+ });
324
+ }
325
+ readline.emitKeypressEvents(process.stdin);
326
+ process.stdin.setRawMode(true);
327
+ process.stdin.resume();
328
+ process.stdout.write(hideCursor);
329
+ render();
330
+ function onKeypress(str, key) {
331
+ if (key.ctrl && key.name === "c") {
332
+ process.stdout.write(showCursor);
333
+ process.stdin.setRawMode(false);
334
+ process.stdin.pause();
335
+ process.exit(0);
336
+ }
337
+ if (key.name === "up" || key.name === "k") {
338
+ cursor = (cursor - 1 + options.length) % options.length;
339
+ render();
340
+ }
341
+ else if (key.name === "down" || key.name === "j") {
342
+ cursor = (cursor + 1) % options.length;
343
+ render();
344
+ }
345
+ else if (key.name === "return" || key.name === "enter") {
346
+ process.stdout.write(showCursor);
347
+ process.stdin.setRawMode(false);
348
+ process.stdin.removeListener("keypress", onKeypress);
349
+ process.stdin.pause();
350
+ resolve(options[cursor].value);
351
+ }
352
+ }
353
+ process.stdin.on("keypress", onKeypress);
354
+ });
355
+ }
287
356
  const CONTENT_TYPES = {
288
357
  ".js": "application/javascript",
289
358
  ".hbc": "application/javascript",
@@ -460,51 +529,106 @@ program
460
529
  const globalCfg = loadGlobalConfig();
461
530
  const serverUrl = (options.server || globalCfg?.serverUrl || "https://api.edge-ota.renbo.site").replace(/\/$/, "");
462
531
  const cwd = process.cwd();
463
- // Try to read existing app.json for project name suggestion
464
- let suggestedName = path.basename(cwd);
465
- try {
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;
532
+ // Fetch existing projects
533
+ spin.start("fetching existing projects from server");
534
+ let projects = [];
476
535
  try {
477
536
  const res = await fetch(`${serverUrl}/api/projects`, {
478
- method: "POST",
479
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
480
- body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
537
+ headers: { Authorization: `Bearer ${token}` }
481
538
  });
482
- if (!res.ok) {
483
- const text = await res.text();
484
- spin.fail(`Failed to register project: ${text}`);
485
- process.exit(1);
539
+ if (res.ok) {
540
+ projects = await res.json();
486
541
  }
487
- const data = await res.json();
488
- projectId = data.id;
489
- spin.stop("project registered");
542
+ spin.stop();
490
543
  }
491
544
  catch (e) {
492
- spin.fail(`Connection error: ${e.message}`);
493
- process.exit(1);
545
+ spin.fail(`Failed to fetch projects: ${e.message}`);
546
+ // Continue with creation if list fails
547
+ }
548
+ let projectId = null;
549
+ let projectName = "";
550
+ if (projects.length > 0) {
551
+ const options = projects.map(p => ({
552
+ label: `${p.name} ${c.dim}(id: ${p.id.slice(0, 8)}...)${c.reset}`,
553
+ value: p
554
+ }));
555
+ options.push({
556
+ label: `${c.green}Create a new project...${c.reset}`,
557
+ value: null
558
+ });
559
+ const selected = await selectOption("Select a project to associate with this app:", options);
560
+ if (selected) {
561
+ projectId = selected.id;
562
+ projectName = selected.name;
563
+ }
564
+ }
565
+ const keys = await generateECDSAKeyPair();
566
+ if (projectId) {
567
+ // Re-initialize existing project: update public key on the server
568
+ spin.start(`associating project "${projectName}"`);
569
+ try {
570
+ const res = await fetch(`${serverUrl}/api/projects/${projectId}`, {
571
+ method: "PUT",
572
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
573
+ body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
574
+ });
575
+ if (!res.ok) {
576
+ const text = await res.text();
577
+ spin.fail(`Failed to update project public key on server: ${text}`);
578
+ process.exit(1);
579
+ }
580
+ spin.stop(`associated project "${projectName}"`);
581
+ }
582
+ catch (e) {
583
+ spin.fail(`Connection error: ${e.message}`);
584
+ process.exit(1);
585
+ }
586
+ }
587
+ else {
588
+ // Create new project
589
+ let suggestedName = path.basename(cwd);
590
+ try {
591
+ const data = JSON.parse(fs.readFileSync(path.resolve(cwd, "app.json"), "utf-8"));
592
+ suggestedName = data?.expo?.name || suggestedName;
593
+ }
594
+ catch { /* ignore */ }
595
+ projectName = (await ask(` ${c.dim}project name [${suggestedName}]:${c.reset} `)) || suggestedName;
596
+ spin.start("registering project on server");
597
+ try {
598
+ const res = await fetch(`${serverUrl}/api/projects`, {
599
+ method: "POST",
600
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
601
+ body: JSON.stringify({ name: projectName, publicKey: keys.publicKey }),
602
+ });
603
+ if (!res.ok) {
604
+ const text = await res.text();
605
+ spin.fail(`Failed to register project: ${text}`);
606
+ process.exit(1);
607
+ }
608
+ const data = await res.json();
609
+ projectId = data.id;
610
+ spin.stop("project registered");
611
+ }
612
+ catch (e) {
613
+ spin.fail(`Connection error: ${e.message}`);
614
+ process.exit(1);
615
+ }
494
616
  }
495
617
  // Save private key globally
496
- savePrivateKey(projectId, keys.privateKey);
497
- // Update app.json
498
- updateAppJson(cwd, serverUrl, projectId, keys.publicKey);
499
- const sep = `${c.dim}${"─".repeat(52)}${c.reset}`;
500
- console.log(`\n${sep}`);
501
- console.log(` ${c.green}✓${c.reset} ${c.bold}Initialised${c.reset}`);
502
- console.log(sep);
503
- console.log(` project ${c.dim}${projectId}${c.reset}`);
504
- console.log(` server ${c.dim}${serverUrl}${c.reset}`);
505
- console.log(` key file ${c.dim}${path.join(GLOBAL_KEYS_DIR, `${projectId}.key`)}${c.reset}`);
506
- console.log(sep + "\n");
507
- console.log(` ${c.dim}Next step:${c.reset} run ${c.cyan}edge-ota push${c.reset} to publish your first update.\n`);
618
+ if (projectId) {
619
+ savePrivateKey(projectId, keys.privateKey);
620
+ // Update app.json
621
+ updateAppJson(cwd, serverUrl, projectId, keys.publicKey);
622
+ const sep = `${c.dim}${"─".repeat(52)}${c.reset}`;
623
+ console.log(`\n${sep}`);
624
+ console.log(` ${c.green}✓${c.reset} ${c.bold}Initialised${c.reset}`);
625
+ console.log(sep);
626
+ console.log(` project ${c.dim}${projectId}${c.reset}`);
627
+ console.log(` server ${c.dim}${serverUrl}${c.reset}`);
628
+ console.log(` key file ${c.dim}${path.join(GLOBAL_KEYS_DIR, `${projectId}.key`)}${c.reset}`);
629
+ console.log(sep + "\n");
630
+ console.log(` ${c.dim}Next step:${c.reset} run ${c.cyan}edge-ota push${c.reset} to publish your first update.\n`);
631
+ }
508
632
  });
509
633
  // ──────────────────────────────────────────────────────────────────────────────
510
634
  // edge-ota push
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
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",