@bipopush/cli 0.2.0 → 0.3.0-beta.3

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 CHANGED
@@ -46,7 +46,7 @@ Published iOS 1.0 v7 to production.
46
46
 
47
47
  - The signing key is saved at `~/.config/bipopush/keys/<appId>.pem`. Keep it private — whoever has it can ship code to your users.
48
48
  - In CI set `BIPOPUSH_TOKEN` (a CLI token) and `BIPOPUSH_SIGNING_KEY` (the PEM, or its base64).
49
- - `bsdiff-nodejs` is an optional native dependency; without it releases still work, just without patches.
49
+ - Patches are made by `bsdiff-wasm` (Debian's bsdiff 4.3 compiled to WebAssembly, BSDIFF40): nothing is compiled on install.
50
50
 
51
51
  ## Environment
52
52
 
package/dist/commands.js CHANGED
@@ -155,7 +155,11 @@ export async function release(token, opts) {
155
155
  throw new Error(`No ${PROJECT_FILE} here. Run: bipopush init`);
156
156
  const project = detectProject(opts.cwd);
157
157
  const privateKey = loadSigningKey(config.appId);
158
- const { app } = await request(`/apps/${config.appId}`, { token });
158
+ const { app, plan } = await request(`/apps/${config.appId}`, { token });
159
+ // Same answer the API gives, before spending time on bundling and uploads.
160
+ if (opts.rollout !== undefined && opts.rollout < 100 && plan?.gradualRollouts === false) {
161
+ throw new Error("Gradual rollouts are on Plus and above. Release to everyone (no --rollout), or upgrade in the dashboard: Subscription.");
162
+ }
159
163
  const wanted = opts.platform === "all" ? ["ios", "android"] : [opts.platform];
160
164
  const platforms = wanted.filter((p) => project[p].id && project[p].version);
161
165
  if (platforms.length === 0)
@@ -197,9 +201,11 @@ export async function release(token, opts) {
197
201
  channel: opts.channel,
198
202
  message: opts.message,
199
203
  mandatory: Boolean(opts.mandatory),
204
+ ...(opts.rollout !== undefined && opts.rollout < 100 ? { rolloutPercent: opts.rollout } : {}),
200
205
  artifacts: packed.map((a) => ({
201
206
  platform: a.platform,
202
207
  appVersion: a.appVersion,
208
+ ...(project[a.platform].build ? { nativeBuild: project[a.platform].build } : {}),
203
209
  sha256: a.sha256,
204
210
  signature: a.signature,
205
211
  bundleSize: a.bundleSize,
@@ -217,7 +223,10 @@ export async function release(token, opts) {
217
223
  }
218
224
  const { release: published, usage } = await waitingForBurst(() => request(`/releases/${created.releaseId}/publish`, { method: "POST", token }));
219
225
  const labels = published.artifacts.map((a) => `${a.platform === "ios" ? "iOS" : "Android"} ${a.appVersion} v${a.version}`);
220
- console.log(`\nPublished ${labels.join(", ")} to ${opts.channel}${opts.mandatory ? " (mandatory)" : ""}.`);
226
+ const rolling = opts.rollout !== undefined && opts.rollout < 100 ? `, to ${opts.rollout}% of devices` : "";
227
+ console.log(`\nPublished ${labels.join(", ")} to ${opts.channel}${opts.mandatory ? " (mandatory)" : ""}${rolling}.`);
228
+ if (rolling)
229
+ console.log(`Raise it later with: bipopush rollout <percent> -c ${opts.channel} (or 100 to finish)`);
221
230
  console.log(`${usage.used} release${usage.used === 1 ? "" : "s"} of this app in the last 24h.`);
222
231
  }
223
232
  /** Waits out a release pause (going over the plan's releases per 10 minutes) instead of failing, e.g. in CI. */
@@ -270,6 +279,57 @@ export async function listApps(token) {
270
279
  }
271
280
  }
272
281
  }
282
+ /** The newest live version per platform and native version in a channel (or the given --number). */
283
+ async function newestLive(token, opts) {
284
+ const config = loadProjectConfig(opts.cwd);
285
+ if (!config)
286
+ throw new Error(`No ${PROJECT_FILE} here. Run: bipopush init`);
287
+ const { releases } = await request(`/apps/${config.appId}/releases`, { token });
288
+ const picked = new Map();
289
+ for (const a of releases.filter((r) => r.channel === opts.channel).flatMap((r) => r.artifacts)) {
290
+ if (a.disabled || a.fileDeleted || (opts.platform !== "all" && a.platform !== opts.platform))
291
+ continue;
292
+ if (opts.version !== undefined && a.version !== opts.version)
293
+ continue;
294
+ const key = `${a.platform}/${a.appVersion}`;
295
+ if (!picked.has(key) || picked.get(key).version < a.version)
296
+ picked.set(key, a);
297
+ }
298
+ if (picked.size === 0)
299
+ throw new Error(`No live ${opts.version ? `v${opts.version} ` : ""}version in ${opts.channel}`);
300
+ return { appId: config.appId, picked: [...picked.values()] };
301
+ }
302
+ const label = (a) => `${a.platform === "ios" ? "iOS" : "Android"} ${a.appVersion} v${a.version}`;
303
+ function patchArtifact(token, appId, artifactId, body) {
304
+ return request(`/apps/${appId}/artifacts/${artifactId}`, { method: "PATCH", token, headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
305
+ }
306
+ /** Changes how many devices get the newest version (100 finishes the rollout). */
307
+ export async function rollout(token, opts) {
308
+ const { appId, picked } = await newestLive(token, opts);
309
+ for (const a of picked) {
310
+ await patchArtifact(token, appId, a.id, { rolloutPercent: opts.percent });
311
+ console.log(`${label(a)} on ${opts.channel}: ${a.rolloutPercent}% → ${opts.percent}% of devices${opts.percent === 100 ? " (rollout complete)" : ""}`);
312
+ }
313
+ }
314
+ /** Disables the newest version of a channel (or --number): devices get the newest enabled one. */
315
+ export async function disable(token, opts) {
316
+ const { appId, picked } = await newestLive(token, opts);
317
+ for (const a of picked) {
318
+ await patchArtifact(token, appId, a.id, { disabled: true });
319
+ console.log(`Disabled ${label(a)} on ${opts.channel}. Devices get the newest enabled version on their next check.`);
320
+ }
321
+ }
322
+ /** Marks the newest version mandatory (or optional) and/or changes its rollout, like CodePush's patch. */
323
+ export async function patchRelease(token, opts) {
324
+ const { appId, picked } = await newestLive(token, opts);
325
+ for (const a of picked) {
326
+ if (opts.mandatory !== undefined)
327
+ await patchArtifact(token, appId, a.id, { mandatory: opts.mandatory });
328
+ if (opts.percent !== undefined)
329
+ await patchArtifact(token, appId, a.id, { rolloutPercent: opts.percent });
330
+ console.log(`Updated ${label(a)} on ${opts.channel}.`);
331
+ }
332
+ }
273
333
  /**
274
334
  * Re-publishes the newest live version of a channel (or a given --version) in another channel,
275
335
  * e.g. staging → production after testing. Same bundle and signature; does not use the daily quota.
package/dist/compat.js ADDED
@@ -0,0 +1,96 @@
1
+ import { parseArgs } from "node:util";
2
+ /** CodePush deployments are "Staging" and "Production"; BipoPush channels are lower-case. */
3
+ const channelOf = (deployment) => deployment.trim().toLowerCase();
4
+ export function percent(value, flag) {
5
+ if (value === undefined)
6
+ return undefined;
7
+ const n = Number(value.replace(/%$/, ""));
8
+ if (!Number.isInteger(n) || n < 1 || n > 100)
9
+ throw new Error(`${flag} must be a whole number from 1 to 100`);
10
+ return n;
11
+ }
12
+ const platformOf = (value) => {
13
+ const p = (value ?? "all").toLowerCase();
14
+ if (p !== "ios" && p !== "android" && p !== "all")
15
+ throw new Error("Platform must be ios, android or all");
16
+ return p;
17
+ };
18
+ /** Returns the mapped command, or null when `args` isn't a CodePush / EAS command line. */
19
+ export function parseCompat(argv) {
20
+ const args = argv[0] === "codepush" ? argv.slice(1) : argv;
21
+ const codepush = argv[0] === "codepush";
22
+ const command = args[0];
23
+ if (command === "release-react") {
24
+ const { positionals, values } = parseArgs({
25
+ args: args.slice(1),
26
+ allowPositionals: true,
27
+ strict: false,
28
+ options: {
29
+ deploymentName: { type: "string", short: "d" },
30
+ description: { type: "string" },
31
+ des: { type: "string" },
32
+ mandatory: { type: "boolean", short: "m" },
33
+ rollout: { type: "string", short: "r" },
34
+ app: { type: "string", short: "a" },
35
+ targetBinaryVersion: { type: "string", short: "t" },
36
+ },
37
+ });
38
+ // code-push release-react <app> <platform>; appcenter codepush release-react -a owner/app (platform from the project).
39
+ const platform = codepush ? positionals[1] ?? positionals[0] : positionals[1];
40
+ return {
41
+ kind: "release",
42
+ platform: platformOf(platform === undefined || /\//.test(platform) ? undefined : platform),
43
+ channel: channelOf(typeof values.deploymentName === "string" ? values.deploymentName : "Staging"),
44
+ message: (typeof values.description === "string" ? values.description : undefined) ?? (typeof values.des === "string" ? values.des : undefined),
45
+ mandatory: values.mandatory === true,
46
+ rollout: percent(typeof values.rollout === "string" ? values.rollout : undefined, "--rollout"),
47
+ };
48
+ }
49
+ if (command === "promote" || command === "rollback" || command === "patch") {
50
+ const { positionals, values } = parseArgs({
51
+ args: args.slice(1),
52
+ allowPositionals: true,
53
+ strict: false,
54
+ options: { mandatory: { type: "string", short: "m" }, rollout: { type: "string", short: "r" }, app: { type: "string", short: "a" } },
55
+ });
56
+ // BipoPush's own promote / rollback use flags; the CodePush forms are positional (<app> <deployment>…).
57
+ const names = typeof values.app === "string" ? positionals : positionals.slice(1);
58
+ if (command === "promote" && names.length >= 2)
59
+ return { kind: "promote", from: channelOf(names[0]), to: channelOf(names[1]) };
60
+ if (command === "rollback" && names.length >= 1)
61
+ return { kind: "rollback", channel: channelOf(names[0]) };
62
+ if (command === "patch" && names.length >= 1) {
63
+ const m = typeof values.mandatory === "string" ? values.mandatory : undefined;
64
+ return {
65
+ kind: "patch",
66
+ channel: channelOf(names[0]),
67
+ mandatory: m === undefined ? undefined : m !== "false",
68
+ rollout: percent(typeof values.rollout === "string" ? values.rollout : undefined, "--rollout"),
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+ if (command === "update" || command === "update:edit") {
74
+ const { values } = parseArgs({
75
+ args: args.slice(1),
76
+ strict: false,
77
+ options: {
78
+ channel: { type: "string" },
79
+ branch: { type: "string" },
80
+ message: { type: "string", short: "m" },
81
+ platform: { type: "string", short: "p" },
82
+ "rollout-percentage": { type: "string" },
83
+ },
84
+ });
85
+ const str = (v) => (typeof v === "string" ? v : undefined);
86
+ const channel = channelOf(str(values.channel) ?? str(values.branch) ?? "production");
87
+ const rollout = percent(str(values["rollout-percentage"]), "--rollout-percentage");
88
+ if (command === "update:edit") {
89
+ if (rollout === undefined)
90
+ throw new Error("update:edit needs --rollout-percentage");
91
+ return { kind: "rollout", channel, platform: platformOf(str(values.platform)), percent: rollout };
92
+ }
93
+ return { kind: "release", platform: platformOf(str(values.platform)), channel, message: str(values.message), mandatory: false, rollout };
94
+ }
95
+ return null;
96
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { parseArgs } from "node:util";
3
- import { init, listApps, promote, release } from "./commands.js";
4
+ import { disable, init, listApps, patchRelease, promote, release, rollout } from "./commands.js";
5
+ import { parseCompat, percent } from "./compat.js";
6
+ import { migrate } from "./migrate.js";
4
7
  import { API_URL, CREDENTIALS_FILE, clearCredentials, loadCredentials } from "./config.js";
5
8
  import { ApiError, request } from "./http.js";
6
9
  import { login } from "./login.js";
@@ -20,14 +23,26 @@ Usage:
20
23
  -c, --channel <name> production (default), staging, beta… — created if the plan allows
21
24
  -m, --message <text> Release note
22
25
  --mandatory Devices install it right away (critical fix)
26
+ --rollout <percent> Only this share of devices gets it at first (e.g. 10; Plus and above)
23
27
  --dry-run Build and show sizes without uploading
28
+ bipopush rollout <percent> Change how many devices get the newest version (100 finishes; Plus and above)
29
+ -c, --channel <name> -p, --platform ios|android|all --number <n>
30
+ bipopush disable Disable the newest version (or --number); devices get the newest enabled one
31
+ -c, --channel <name> -p, --platform ios|android|all --number <n>
24
32
  bipopush promote Re-publish a tested version in another channel (same bundle)
25
33
  --from <channel> --to <channel> (default: staging → production)
26
34
  -p, --platform ios|android|all (default: all)
27
35
  --number <n> Version to promote, e.g. 3 for v3 (default: newest live one)
28
36
  bipopush apps List your apps by organization
37
+ bipopush migrate Steps to move this project from CodePush, RevoPush or expo-updates
29
38
  bipopush logout Revoke this CLI's token and forget it
30
39
 
40
+ Coming from CodePush, RevoPush or Expo? Their commands work too, e.g.
41
+ bipopush release-react MyApp ios -d Production --description "Fix" -m -r 25
42
+ bipopush promote MyApp Staging Production bipopush rollback MyApp Production
43
+ bipopush patch MyApp Production -m true -r 50
44
+ bipopush update --channel production --message "Fix" --rollout-percentage 10
45
+
31
46
  Environment:
32
47
  BIPOPUSH_API_URL API base URL (default https://api.bipopush.com)
33
48
  BIPOPUSH_SIGN_URL Sign-in URL (default https://sign.bipopush.com)
@@ -77,7 +92,26 @@ function oneOf(value, allowed, fallback, flag) {
77
92
  throw new Error(`${flag} must be one of: ${allowed.join(", ")}`);
78
93
  return value;
79
94
  }
95
+ async function runCompat(compat) {
96
+ const cwd = process.cwd();
97
+ const token = await requireToken();
98
+ switch (compat.kind) {
99
+ case "release":
100
+ return release(token, { cwd, platform: compat.platform, channel: compat.channel, message: compat.message, mandatory: compat.mandatory, rollout: compat.rollout });
101
+ case "promote":
102
+ return promote(token, { cwd, from: compat.from, to: compat.to, platform: "all" });
103
+ case "rollback":
104
+ return disable(token, { cwd, channel: compat.channel, platform: "all" });
105
+ case "patch":
106
+ return patchRelease(token, { cwd, channel: compat.channel, platform: "all", mandatory: compat.mandatory, percent: compat.rollout });
107
+ case "rollout":
108
+ return rollout(token, { cwd, channel: compat.channel, platform: compat.platform, percent: compat.percent });
109
+ }
110
+ }
80
111
  async function main() {
112
+ const compat = parseCompat(process.argv.slice(2));
113
+ if (compat)
114
+ return runCompat(compat);
81
115
  const { positionals, values } = parseArgs({
82
116
  allowPositionals: true,
83
117
  options: {
@@ -96,11 +130,12 @@ async function main() {
96
130
  from: { type: "string" },
97
131
  to: { type: "string" },
98
132
  number: { type: "string" },
133
+ rollout: { type: "string" },
99
134
  },
100
135
  });
101
136
  const [command] = positionals;
102
137
  if (values.version)
103
- return console.log("0.2.0");
138
+ return console.log(createRequire(import.meta.url)("../package.json").version);
104
139
  if (values.help || !command)
105
140
  return console.log(HELP);
106
141
  const cwd = process.cwd();
@@ -125,8 +160,29 @@ async function main() {
125
160
  channel: channelArg(values.channel, "production", "--channel"),
126
161
  message: values.message,
127
162
  mandatory: values.mandatory,
163
+ rollout: percent(values.rollout, "--rollout"),
128
164
  dryRun: values["dry-run"],
129
165
  });
166
+ case "rollout": {
167
+ const share = percent(positionals[1], "rollout <percent>");
168
+ if (share === undefined)
169
+ throw new Error("Usage: bipopush rollout <percent> [-c channel]");
170
+ return rollout(await requireToken(), {
171
+ cwd,
172
+ channel: channelArg(values.channel, "production", "--channel"),
173
+ platform: oneOf(values.platform, ["ios", "android", "all"], "all", "--platform"),
174
+ version: values.number ? Number(values.number.replace(/^v/i, "")) : undefined,
175
+ percent: share,
176
+ });
177
+ }
178
+ case "disable":
179
+ case "rollback": // CodePush's name for it
180
+ return disable(await requireToken(), {
181
+ cwd,
182
+ channel: channelArg(values.channel, "production", "--channel"),
183
+ platform: oneOf(values.platform, ["ios", "android", "all"], "all", "--platform"),
184
+ version: values.number ? Number(values.number.replace(/^v/i, "")) : undefined,
185
+ });
130
186
  case "promote":
131
187
  return promote(await requireToken(), {
132
188
  cwd,
@@ -137,6 +193,8 @@ async function main() {
137
193
  });
138
194
  case "apps":
139
195
  return listApps(await requireToken());
196
+ case "migrate":
197
+ return migrate(cwd);
140
198
  case "logout":
141
199
  return logout();
142
200
  default:
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ /** OTA libraries BipoPush replaces, by package name. */
4
+ const SOURCES = {
5
+ "react-native-code-push": "CodePush",
6
+ "@revopush/react-native-code-push": "RevoPush",
7
+ "@code-push-next/react-native-code-push": "CodePush (code-push-next)",
8
+ "expo-updates": "Expo Updates",
9
+ };
10
+ /** Files under src-like folders that import the old library (at most 20, for the summary). */
11
+ function importers(cwd, names) {
12
+ const found = [];
13
+ const walk = (dir) => {
14
+ for (const entry of readdirSync(dir)) {
15
+ if (found.length >= 20 || entry === "node_modules" || entry.startsWith(".") || entry === "ios" || entry === "android")
16
+ continue;
17
+ const full = path.join(dir, entry);
18
+ if (statSync(full).isDirectory())
19
+ walk(full);
20
+ else if (/\.(jsx?|tsx?)$/.test(entry) && names.some((n) => readFileSync(full, "utf8").includes(`"${n}"`) || readFileSync(full, "utf8").includes(`'${n}'`))) {
21
+ found.push(path.relative(cwd, full));
22
+ }
23
+ }
24
+ };
25
+ walk(cwd);
26
+ return found;
27
+ }
28
+ /** Prints what to change to move this project from CodePush, RevoPush or expo-updates to BipoPush. */
29
+ export function migrate(cwd) {
30
+ const pkgFile = path.join(cwd, "package.json");
31
+ if (!existsSync(pkgFile))
32
+ throw new Error("Run this in your React Native / Expo project folder (no package.json here)");
33
+ const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
34
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
35
+ const present = Object.keys(SOURCES).filter((name) => deps[name]);
36
+ if (present.length === 0) {
37
+ console.log("No CodePush, RevoPush or expo-updates in this project. Start with: npx @bipopush/cli init");
38
+ return;
39
+ }
40
+ const codePush = present.filter((n) => n !== "expo-updates");
41
+ const expo = present.includes("expo-updates");
42
+ console.log(`Found ${present.map((n) => SOURCES[n]).join(" and ")}. To move to BipoPush:\n`);
43
+ let step = 1;
44
+ const say = (text) => console.log(`${step++}. ${text}`);
45
+ say(`Install the SDK: npm install @bipopush/react-native${expo ? " (Expo: npx expo install @bipopush/react-native)" : ""}`);
46
+ say(`Remove ${present.join(", ")} (npm uninstall), and its native setup:`);
47
+ if (codePush.length) {
48
+ console.log(" iOS: CodePush.bundleURL() in AppDelegate and CodePushDeploymentKey in Info.plist");
49
+ console.log(" Android: CodePush in MainApplication and CodePushDeploymentKey in strings.xml");
50
+ }
51
+ if (expo)
52
+ console.log(' app.json: the "updates" block and "runtimeVersion" (expo-updates); keep your other plugins');
53
+ say("Register the app and write its keys into the native project: npx @bipopush/cli init");
54
+ const files = importers(cwd, present);
55
+ const subpath = codePush.length ? "codepush" : "expo-updates";
56
+ say(`Keep your code: change the import to "@bipopush/react-native/${subpath}"${files.length ? ` (${files.length} file${files.length === 1 ? "" : "s"}: ${files.slice(0, 5).join(", ")}${files.length > 5 ? "…" : ""})` : ""},`);
57
+ console.log(` or leave imports alone and in metro.config.js: module.exports = require("@bipopush/react-native/metro").withBipoPush(config, { ${codePush.length ? "codePush: true" : "expoUpdates: true"} })`);
58
+ say("Rebuild the native app once (it now embeds BipoPush), then ship updates: npx @bipopush/cli release");
59
+ if (codePush.length) {
60
+ console.log("\nYour CodePush scripts keep working: bipopush release-react <app> <platform> -d Production, promote, rollback and patch.");
61
+ console.log("Deployments become channels: Production → production, Staging → staging.");
62
+ }
63
+ if (expo)
64
+ console.log("\nYour EAS commands map too: bipopush update --channel production --message \"…\" --rollout-percentage 10.");
65
+ }
package/dist/pack.js CHANGED
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
5
5
  import { tmpdir } from "node:os";
6
6
  import path from "node:path";
7
7
  import { brotliCompressSync, brotliDecompressSync, constants } from "node:zlib";
8
+ import { loadBsdiff } from "bsdiff-wasm";
8
9
  import { signDigest } from "./keys.js";
9
10
  /** A patch is only worth shipping when it is clearly smaller than the compressed full bundle. */
10
11
  export const PATCH_RATIO = 0.6;
@@ -45,22 +46,15 @@ export function compress(data) {
45
46
  export function decompress(data) {
46
47
  return brotliDecompressSync(data);
47
48
  }
48
- /** BSDIFF40 patch (old → new), or null when the optional native addon is unavailable. */
49
- export async function makePatch(oldData, newData, workDir) {
50
- let bsdiff;
51
- try {
52
- bsdiff = localRequire("bsdiff-nodejs");
53
- }
54
- catch {
55
- return null;
56
- }
57
- const oldFile = path.join(workDir, "base.hbc");
58
- const newFile = path.join(workDir, "new.hbc");
59
- const patchFile = path.join(workDir, "bundle.bspatch");
60
- writeFileSync(oldFile, oldData);
61
- writeFileSync(newFile, newData);
62
- await bsdiff.diff(oldFile, newFile, patchFile);
63
- return readFileSync(patchFile);
49
+ /** BSDIFF40 patch (old → new), the format the SDK's bspatch applies. WebAssembly: nothing to compile on install. */
50
+ export async function makePatch(oldData, newData) {
51
+ const bsdiff = await loadBsdiff();
52
+ bsdiff.FS.writeFile("/base.hbc", oldData);
53
+ bsdiff.FS.writeFile("/new.hbc", newData);
54
+ const code = bsdiff.callMain(["/base.hbc", "/new.hbc", "/bundle.bspatch"]);
55
+ if (code)
56
+ throw new Error(`bsdiff failed (exit ${code})`);
57
+ return Buffer.from(bsdiff.FS.readFile("/bundle.bspatch"));
64
58
  }
65
59
  /**
66
60
  * Canonical manifest digest (same in the API and the native SDK):
@@ -148,10 +142,8 @@ async function packIn(work, opts) {
148
142
  let patch = null;
149
143
  let patchNote = "no previous release for this channel and version";
150
144
  if (opts.base) {
151
- const data = await makePatch(opts.base.hbc, hbc, work);
152
- if (!data)
153
- patchNote = "bsdiff-nodejs not installed";
154
- else if (data.length >= full.length * PATCH_RATIO)
145
+ const data = await makePatch(opts.base.hbc, hbc);
146
+ if (data.length >= full.length * PATCH_RATIO)
155
147
  patchNote = `patch ${data.length} B is not smaller than ${PATCH_RATIO * 100}% of full`;
156
148
  else {
157
149
  patch = { baseArtifactId: opts.base.artifactId, data };
package/dist/project.js CHANGED
@@ -32,7 +32,7 @@ function plistValue(file, keyPath) {
32
32
  }
33
33
  function readIos(root) {
34
34
  const iosDir = path.join(root, "ios");
35
- const empty = { info: { id: null, version: null, scheme: null }, plist: null };
35
+ const empty = { info: { id: null, version: null, build: null, scheme: null }, plist: null };
36
36
  if (!existsSync(iosDir))
37
37
  return empty;
38
38
  const proj = readdirSync(iosDir).find((f) => f.endsWith(".xcodeproj"));
@@ -50,7 +50,10 @@ function readIos(root) {
50
50
  // "$(MARKETING_VERSION)" in bare React Native projects.
51
51
  const plistVersion = plist ? plistValue(plist, "CFBundleShortVersionString") : null;
52
52
  const version = plistVersion && !plistVersion.includes("$(") ? plistVersion : pbxSetting(pbx, "MARKETING_VERSION");
53
- return { info: { id, version, scheme }, plist: plist && existsSync(plist) ? plist : null };
53
+ // Same for the build: CFBundleVersion, "$(CURRENT_PROJECT_VERSION)" in bare projects.
54
+ const plistBuild = plist ? plistValue(plist, "CFBundleVersion") : null;
55
+ const build = plistBuild && !plistBuild.includes("$(") ? plistBuild : pbxSetting(pbx, "CURRENT_PROJECT_VERSION");
56
+ return { info: { id, version, build, scheme }, plist: plist && existsSync(plist) ? plist : null };
54
57
  }
55
58
  function androidManifestFile(root) {
56
59
  const file = path.join(root, "android", "app", "src", "main", "AndroidManifest.xml");
@@ -59,14 +62,15 @@ function androidManifestFile(root) {
59
62
  function readAndroid(root) {
60
63
  const gradleFile = ["build.gradle", "build.gradle.kts"].map((f) => path.join(root, "android", "app", f)).find(existsSync);
61
64
  if (!gradleFile)
62
- return { id: null, version: null, scheme: null };
65
+ return { id: null, version: null, build: null, scheme: null };
63
66
  const gradle = readFileSync(gradleFile, "utf8");
64
67
  const id = gradle.match(/applicationId\s*=?\s*["']([^"']+)["']/)?.[1] ?? null;
65
68
  const version = gradle.match(/versionName\s*=?\s*["']([^"']+)["']/)?.[1] ?? null;
69
+ const build = gradle.match(/versionCode\s*=?\s*(\d+)/)?.[1] ?? null;
66
70
  const manifestFile = path.join(root, "android", "app", "src", "main", "AndroidManifest.xml");
67
71
  const manifest = existsSync(manifestFile) ? readFileSync(manifestFile, "utf8") : "";
68
72
  const scheme = manifest.match(/<data[^>]*android:scheme="([^"]+)"/)?.[1] ?? null;
69
- return { id, version, scheme };
73
+ return { id, version, build, scheme };
70
74
  }
71
75
  /** Detects Expo vs bare React Native and reads native identifiers, versions and URL scheme. */
72
76
  export function detectProject(root) {
@@ -88,8 +92,18 @@ export function detectProject(root) {
88
92
  type: "expo",
89
93
  name: expo.name ?? pkg.name ?? "app",
90
94
  entryFile,
91
- ios: { id: expo.ios?.bundleIdentifier ?? null, version: expo.ios?.version ?? expo.version ?? null, scheme },
92
- android: { id: expo.android?.package ?? null, version: expo.version ?? null, scheme },
95
+ ios: {
96
+ id: expo.ios?.bundleIdentifier ?? null,
97
+ version: expo.ios?.version ?? expo.version ?? null,
98
+ build: expo.ios?.buildNumber != null ? String(expo.ios.buildNumber) : null,
99
+ scheme,
100
+ },
101
+ android: {
102
+ id: expo.android?.package ?? null,
103
+ version: expo.version ?? null,
104
+ build: expo.android?.versionCode != null ? String(expo.android.versionCode) : null,
105
+ scheme,
106
+ },
93
107
  iosInfoPlist: null,
94
108
  androidManifest: null,
95
109
  };
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@bipopush/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-beta.3",
4
4
  "description": "BipoPush CLI — log in and publish releases",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "BipoPush <contact@bipopush.com> (https://bipopush.com)",
8
8
  "homepage": "https://bipopush.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bipopush/bipopush.git",
12
+ "directory": "cli"
13
+ },
9
14
  "bugs": {
10
15
  "email": "contact@bipopush.com"
11
16
  },
@@ -35,10 +40,10 @@
35
40
  "@types/node": "^26.6.3",
36
41
  "typescript": "^6.0.3"
37
42
  },
38
- "optionalDependencies": {
39
- "bsdiff-nodejs": "^2.2.0"
40
- },
41
43
  "publishConfig": {
42
44
  "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "bsdiff-wasm": "0.1.4"
43
48
  }
44
49
  }