@fireota/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +76 -2
- package/bin/fireota.mjs +255 -0
- package/lib/package.mjs +26 -0
- package/package.json +38 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FireOTA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Sujay-Dynamisch/FireOTA/main/public/logo-mark.png" alt="FireOTA" width="96" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">FireOTA CLI</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Build FireOTA update packages and publish React Native over-the-air releases from a terminal or CI.<br />
|
|
9
|
+
Works with <a href="https://www.npmjs.com/package/@fireota/react-native"><code>@fireota/react-native</code></a>.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@fireota/cli"><img alt="@fireota/cli" src="https://img.shields.io/npm/v/@fireota/cli?label=%40fireota%2Fcli" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@fireota/react-native"><img alt="@fireota/react-native" src="https://img.shields.io/npm/v/@fireota/react-native?label=%40fireota%2Freact-native" /></a>
|
|
15
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-blue" />
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
> **Beta (0.x).** Requires Node.js 18.17+.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install --save-dev @fireota/cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Set two environment variables (create the API key in FireOTA → Settings → API keys):
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
FIREOTA_URL=https://ota.example.com
|
|
32
|
+
FIREOTA_API_KEY=fota_sk_live_...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Scopes: `release.write` (create, upload), `release.publish` (publish, pause, roll back), `rollout.write` (change rollout).
|
|
36
|
+
|
|
37
|
+
## Publish in one command
|
|
38
|
+
|
|
39
|
+
Run from your React Native project root:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npx fireota publish --app YOUR_APP_ID --platform android \
|
|
43
|
+
--version 1.3.1 --runtime 1.3.0 --channel production --rollout 10 --notes "Fix checkout crash"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This runs the React Native bundler, compiles Hermes bytecode when Hermes is enabled, packs the images next to the bundle, creates the release, uploads it to your Firebase Storage (checksum verified) and publishes it. Retries are safe: the same inputs never create a duplicate release.
|
|
47
|
+
|
|
48
|
+
- `--version` must be new for each release on a channel.
|
|
49
|
+
- `--runtime` must equal the runtime version of the store build that should receive it.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
fireota bundle --platform <android|ios> [--entry index.js] [--out update.fota] [--hermes auto|on|off]
|
|
55
|
+
fireota publish --app <appId> --platform <p> --version <x.y.z> --runtime <x.y.z>
|
|
56
|
+
[--channel production] [--rollout 10] [--notes "…"] [--mandatory] [--native ">=1.4.0"] [--file update.fota]
|
|
57
|
+
fireota release create --app <appId> --platform <p> --version <v> --runtime <r> [--channel c]
|
|
58
|
+
fireota release upload <releaseId> --file update.fota
|
|
59
|
+
fireota release publish <releaseId> [--rollout 100]
|
|
60
|
+
fireota release rollout <releaseId> --percentage <0-100>
|
|
61
|
+
fireota release pause <releaseId>
|
|
62
|
+
fireota release rollback <releaseId> --reason "…" [--target <releaseId>]
|
|
63
|
+
fireota releases [--app <appId>]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## GitHub Actions
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
- run: npx fireota publish --app ${{ vars.FIREOTA_APP_ID }} --platform android --version 1.3.${{ github.run_number }} --runtime 1.3.0 --channel beta
|
|
70
|
+
env:
|
|
71
|
+
FIREOTA_URL: ${{ vars.FIREOTA_URL }}
|
|
72
|
+
FIREOTA_API_KEY: ${{ secrets.FIREOTA_API_KEY }}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/bin/fireota.mjs
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FireOTA CLI — build update packages and manage releases from a terminal or CI.
|
|
4
|
+
* Needs Node 18+. Configuration via environment:
|
|
5
|
+
* FIREOTA_URL your FireOTA deployment, e.g. https://ota.example.com
|
|
6
|
+
* FIREOTA_API_KEY an API key from Settings -> API keys (fota_sk_...)
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { packDirectory } from "../lib/package.mjs";
|
|
15
|
+
|
|
16
|
+
const HELP = `FireOTA CLI
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
fireota bundle --platform <android|ios> [--entry index.js] [--out update.fota] [--hermes auto|on|off]
|
|
20
|
+
fireota publish --app <appId> --platform <android|ios> --version <x.y.z> --runtime <x.y.z>
|
|
21
|
+
[--channel production] [--rollout 10] [--notes "..."] [--mandatory] [--native ">=1.4.0"]
|
|
22
|
+
[--file update.fota] (builds the package first when --file is omitted)
|
|
23
|
+
fireota release create --app <appId> --platform <p> --version <v> --runtime <r> [--channel c] [--notes n] [--mandatory] [--native c]
|
|
24
|
+
fireota release upload <releaseId> --file update.fota
|
|
25
|
+
fireota release publish <releaseId> [--rollout 100]
|
|
26
|
+
fireota release rollout <releaseId> --percentage <0-100>
|
|
27
|
+
fireota release pause <releaseId>
|
|
28
|
+
fireota release rollback <releaseId> --reason "..." [--target <releaseId>]
|
|
29
|
+
fireota releases [--app <appId>]
|
|
30
|
+
|
|
31
|
+
Environment: FIREOTA_URL, FIREOTA_API_KEY. Run from your React Native project root.`;
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const positional = [];
|
|
35
|
+
const flags = {};
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
const arg = argv[i];
|
|
38
|
+
if (arg.startsWith("--")) {
|
|
39
|
+
const key = arg.slice(2);
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
if (next === undefined || next.startsWith("--")) flags[key] = true;
|
|
42
|
+
else {
|
|
43
|
+
flags[key] = next;
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
} else positional.push(arg);
|
|
47
|
+
}
|
|
48
|
+
return { positional, flags };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(message) {
|
|
52
|
+
console.error(`fireota: ${message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function need(flags, key) {
|
|
57
|
+
const value = flags[key];
|
|
58
|
+
if (value === undefined || value === true) fail(`--${key} is required`);
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sha256(buffer) {
|
|
63
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------- packaging ----------
|
|
67
|
+
|
|
68
|
+
function hermesEnabled(platform, mode) {
|
|
69
|
+
if (mode === "on") return true;
|
|
70
|
+
if (mode === "off") return false;
|
|
71
|
+
if (platform === "android") {
|
|
72
|
+
const props = existsSync("android/gradle.properties") ? readFileSync("android/gradle.properties", "utf8") : "";
|
|
73
|
+
return !/^\s*hermesEnabled\s*=\s*false/m.test(props);
|
|
74
|
+
}
|
|
75
|
+
const podfile = existsSync("ios/Podfile") ? readFileSync("ios/Podfile", "utf8") : "";
|
|
76
|
+
return !/:hermes_enabled\s*=>\s*false/.test(podfile);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hermesc() {
|
|
80
|
+
const dir = { win32: "win64-bin", darwin: "osx-bin", linux: "linux64-bin" }[process.platform];
|
|
81
|
+
const candidates = [
|
|
82
|
+
join("node_modules", "react-native", "sdks", "hermesc", dir ?? "", process.platform === "win32" ? "hermesc.exe" : "hermesc"),
|
|
83
|
+
join("node_modules", "hermes-compiler", "hermesc", dir ?? "", process.platform === "win32" ? "hermesc.exe" : "hermesc"),
|
|
84
|
+
];
|
|
85
|
+
const found = candidates.find((c) => existsSync(c));
|
|
86
|
+
if (!found) fail("Hermes is enabled but hermesc was not found in node_modules. Run npm install, or pass --hermes off.");
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Runs the React Native bundler (+ Hermes) and packs bundle and assets into one update file. */
|
|
91
|
+
function bundle(flags) {
|
|
92
|
+
const platform = need(flags, "platform");
|
|
93
|
+
if (!["android", "ios"].includes(platform)) fail("--platform must be android or ios");
|
|
94
|
+
if (!existsSync("package.json") || !existsSync(join("node_modules", "react-native"))) {
|
|
95
|
+
fail("Run this from your React Native project root (react-native must be installed).");
|
|
96
|
+
}
|
|
97
|
+
const entry = flags.entry ? String(flags.entry) : existsSync("index.js") ? "index.js" : "index.ts";
|
|
98
|
+
const bundleName = platform === "android" ? "index.android.bundle" : "main.jsbundle";
|
|
99
|
+
const work = mkdtempSync(join(tmpdir(), "fireota-"));
|
|
100
|
+
const out = join(work, "package");
|
|
101
|
+
try {
|
|
102
|
+
const jsOut = join(work, "bundle.js");
|
|
103
|
+
console.log(`Bundling ${entry} for ${platform}…`);
|
|
104
|
+
// Run the project's own React Native CLI through Node: no shell, same on every OS.
|
|
105
|
+
execFileSync(
|
|
106
|
+
process.execPath,
|
|
107
|
+
[join("node_modules", "react-native", "cli.js"), "bundle", "--platform", platform, "--dev", "false", "--minify", "false", "--entry-file", entry, "--bundle-output", jsOut, "--assets-dest", out],
|
|
108
|
+
{ stdio: "inherit" },
|
|
109
|
+
);
|
|
110
|
+
// The bundler only creates the assets folder when the app has images.
|
|
111
|
+
mkdirSync(out, { recursive: true });
|
|
112
|
+
if (hermesEnabled(platform, String(flags.hermes ?? "auto"))) {
|
|
113
|
+
console.log("Compiling to Hermes bytecode…");
|
|
114
|
+
execFileSync(hermesc(), ["-O", "-w", "-emit-binary", "-out", join(out, bundleName), jsOut], { stdio: "inherit" });
|
|
115
|
+
} else {
|
|
116
|
+
writeFileSync(join(out, bundleName), readFileSync(jsOut));
|
|
117
|
+
}
|
|
118
|
+
const packed = packDirectory(out);
|
|
119
|
+
const target = resolve(String(flags.out ?? `fireota-${platform}.fota`));
|
|
120
|
+
writeFileSync(target, packed);
|
|
121
|
+
console.log(`Package: ${target}\nSize: ${(packed.length / 1024 / 1024).toFixed(2)} MB\nSHA-256: ${sha256(packed)}`);
|
|
122
|
+
return target;
|
|
123
|
+
} finally {
|
|
124
|
+
rmSync(work, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------- API ----------
|
|
129
|
+
|
|
130
|
+
async function api(method, path, body, extraHeaders = {}) {
|
|
131
|
+
const base = process.env.FIREOTA_URL;
|
|
132
|
+
const key = process.env.FIREOTA_API_KEY;
|
|
133
|
+
if (!base) fail("Set FIREOTA_URL to your FireOTA deployment URL.");
|
|
134
|
+
if (!key) fail("Set FIREOTA_API_KEY (Settings -> API keys).");
|
|
135
|
+
const headers = { authorization: `Bearer ${key}`, ...extraHeaders };
|
|
136
|
+
let payload = body;
|
|
137
|
+
if (body && !(body instanceof FormData)) {
|
|
138
|
+
headers["content-type"] = "application/json";
|
|
139
|
+
payload = JSON.stringify(body);
|
|
140
|
+
}
|
|
141
|
+
const response = await fetch(`${base.replace(/\/+$/, "")}/api/public/v1${path}`, { method, headers, body: payload });
|
|
142
|
+
let json;
|
|
143
|
+
try {
|
|
144
|
+
json = await response.json();
|
|
145
|
+
} catch {
|
|
146
|
+
fail(`Unexpected response from FireOTA (HTTP ${response.status}).`);
|
|
147
|
+
}
|
|
148
|
+
if (!json.success) fail(`${json.error?.code ?? "ERROR"}: ${json.error?.message ?? "request failed"}`);
|
|
149
|
+
return json.data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function createRelease(flags) {
|
|
153
|
+
const body = {
|
|
154
|
+
appId: need(flags, "app"),
|
|
155
|
+
channel: String(flags.channel ?? "production"),
|
|
156
|
+
platform: need(flags, "platform"),
|
|
157
|
+
version: need(flags, "version"),
|
|
158
|
+
runtimeVersion: need(flags, "runtime"),
|
|
159
|
+
...(flags.notes ? { releaseNotes: String(flags.notes) } : {}),
|
|
160
|
+
...(flags.mandatory ? { mandatory: true } : {}),
|
|
161
|
+
...(flags.native ? { nativeVersionConstraint: String(flags.native) } : {}),
|
|
162
|
+
...(process.env.GITHUB_SHA ? { gitCommitSha: process.env.GITHUB_SHA } : {}),
|
|
163
|
+
...(process.env.GITHUB_REF_NAME ? { gitBranch: process.env.GITHUB_REF_NAME } : {}),
|
|
164
|
+
};
|
|
165
|
+
// Same inputs -> same release, so a retried CI job never creates duplicates.
|
|
166
|
+
const idempotencyKey = sha256(Buffer.from(JSON.stringify(body)));
|
|
167
|
+
const release = await api("POST", "/releases", body, { "idempotency-key": idempotencyKey });
|
|
168
|
+
console.log(`Release #${release.number} (${release.id}) — ${release.status}`);
|
|
169
|
+
return release;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function uploadRelease(releaseId, file) {
|
|
173
|
+
if (!existsSync(file)) fail(`File not found: ${file}`);
|
|
174
|
+
const bytes = readFileSync(file);
|
|
175
|
+
const form = new FormData();
|
|
176
|
+
form.append("bundle", new Blob([bytes], { type: "application/octet-stream" }), "update.fota");
|
|
177
|
+
console.log(`Uploading ${(bytes.length / 1024 / 1024).toFixed(2)} MB…`);
|
|
178
|
+
const result = await api("POST", `/releases/${releaseId}/bundle`, form, { "x-bundle-sha256": sha256(bytes) });
|
|
179
|
+
console.log(`Verified in your Firebase Storage. SHA-256 ${result.checksum}`);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function releaseAction(releaseId, body) {
|
|
184
|
+
return api("POST", `/releases/${releaseId}/publish`, body);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function main() {
|
|
188
|
+
const [command, sub, ...rest] = process.argv.slice(2);
|
|
189
|
+
const { positional, flags } = parseArgs([sub, ...rest].filter((v) => v !== undefined));
|
|
190
|
+
|
|
191
|
+
if (!command || command === "help" || command === "--help" || flags.help) {
|
|
192
|
+
console.log(HELP);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (command === "bundle") {
|
|
196
|
+
bundle(parseArgs(process.argv.slice(3)).flags);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (command === "releases") {
|
|
200
|
+
const query = flags.app ? `?appId=${encodeURIComponent(String(flags.app))}` : "";
|
|
201
|
+
const rows = await api("GET", `/releases${query}`);
|
|
202
|
+
for (const r of rows) {
|
|
203
|
+
console.log(`#${r.number}\t${r.platform}\t${r.version}\t${r.status}\t${r.rollout_percentage}%\t${r.id}`);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (command === "publish") {
|
|
208
|
+
const all = parseArgs(process.argv.slice(3)).flags;
|
|
209
|
+
const file = all.file ? String(all.file) : bundle(all);
|
|
210
|
+
const release = await createRelease(all);
|
|
211
|
+
await uploadRelease(release.id, file);
|
|
212
|
+
const result = await releaseAction(release.id, { action: "publish", rolloutPercentage: Number(all.rollout ?? 100) });
|
|
213
|
+
console.log(`Published #${release.number} at ${result.rollout}%.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (command === "release") {
|
|
217
|
+
const id = positional[1];
|
|
218
|
+
switch (positional[0]) {
|
|
219
|
+
case "create":
|
|
220
|
+
await createRelease(flags);
|
|
221
|
+
return;
|
|
222
|
+
case "upload":
|
|
223
|
+
await uploadRelease(id ?? fail("release id required"), need(flags, "file"));
|
|
224
|
+
return;
|
|
225
|
+
case "publish": {
|
|
226
|
+
const r = await releaseAction(id ?? fail("release id required"), { action: "publish", rolloutPercentage: Number(flags.rollout ?? 100) });
|
|
227
|
+
console.log(`Published at ${r.rollout}%.`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case "rollout": {
|
|
231
|
+
const r = await releaseAction(id ?? fail("release id required"), { action: "rollout", rolloutPercentage: Number(need(flags, "percentage")) });
|
|
232
|
+
console.log(`Rollout set to ${r.rollout}%.`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case "pause":
|
|
236
|
+
await releaseAction(id ?? fail("release id required"), { action: "pause" });
|
|
237
|
+
console.log("Paused.");
|
|
238
|
+
return;
|
|
239
|
+
case "rollback": {
|
|
240
|
+
const r = await releaseAction(id ?? fail("release id required"), {
|
|
241
|
+
action: "rollback",
|
|
242
|
+
reason: need(flags, "reason"),
|
|
243
|
+
...(flags.target ? { targetReleaseId: String(flags.target) } : {}),
|
|
244
|
+
});
|
|
245
|
+
console.log(r.restoredReleaseId ? `Rolled back; now serving ${r.restoredReleaseId}.` : "Rolled back; channel has no active release.");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
fail(`unknown release command: ${positional[0] ?? ""}\n\n${HELP}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
fail(`unknown command: ${command}\n\n${HELP}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
package/lib/package.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FireOTA update package: gzip( "FOTA1\n" | u32 big-endian header length | JSON {files:[{path,size}]} | file bytes ).
|
|
3
|
+
* The native SDK unpacks exactly this format (FireOTAStore.kt / FireOTA.m).
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join, relative, sep } from "node:path";
|
|
7
|
+
import { gzipSync } from "node:zlib";
|
|
8
|
+
|
|
9
|
+
function walk(dir, root = dir) {
|
|
10
|
+
return readdirSync(dir).flatMap((name) => {
|
|
11
|
+
const full = join(dir, name);
|
|
12
|
+
return statSync(full).isDirectory() ? walk(full, root) : [relative(root, full).split(sep).join("/")];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** gzip( "FOTA1\n" | u32 header length | JSON {files:[{path,size}]} | file bytes ) */
|
|
17
|
+
export function packDirectory(dir) {
|
|
18
|
+
const paths = walk(dir).sort();
|
|
19
|
+
const files = paths.map((path) => ({ path, size: statSync(join(dir, path)).size }));
|
|
20
|
+
const header = Buffer.from(JSON.stringify({ version: 1, files }), "utf8");
|
|
21
|
+
const length = Buffer.alloc(4);
|
|
22
|
+
length.writeUInt32BE(header.length);
|
|
23
|
+
const parts = [Buffer.from("FOTA1\n", "ascii"), length, header, ...paths.map((p) => readFileSync(join(dir, p)))];
|
|
24
|
+
return gzipSync(Buffer.concat(parts), { level: 9 });
|
|
25
|
+
}
|
|
26
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fireota/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Build FireOTA update packages and publish React Native OTA releases from a terminal or CI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"fireota": "bin/fireota.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.17"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react-native",
|
|
21
|
+
"ota",
|
|
22
|
+
"over-the-air",
|
|
23
|
+
"fireota",
|
|
24
|
+
"cli",
|
|
25
|
+
"ci"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"author": "FireOTA",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Sujay-Dynamisch/FireOTA.git",
|
|
34
|
+
"directory": "packages/cli"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Sujay-Dynamisch/FireOTA/tree/main/packages/cli#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Sujay-Dynamisch/FireOTA/issues"
|
|
39
|
+
}
|
|
40
|
+
}
|