@upfluxhq/cli 1.3.1 → 1.4.0

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.
@@ -36,7 +36,7 @@ function buildUpfluxJson(appId, serverUrl, runtimeVersion, profiles) {
36
36
  }
37
37
  return {
38
38
  version: 1,
39
- app: Object.assign({ id: appId, buildNumber: 0, runtimeVersion }, (serverUrl !== DEFAULT_SERVER_URL ? { serverUrl } : {})),
39
+ app: Object.assign({ id: appId, runtimeVersion }, (serverUrl !== DEFAULT_SERVER_URL ? { serverUrl } : {})),
40
40
  deployments,
41
41
  };
42
42
  }
@@ -44,7 +44,7 @@ const init = new commander_1.Command("init")
44
44
  .description("Scaffold upflux.json in your project root")
45
45
  .option("--force", "Overwrite existing upflux.json")
46
46
  .action(async (opts) => {
47
- var _a, _b;
47
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
48
48
  console.log(chalk_1.default.bold("\nšŸš€ Upflux Project Setup\n"));
49
49
  // Detect Expo project
50
50
  let isExpo = false;
@@ -57,10 +57,67 @@ const init = new commander_1.Command("init")
57
57
  expoAppJson = parsed;
58
58
  }
59
59
  }
60
- catch (_c) {
60
+ catch (_k) {
61
61
  // Ignore parse errors
62
62
  }
63
63
  }
64
+ // Expo projects configure app.json directly + track buildNumber in .upflux (no upflux.json)
65
+ if (isExpo && expoAppJson) {
66
+ console.log(chalk_1.default.blue("šŸ“± Detected Expo project (configuring app.json)\n"));
67
+ const existingUrl = (_b = (_a = expoAppJson.expo) === null || _a === void 0 ? void 0 : _a.updates) === null || _b === void 0 ? void 0 : _b.url;
68
+ const defaultServerUrl = existingUrl
69
+ ? existingUrl.replace(/\/api\/updates\/check\/?$/, "")
70
+ : DEFAULT_SERVER_URL;
71
+ const { serverUrl } = await inquirer_1.default.prompt([
72
+ {
73
+ type: "input",
74
+ name: "serverUrl",
75
+ message: `Server URL (press Enter for ${defaultServerUrl}):`,
76
+ default: defaultServerUrl,
77
+ },
78
+ ]);
79
+ const defaultRuntimeVersion = (typeof ((_c = expoAppJson.expo) === null || _c === void 0 ? void 0 : _c.runtimeVersion) === "string" && expoAppJson.expo.runtimeVersion) ||
80
+ ((_d = expoAppJson.expo) === null || _d === void 0 ? void 0 : _d.version) ||
81
+ "1.0.0";
82
+ const { runtimeVersion } = await inquirer_1.default.prompt([
83
+ {
84
+ type: "input",
85
+ name: "runtimeVersion",
86
+ message: "Runtime version (e.g. 1.0.0 — update when shipping a new binary):",
87
+ default: defaultRuntimeVersion,
88
+ validate: (val) => {
89
+ if (!val.trim())
90
+ return "Runtime version is required";
91
+ if (!/^\d+\.\d+\.\d+/.test(val.trim()))
92
+ return "Use semver format, e.g. 1.0.0";
93
+ return true;
94
+ },
95
+ },
96
+ ]);
97
+ const existingClientKey = ((_g = (_f = (_e = expoAppJson.expo) === null || _e === void 0 ? void 0 : _e.updates) === null || _f === void 0 ? void 0 : _f.requestHeaders) === null || _g === void 0 ? void 0 : _g["x-client-key"]) || "";
98
+ const { clientKey } = await inquirer_1.default.prompt([
99
+ Object.assign(Object.assign({ type: "input", name: "clientKey", message: `Client key ${chalk_1.default.gray("(UPX-CLT-... or upx_cli_...)")}:` }, (existingClientKey ? { default: existingClientKey } : {})), { validate: (val) => {
100
+ if (!val.trim())
101
+ return "Client key is required";
102
+ if (val.trim().length < 10)
103
+ return "Client key looks too short";
104
+ return true;
105
+ } }),
106
+ ]);
107
+ expoAppJson.expo = expoAppJson.expo || {};
108
+ expoAppJson.expo.runtimeVersion = runtimeVersion.trim();
109
+ expoAppJson.expo.updates = Object.assign(Object.assign({ enabled: true, checkAutomatically: "ON_LOAD", fallbackToCacheTimeout: 0 }, (expoAppJson.expo.updates || {})), { url: `${serverUrl.trim().replace(/\/+$/, "")}/api/updates/check`, requestHeaders: Object.assign(Object.assign({}, (((_h = expoAppJson.expo.updates) === null || _h === void 0 ? void 0 : _h.requestHeaders) || {})), { "x-client-key": clientKey.trim() }) });
110
+ fs_1.default.writeFileSync(APP_JSON_FILE, JSON.stringify(expoAppJson, null, 2));
111
+ console.log(chalk_1.default.green("\nāœ“ Updated app.json with Expo Updates config\n"));
112
+ console.log(chalk_1.default.bold("šŸ“± Expo Next Steps:\n"));
113
+ console.log(chalk_1.default.white(` 1. Install expo-updates:\n` +
114
+ ` npx expo install expo-updates\n\n` +
115
+ ` 2. Authenticate CLI with your publish key:\n` +
116
+ ` upflux login\n\n` +
117
+ ` 3. Publish an OTA release:\n` +
118
+ ` upflux release\n`));
119
+ return;
120
+ }
64
121
  // Check if upflux.json already exists
65
122
  if (fs_1.default.existsSync(UPFLUX_JSON_FILE) && !opts.force) {
66
123
  console.log(chalk_1.default.yellow("āš ļø upflux.json already exists."));
@@ -96,7 +153,7 @@ const init = new commander_1.Command("init")
96
153
  },
97
154
  ]);
98
155
  // Step 3: Runtime version (semver, used to form auto release labels)
99
- const defaultRuntimeVersion = (isExpo && ((_a = expoAppJson === null || expoAppJson === void 0 ? void 0 : expoAppJson.expo) === null || _a === void 0 ? void 0 : _a.version))
156
+ const defaultRuntimeVersion = (isExpo && ((_j = expoAppJson === null || expoAppJson === void 0 ? void 0 : expoAppJson.expo) === null || _j === void 0 ? void 0 : _j.version))
100
157
  ? expoAppJson.expo.version
101
158
  : "1.0.0";
102
159
  const { runtimeVersion } = await inquirer_1.default.prompt([
@@ -153,41 +210,14 @@ const init = new commander_1.Command("init")
153
210
  // Show what was created
154
211
  console.log(chalk_1.default.gray("Preview:"));
155
212
  console.log(chalk_1.default.gray(JSON.stringify(content, null, 2)));
156
- if (isExpo && expoAppJson) {
157
- const primaryProfile = profiles.find(p => p.name === "production") || profiles[0];
158
- if (primaryProfile) {
159
- const { updateAppJson } = await inquirer_1.default.prompt([
160
- {
161
- type: "confirm",
162
- name: "updateAppJson",
163
- message: `Configure app.json with Expo Updates URL and client key for "${primaryProfile.name}"?`,
164
- default: true,
165
- },
166
- ]);
167
- if (updateAppJson) {
168
- expoAppJson.expo = expoAppJson.expo || {};
169
- expoAppJson.expo.runtimeVersion = runtimeVersion.trim();
170
- expoAppJson.expo.updates = Object.assign(Object.assign({ enabled: true, checkAutomatically: "ON_LOAD", fallbackToCacheTimeout: 0 }, (expoAppJson.expo.updates || {})), { url: `${serverUrl.trim()}/api/updates/check`, requestHeaders: Object.assign(Object.assign({}, (((_b = expoAppJson.expo.updates) === null || _b === void 0 ? void 0 : _b.requestHeaders) || {})), { "x-client-key": primaryProfile.clientKey.trim() }) });
171
- fs_1.default.writeFileSync(APP_JSON_FILE, JSON.stringify(expoAppJson, null, 2));
172
- console.log(chalk_1.default.green("āœ“ Updated app.json with Expo Updates config\n"));
173
- }
174
- }
175
- console.log(chalk_1.default.bold("\nšŸ“± Expo Usage:\n"));
176
- console.log(chalk_1.default.white(` 1. Install expo-updates:\n` +
177
- ` npx expo install expo-updates\n\n` +
178
- ` 2. Publish a release:\n` +
179
- ` upflux release\n`));
180
- }
181
- else {
182
- // Usage hint
183
- console.log(chalk_1.default.bold("\nšŸ“¦ SDK Usage:\n"));
184
- console.log(chalk_1.default.white(` import { Upflux } from '@upfluxhq/react-native';\n\n` +
185
- ` // Auto-selects 'development' in dev, 'production' in release\n` +
186
- ` // Auto-increments buildNumber on every release\n` +
187
- ` await Upflux.init();\n\n` +
188
- ` // Or specify a profile:\n` +
189
- ` await Upflux.init('production');`));
190
- }
213
+ // Usage hint
214
+ console.log(chalk_1.default.bold("\nšŸ“¦ SDK Usage:\n"));
215
+ console.log(chalk_1.default.white(` import { Upflux } from '@upfluxhq/react-native';\n\n` +
216
+ ` // Auto-selects 'development' in dev, 'production' in release\n` +
217
+ ` // Auto-increments buildNumber on every release\n` +
218
+ ` await Upflux.init();\n\n` +
219
+ ` // Or specify a profile:\n` +
220
+ ` await Upflux.init('production');`));
191
221
  // Security note about committing the file
192
222
  console.log(chalk_1.default.cyan("\nšŸ’” Tip: upflux.json is safe to commit to git.\n" +
193
223
  " upx_cli_* keys are read-only — they cannot publish releases.\n" +
@@ -198,13 +198,56 @@ function readRootUpfluxJson() {
198
198
  return null;
199
199
  }
200
200
  }
201
- /** Increment app.buildNumber, write back to upflux.json, return new value. */
202
- function incrementBuildNumber(json) {
203
- var _a;
204
- const next = ((_a = json.app.buildNumber) !== null && _a !== void 0 ? _a : 0) + 1;
205
- json.app.buildNumber = next;
206
- fs.writeFileSync(path.resolve('upflux.json'), JSON.stringify(json, null, 2));
207
- return next;
201
+ /** Get version from package.json as fallback when upflux.json / app.json is absent */
202
+ function getPackageJsonVersion() {
203
+ const pkgPath = path.resolve('package.json');
204
+ if (!fs.existsSync(pkgPath))
205
+ return undefined;
206
+ try {
207
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
208
+ return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : undefined;
209
+ }
210
+ catch (_a) {
211
+ return undefined;
212
+ }
213
+ }
214
+ /**
215
+ * Query GET /cli/status on the Upflux server to find the highest +N build number
216
+ * for the given runtimeVersion on the target deployment, and return maxN + 1.
217
+ * Starts at 1 if no matching releases exist (or if all were deleted in the Dashboard).
218
+ */
219
+ async function resolveNextBuildNumberFromServer(runtimeVersion, authKey) {
220
+ var _a, _b, _c, _d, _e, _f, _g;
221
+ try {
222
+ const res = await apiClient_1.apiClient.get('/cli/status', {
223
+ headers: { 'x-publish-key': authKey },
224
+ });
225
+ const labels = new Set();
226
+ for (const r of ((_a = res.data) === null || _a === void 0 ? void 0 : _a.releases) || []) {
227
+ if (r === null || r === void 0 ? void 0 : r.releaseLabel)
228
+ labels.add(r.releaseLabel);
229
+ }
230
+ if ((_d = (_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.current) === null || _c === void 0 ? void 0 : _c.ios) === null || _d === void 0 ? void 0 : _d.releaseLabel) {
231
+ labels.add(res.data.current.ios.releaseLabel);
232
+ }
233
+ if ((_g = (_f = (_e = res.data) === null || _e === void 0 ? void 0 : _e.current) === null || _f === void 0 ? void 0 : _f.android) === null || _g === void 0 ? void 0 : _g.releaseLabel) {
234
+ labels.add(res.data.current.android.releaseLabel);
235
+ }
236
+ let maxBuild = 0;
237
+ const prefix = `${runtimeVersion}+`;
238
+ for (const label of labels) {
239
+ if (label.startsWith(prefix)) {
240
+ const num = parseInt(label.slice(prefix.length), 10);
241
+ if (!isNaN(num) && num > maxBuild) {
242
+ maxBuild = num;
243
+ }
244
+ }
245
+ }
246
+ return maxBuild + 1;
247
+ }
248
+ catch (_h) {
249
+ return 1;
250
+ }
208
251
  }
209
252
  /**
210
253
  * Generate an auto release label.
@@ -350,40 +393,6 @@ function writeUpfluxConfig(outputDir, releaseLabel, appId, buildNumber) {
350
393
  const configPath = path.join(outputDir, "upflux.json");
351
394
  fs.writeFileSync(configPath, configContent);
352
395
  console.log(chalk_1.default.gray(` Created upflux.json (label: ${releaseLabel}${buildNumber !== undefined ? `, build: ${buildNumber}` : ''})`));
353
- copyUpfluxConfigToNativeAssets(configContent);
354
- }
355
- /**
356
- * Copy upflux.json to native platform asset directories
357
- *
358
- * The React Native SDK reads upflux.json from native assets at runtime to detect
359
- * which OTA version is embedded in the binary. Without this file in the right place,
360
- * the SDK cannot compare embedded vs server versions, causing unnecessary downloads
361
- * even when the Play Store / App Store binary already has the latest code.
362
- *
363
- * Android: android/app/src/main/assets/upflux.json
364
- * iOS: ios/ (added to Xcode project bundle resources manually by developer)
365
- *
366
- * @param configContent - The JSON string content of upflux.json
367
- */
368
- function copyUpfluxConfigToNativeAssets(configContent) {
369
- // Android: copy to assets directory where UpfluxModule.readAsset() can find it
370
- const androidAssetsDir = path.resolve("android", "app", "src", "main", "assets");
371
- if (fs.existsSync(path.resolve("android"))) {
372
- // Ensure assets directory exists
373
- if (!fs.existsSync(androidAssetsDir)) {
374
- fs.mkdirSync(androidAssetsDir, { recursive: true });
375
- }
376
- const androidConfigPath = path.join(androidAssetsDir, "upflux.json");
377
- fs.writeFileSync(androidConfigPath, configContent);
378
- console.log(chalk_1.default.gray(` Copied upflux.json → android/app/src/main/assets/`));
379
- }
380
- // iOS: copy to ios/ project root (developer adds to Xcode bundle resources)
381
- const iosDir = path.resolve("ios");
382
- if (fs.existsSync(iosDir)) {
383
- const iosConfigPath = path.join(iosDir, "upflux.json");
384
- fs.writeFileSync(iosConfigPath, configContent);
385
- console.log(chalk_1.default.gray(` Copied upflux.json → ios/`));
386
- }
387
396
  }
388
397
  const release = new commander_1.Command("release")
389
398
  .description("Bundle and upload a new release to Upflux")
@@ -407,7 +416,7 @@ const release = new commander_1.Command("release")
407
416
  .option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
408
417
  .option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
409
418
  .action(async (opts) => {
410
- var _a, _b, _c, _d, _e, _f;
419
+ var _a, _b, _c, _d, _e, _f, _g;
411
420
  try {
412
421
  // ========================================
413
422
  // VALIDATION PHASE
@@ -479,50 +488,29 @@ const release = new commander_1.Command("release")
479
488
  process.exit(1);
480
489
  }
481
490
  // ── Label Resolution ─────────────────────────────────────────────
482
- // Two paths: manual (--label provided) or auto (buildNumber from upflux.json)
491
+ // Two paths: manual (--label provided) or auto (derived from server deployment releases)
483
492
  let resolvedLabel;
484
493
  const rootJson = readRootUpfluxJson();
485
- const currentBuildNumber = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber;
494
+ const isExpo = opts.expo || isExpoProject();
486
495
  if (opts.label) {
487
- // Manual mode: validate semver (now allows + notation), still increment buildNumber for tracking
488
496
  if (!isValidReleaseLabel(opts.label)) {
489
497
  console.error(chalk_1.default.red("āœ— Invalid release label format"));
490
498
  console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, 1.0.0+5, etc."));
491
499
  process.exit(1);
492
500
  }
493
501
  resolvedLabel = opts.label;
494
- if (rootJson && currentBuildNumber !== undefined) {
495
- const tracked = incrementBuildNumber(rootJson);
496
- console.log(chalk_1.default.gray(` buildNumber → ${tracked} (tracking)`));
497
- }
498
502
  console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (manual)`));
499
503
  }
500
504
  else {
501
- // Auto mode: require upflux.json with buildNumber field (or auto-generate for Expo)
502
- if (!rootJson) {
503
- if (isExpoProject() || opts.expo) {
504
- const rv = getExpoRuntimeVersion() || '1.0.0';
505
- resolvedLabel = `${rv}+${Math.floor(Date.now() / 1000)}`;
506
- console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (auto from Expo runtimeVersion)`));
507
- }
508
- else {
509
- console.error(chalk_1.default.red("āœ— No upflux.json found and no --label provided"));
510
- console.error(chalk_1.default.gray("Run: upflux init OR upflux release --label v1.0.0"));
511
- process.exit(1);
512
- }
513
- }
514
- else {
515
- if (rootJson.app.buildNumber === undefined) {
516
- console.error(chalk_1.default.red("āœ— upflux.json is missing app.buildNumber"));
517
- console.error(chalk_1.default.gray('Add "buildNumber": 0 to the app section, or use --label'));
518
- process.exit(1);
519
- }
520
- const nextBuild = incrementBuildNumber(rootJson);
521
- const effectiveRuntimeVersion = rootJson.app.runtimeVersion || getExpoRuntimeVersion();
522
- resolvedLabel = generateReleaseLabel(nextBuild, effectiveRuntimeVersion);
523
- console.log(chalk_1.default.green(` āœ“ buildNumber: ${nextBuild - 1} → ${nextBuild}`));
524
- console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (auto)`));
525
- }
505
+ const effectiveRuntimeVersion = (isExpo ? getExpoRuntimeVersion() : undefined) ||
506
+ ((_b = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app) === null || _b === void 0 ? void 0 : _b.runtimeVersion) ||
507
+ getExpoRuntimeVersion() ||
508
+ getPackageJsonVersion() ||
509
+ "1.0.0";
510
+ const nextBuild = await resolveNextBuildNumberFromServer(effectiveRuntimeVersion, authKey);
511
+ resolvedLabel = generateReleaseLabel(nextBuild, effectiveRuntimeVersion);
512
+ console.log(chalk_1.default.green(` āœ“ Release build #${nextBuild} for runtime ${effectiveRuntimeVersion}`));
513
+ console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (auto)`));
526
514
  }
527
515
  // Validation 6: Rollout percentage
528
516
  const rolloutValidation = isValidRollout(opts.rollout);
@@ -558,7 +546,6 @@ const release = new commander_1.Command("release")
558
546
  let expoBundleExt = "hbc";
559
547
  // Detect project type
560
548
  const isCapacitor = opts.capacitor || isCapacitorProject();
561
- const isExpo = opts.expo || isExpoProject();
562
549
  if (isExpo) {
563
550
  // ========================================
564
551
  // EXPO BUNDLING
@@ -588,7 +575,7 @@ const release = new commander_1.Command("release")
588
575
  }
589
576
  const expoMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
590
577
  if (platform === 'all') {
591
- if (!((_b = expoMeta.fileMetadata) === null || _b === void 0 ? void 0 : _b.ios) || !((_c = expoMeta.fileMetadata) === null || _c === void 0 ? void 0 : _c.android)) {
578
+ if (!((_c = expoMeta.fileMetadata) === null || _c === void 0 ? void 0 : _c.ios) || !((_d = expoMeta.fileMetadata) === null || _d === void 0 ? void 0 : _d.android)) {
592
579
  console.error(chalk_1.default.red("āœ— Expo metadata must contain fileMetadata for both ios and android when platform is 'all'"));
593
580
  process.exit(1);
594
581
  }
@@ -625,7 +612,7 @@ const release = new commander_1.Command("release")
625
612
  console.log(chalk_1.default.gray(` Android: ${androidBundlePath}`));
626
613
  }
627
614
  else {
628
- const platformMeta = (_d = expoMeta.fileMetadata) === null || _d === void 0 ? void 0 : _d[platform];
615
+ const platformMeta = (_e = expoMeta.fileMetadata) === null || _e === void 0 ? void 0 : _e[platform];
629
616
  if (!platformMeta) {
630
617
  console.error(chalk_1.default.red(`āœ— No Expo metadata found for platform: ${platform}`));
631
618
  process.exit(1);
@@ -710,7 +697,7 @@ const release = new commander_1.Command("release")
710
697
  try {
711
698
  fs.accessSync(bundlePath, fs.constants.R_OK);
712
699
  }
713
- catch (_g) {
700
+ catch (_h) {
714
701
  console.error(chalk_1.default.red(`āœ— Cannot read bundle file: ${bundlePath}`));
715
702
  process.exit(1);
716
703
  }
@@ -1008,7 +995,7 @@ const release = new commander_1.Command("release")
1008
995
  let androidExpoMetadata;
1009
996
  let runtimeVersion = undefined;
1010
997
  if (isExpo) {
1011
- runtimeVersion = getExpoRuntimeVersion() || ((_e = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app) === null || _e === void 0 ? void 0 : _e.runtimeVersion) || "1.0.0";
998
+ runtimeVersion = getExpoRuntimeVersion() || ((_f = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app) === null || _f === void 0 ? void 0 : _f.runtimeVersion) || "1.0.0";
1012
999
  const outputDir = path.resolve(opts.outputDir);
1013
1000
  const buildPlatformExpoMetadata = (targetPlatform, targetBundlePath, targetAssetsConfig) => {
1014
1001
  const manifestId = crypto.randomUUID();
@@ -1130,7 +1117,7 @@ const release = new commander_1.Command("release")
1130
1117
  }
1131
1118
  catch (error) {
1132
1119
  console.error(chalk_1.default.red(`\nāœ— Failed to upload release: ${error.message}`));
1133
- if ((_f = error.response) === null || _f === void 0 ? void 0 : _f.data) {
1120
+ if ((_g = error.response) === null || _g === void 0 ? void 0 : _g.data) {
1134
1121
  console.error(chalk_1.default.gray(JSON.stringify(error.response.data, null, 2)));
1135
1122
  }
1136
1123
  process.exit(1);
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ const program = new commander_1.Command();
15
15
  program
16
16
  .name("upflux")
17
17
  .description("Upflux CLI for OTA updates")
18
- .version("1.0.0");
18
+ .version("1.4.0");
19
19
  program.addCommand(init_1.default);
20
20
  program.addCommand(login_1.default);
21
21
  program.addCommand(logout_1.default);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upfluxhq/cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Upflux CLI for managing OTA updates",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",