@renbostudios/edge-ota 0.1.0 → 0.2.1

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 (3) hide show
  1. package/README.md +9 -3
  2. package/dist/index.js +109 -14
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -30,15 +30,19 @@ edge-ota status
30
30
 
31
31
  ### `edge-ota init [options]`
32
32
 
33
- Generates an ECDSA P-256 key pair, writes `edge-ota.config.json` and `.edge-ota.private.key`, and auto-adds the private key to `.gitignore`.
33
+ Generates an ECDSA P-256 key pair, writes `edge-ota.config.json` and `.edge-ota.private.key`, auto-adds the private key to `.gitignore`, and automatically updates your Expo `app.json` configuration file with the new updates URL.
34
+
35
+ If `--server` is not specified, you will be prompted interactively to enter the URL of your hosted or local EdgeOTA server and your optional Project ID.
34
36
 
35
37
  | Option | Default | Description |
36
38
  |--------|---------|-------------|
37
- | `-s, --server <url>` | `http://localhost:3000` | Your EdgeOTA server URL |
39
+ | `-s, --server <url>` | — | Your EdgeOTA server URL (prompts if omitted) |
40
+ | `--project <id>` | — | Your EdgeOTA Project ID (prompts if omitted, optional) |
38
41
 
39
42
  **Output:**
40
- - `edge-ota.config.json` — server URL + public key (safe to commit)
43
+ - `edge-ota.config.json` — server URL, public key, and optional project ID (safe to commit)
41
44
  - `.edge-ota.private.key` — private signing key (**never commit this**)
45
+ - **Auto-configured:** `app.json` is modified to point `expo.updates.url` to your server's updates endpoint. If a Project ID is configured, it points to `/api/projects/:projectId/updates`; otherwise, it falls back to the global `/api/updates` endpoint.
42
46
 
43
47
  ---
44
48
 
@@ -51,6 +55,7 @@ Runs `expo export`, hashes every asset with SHA-256, signs the payload with your
51
55
  | `-c, --channel <name>` | `production` | Deployment channel |
52
56
  | `-r, --runtime <version>` | `1.0.0` | Native runtime version (must match `runtimeVersion` in app.json) |
53
57
  | `-p, --platform <platform>` | `all` | `ios`, `android`, or `all` |
58
+ | `--project <id>` | — | Project ID (overrides value in `edge-ota.config.json`) |
54
59
  | `--skip-export` | — | Skip `expo export` — use existing `./dist` |
55
60
  | `--dry-run` | — | Sign the payload but don't upload |
56
61
 
@@ -72,6 +77,7 @@ Lists recent releases from the server as a table.
72
77
  |--------|---------|-------------|
73
78
  | `-t, --token <token>` | `$EDGE_OTA_TOKEN` | Auth token from the dashboard |
74
79
  | `-n, --limit <n>` | `10` | Number of releases to show |
80
+ | `--project <id>` | — | Project ID to query (overrides config) |
75
81
 
76
82
  ```bash
77
83
  export EDGE_OTA_TOKEN="your-dashboard-token"
package/dist/index.js CHANGED
@@ -30,7 +30,46 @@ import fs from "fs";
30
30
  import path from "path";
31
31
  import { createWriteStream } from "fs";
32
32
  import archiver from "archiver";
33
+ import readline from "readline";
33
34
  import { generateECDSAKeyPair, signPayload, sha256Hex } from "./core/index.js";
35
+ function askQuestion(query) {
36
+ const rl = readline.createInterface({
37
+ input: process.stdin,
38
+ output: process.stdout
39
+ });
40
+ return new Promise((resolve) => {
41
+ rl.question(query, (answer) => {
42
+ rl.close();
43
+ resolve(answer.trim());
44
+ });
45
+ });
46
+ }
47
+ function updateAppJson(cwd, serverUrl, projectId) {
48
+ const appJsonPath = path.resolve(cwd, "app.json");
49
+ if (!fs.existsSync(appJsonPath)) {
50
+ console.log("⚠️ app.json not found in this directory. Skipping auto-update.");
51
+ return;
52
+ }
53
+ try {
54
+ const raw = fs.readFileSync(appJsonPath, "utf-8");
55
+ const data = JSON.parse(raw);
56
+ if (!data.expo) {
57
+ data.expo = {};
58
+ }
59
+ if (!data.expo.updates) {
60
+ data.expo.updates = {};
61
+ }
62
+ const updateUrl = projectId
63
+ ? `${serverUrl.replace(/\/$/, "")}/api/projects/${projectId}/updates`
64
+ : `${serverUrl.replace(/\/$/, "")}/api/updates`;
65
+ data.expo.updates.url = updateUrl;
66
+ fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
67
+ console.log(`✅ Updated app.json: expo.updates.url set to "${updateUrl}"`);
68
+ }
69
+ catch (error) {
70
+ console.error(`❌ Failed to update app.json: ${error.message}`);
71
+ }
72
+ }
34
73
  function loadConfig(cwd) {
35
74
  const configPath = path.resolve(cwd, "edge-ota.config.json");
36
75
  if (!fs.existsSync(configPath)) {
@@ -66,6 +105,7 @@ async function hashFile(filePath) {
66
105
  }
67
106
  const CONTENT_TYPES = {
68
107
  ".js": "application/javascript",
108
+ ".hbc": "application/javascript",
69
109
  ".json": "application/json",
70
110
  ".png": "image/png",
71
111
  ".jpg": "image/jpeg",
@@ -105,19 +145,22 @@ async function collectAssets(distDir) {
105
145
  }
106
146
  // ─── Detect the JS bundle within the Expo export output ──────────────────────
107
147
  function findBundle(distDir, platform) {
108
- // Expo SDK 50+ writes: dist/_expo/static/js/<platform>/<hash>.js
148
+ // Expo SDK 50+ writes: dist/_expo/static/js/<platform>/<hash>.js (or .hbc)
109
149
  const expoStaticJs = path.join(distDir, "_expo", "static", "js", platform);
110
150
  if (fs.existsSync(expoStaticJs)) {
111
- const files = fs.readdirSync(expoStaticJs).filter(f => f.endsWith(".js"));
151
+ const files = fs.readdirSync(expoStaticJs).filter(f => f.endsWith(".js") || f.endsWith(".hbc"));
112
152
  if (files.length)
113
153
  return path.join(expoStaticJs, files[0]);
114
154
  }
115
155
  // Fallback: older SDK flat layout
116
- const flat = path.join(distDir, `index.${platform}.js`);
117
- if (fs.existsSync(flat))
118
- return flat;
119
- // Any .js at root
120
- const rootJs = fs.readdirSync(distDir).find(f => f.endsWith(".js"));
156
+ const flatJs = path.join(distDir, `index.${platform}.js`);
157
+ if (fs.existsSync(flatJs))
158
+ return flatJs;
159
+ const flatHbc = path.join(distDir, `index.${platform}.hbc`);
160
+ if (fs.existsSync(flatHbc))
161
+ return flatHbc;
162
+ // Any .js or .hbc at root
163
+ const rootJs = fs.readdirSync(distDir).find(f => f.endsWith(".js") || f.endsWith(".hbc"));
121
164
  if (rootJs)
122
165
  return path.join(distDir, rootJs);
123
166
  return null;
@@ -134,14 +177,45 @@ program
134
177
  program
135
178
  .command("init")
136
179
  .description("Generate an ECDSA P-256 key pair and write edge-ota.config.json")
137
- .option("-s, --server <url>", "EdgeOTA server URL", "http://localhost:3000")
180
+ .option("-s, --server <url>", "EdgeOTA server URL")
181
+ .option("--project <id>", "EdgeOTA Project ID (optional)")
138
182
  .action(async (options) => {
183
+ let serverUrl = options.server;
184
+ let projectId = options.project;
185
+ if (!serverUrl) {
186
+ console.log("ℹ️ Server URL not provided via -s/--server.");
187
+ const answer = await askQuestion("Enter your EdgeOTA server URL [http://localhost:3000]: ");
188
+ if (!answer) {
189
+ serverUrl = "http://localhost:3000";
190
+ }
191
+ else if (answer.toLowerCase().includes("host")) {
192
+ console.log("\nℹ️ Please set the server URL to your hosted EdgeOTA instance (e.g. https://ota.renbostudios.com)");
193
+ const hostedAnswer = await askQuestion("Hosted server URL: ");
194
+ serverUrl = hostedAnswer.trim() || "http://localhost:3000";
195
+ }
196
+ else {
197
+ serverUrl = answer;
198
+ }
199
+ }
200
+ if (!projectId && !options.server) {
201
+ console.log("\nℹ️ Project ID not provided via --project.");
202
+ const pAnswer = await askQuestion("Enter your EdgeOTA Project ID (optional, press Enter for single-tenant self-hosted): ");
203
+ if (pAnswer.trim()) {
204
+ projectId = pAnswer.trim();
205
+ }
206
+ }
207
+ // Normalise URL (remove trailing slashes)
208
+ serverUrl = serverUrl.replace(/\/$/, "");
139
209
  console.log("⚙️ Generating ECDSA P-256 key pair...");
140
210
  const keys = await generateECDSAKeyPair();
141
211
  const config = {
142
- serverUrl: options.server,
212
+ serverUrl: serverUrl,
143
213
  publicKey: keys.publicKey
144
214
  };
215
+ if (projectId) {
216
+ config.projectId = projectId;
217
+ }
218
+ const cwd = process.cwd();
145
219
  fs.writeFileSync("edge-ota.config.json", JSON.stringify(config, null, 2));
146
220
  fs.writeFileSync(".edge-ota.private.key", keys.privateKey, { mode: 0o600 });
147
221
  // Add the private key to .gitignore if it isn't already
@@ -157,15 +231,23 @@ program
157
231
  else {
158
232
  fs.writeFileSync(gitignorePath, `${gitignoreEntry}\n`);
159
233
  }
234
+ // Auto-update app.json updates.url
235
+ updateAppJson(cwd, serverUrl, projectId);
236
+ const targetUrlStr = projectId
237
+ ? `${serverUrl}/api/projects/${projectId}/updates`
238
+ : `${serverUrl}/api/updates`;
160
239
  console.log("\n✅ EdgeOTA initialised.");
161
240
  console.log(" Config : edge-ota.config.json");
162
241
  console.log(" Key : .edge-ota.private.key (keep this secret!)");
163
242
  console.log("\n📋 Public key to paste into your dashboard → Settings → Infrastructure:\n");
164
243
  console.log(keys.publicKey);
165
- console.log("\n💡 Add this to your app.json:\n" +
244
+ console.log("\n💡 Your app.json updates configuration has been configured to:\n" +
166
245
  ' "updates": {\n' +
167
- ' "url": "' + options.server + '/api/updates"\n' +
168
- " }");
246
+ ' "url": "' + targetUrlStr + '"\n' +
247
+ " }\n" +
248
+ "\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
249
+ "\n🚀 Need scalable app development or custom Cloudflare setup?" +
250
+ "\n👉 Partner with us at https://renbostudios.com to launch your next project!");
169
251
  });
170
252
  // ──────────────────────────────────────────────────────
171
253
  // edge-ota push
@@ -176,6 +258,7 @@ program
176
258
  .option("-c, --channel <channel>", "Deployment channel", "production")
177
259
  .option("-r, --runtime <version>", "React Native runtime version (semver)", "1.0.0")
178
260
  .option("-p, --platform <platform>", "Target platform: ios | android | all", "all")
261
+ .option("--project <id>", "Project ID (optional, overrides config)")
179
262
  .option("--skip-export", "Skip expo export (use existing ./dist directory)")
180
263
  .option("--dry-run", "Build and sign the payload but do NOT upload")
181
264
  .action(async (options) => {
@@ -272,7 +355,9 @@ program
272
355
  }
273
356
  }
274
357
  console.log("\n🎉 Done. Your app will receive this update on the next OTA sync.\n" +
275
- " Powered by EdgeOTA — https://github.com/edge-ota");
358
+ "\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
359
+ "\n🚀 Need scalable app development or custom Cloudflare setup?" +
360
+ "\n👉 Partner with us at https://renbostudios.com to launch your next project!");
276
361
  });
277
362
  // ──────────────────────────────────────────────────────
278
363
  // edge-ota status
@@ -282,16 +367,22 @@ program
282
367
  .description("List recent releases from the EdgeOTA server")
283
368
  .option("-t, --token <token>", "Auth token (or set EDGE_OTA_TOKEN env var)")
284
369
  .option("-n, --limit <n>", "Number of releases to show", "10")
370
+ .option("--project <id>", "Project ID (optional)")
285
371
  .action(async (options) => {
286
372
  const cwd = process.cwd();
287
373
  const config = loadConfig(cwd);
288
374
  const token = options.token || process.env.EDGE_OTA_TOKEN;
375
+ const projectId = options.project || config.projectId;
289
376
  if (!token) {
290
377
  console.error("❌ Auth token required. Use -t <token> or set EDGE_OTA_TOKEN.");
291
378
  process.exit(1);
292
379
  }
380
+ const headers = { Authorization: `Bearer ${token}` };
381
+ if (projectId) {
382
+ headers["x-project-id"] = projectId;
383
+ }
293
384
  const res = await fetch(`${config.serverUrl}/api/releases`, {
294
- headers: { Authorization: `Bearer ${token}` }
385
+ headers
295
386
  });
296
387
  if (!res.ok) {
297
388
  console.error(`❌ Failed to fetch releases (HTTP ${res.status})`);
@@ -311,6 +402,8 @@ program
311
402
  "By": r.publisher
312
403
  }));
313
404
  console.table(rows);
405
+ console.log("\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
406
+ "\n👉 Need scalable app development or cloud setup? Visit us at https://renbostudios.com");
314
407
  });
315
408
  // ──────────────────────────────────────────────────────
316
409
  // edge-ota keygen
@@ -324,5 +417,7 @@ program
324
417
  console.log(keys.privateKey);
325
418
  console.log("\n--- PUBLIC KEY (paste into dashboard) ---");
326
419
  console.log(keys.publicKey);
420
+ console.log("\n✨ Powered by EdgeOTA — A product of Renbo Studios" +
421
+ "\n👉 Need scalable app development or cloud setup? Visit us at https://renbostudios.com");
327
422
  });
328
423
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
+ "preferGlobal": true,
4
5
  "description": "Zero-SDK, self-hostable OTA update CLI for Expo — push signed JS bundles to any EdgeOTA server",
5
6
  "license": "MIT",
6
7
  "author": "EdgeOTA Contributors",