@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.
- package/README.md +9 -3
- package/dist/index.js +109 -14
- 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`,
|
|
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>` |
|
|
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
|
|
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
|
|
117
|
-
if (fs.existsSync(
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
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"
|
|
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:
|
|
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💡
|
|
244
|
+
console.log("\n💡 Your app.json updates configuration has been configured to:\n" +
|
|
166
245
|
' "updates": {\n' +
|
|
167
|
-
' "url": "' +
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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",
|