@upfluxhq/cli 1.1.0 → 1.3.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.
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
44
  const inquirer_1 = __importDefault(require("inquirer"));
45
45
  const archiver_1 = __importDefault(require("archiver"));
46
+ const crypto = __importStar(require("crypto"));
46
47
  const apiClient_1 = require("../client/apiClient");
47
48
  const configStore_1 = require("../config/configStore");
48
49
  const native_detector_1 = require("../utils/native-detector");
@@ -69,6 +70,43 @@ const CAPACITOR_CONFIG_FILES = [
69
70
  function isCapacitorProject() {
70
71
  return CAPACITOR_CONFIG_FILES.some((file) => fs.existsSync(path.resolve(file)));
71
72
  }
73
+ /**
74
+ * Check if current directory is an Expo project
75
+ */
76
+ function isExpoProject() {
77
+ const appJsonPath = path.resolve('app.json');
78
+ if (!fs.existsSync(appJsonPath))
79
+ return false;
80
+ try {
81
+ const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
82
+ return !!appJson.expo;
83
+ }
84
+ catch (_a) {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Get Expo runtime version or fallback to version from app.json
90
+ */
91
+ function getExpoRuntimeVersion() {
92
+ var _a, _b;
93
+ const appJsonPath = path.resolve('app.json');
94
+ if (!fs.existsSync(appJsonPath))
95
+ return undefined;
96
+ try {
97
+ const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
98
+ const rv = (_a = appJson === null || appJson === void 0 ? void 0 : appJson.expo) === null || _a === void 0 ? void 0 : _a.runtimeVersion;
99
+ if (typeof rv === 'string' && rv.trim())
100
+ return rv.trim();
101
+ if (typeof ((_b = appJson === null || appJson === void 0 ? void 0 : appJson.expo) === null || _b === void 0 ? void 0 : _b.version) === 'string' && appJson.expo.version.trim()) {
102
+ return appJson.expo.version.trim();
103
+ }
104
+ return undefined;
105
+ }
106
+ catch (_c) {
107
+ return undefined;
108
+ }
109
+ }
72
110
  /**
73
111
  * Get Capacitor webDir from config
74
112
  */
@@ -122,6 +160,13 @@ async function zipDirectory(sourceDir, outPath) {
122
160
  archive.finalize();
123
161
  });
124
162
  }
163
+ /**
164
+ * Compute Expo's required base64url-encoded SHA-256 hash
165
+ */
166
+ function computeBase64urlHash(filePath) {
167
+ const file = fs.readFileSync(filePath);
168
+ return crypto.createHash('sha256').update(file).digest('base64url');
169
+ }
125
170
  /**
126
171
  * Validate MongoDB ObjectId format
127
172
  */
@@ -135,11 +180,41 @@ function isValidAppId(appId) {
135
180
  return /^[a-zA-Z0-9-_]+$/.test(appId) && appId.length >= 3;
136
181
  }
137
182
  /**
138
- * Validate release label format (semver-like)
183
+ * Validate release label format (semver-like, allows + build metadata)
139
184
  */
140
185
  function isValidReleaseLabel(label) {
141
- // Allow v1.0.0, 1.0.0, v1.0.0-beta, etc.
142
- return /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?$/.test(label);
186
+ // Allow v1.0.0, 1.0.0, v1.0.0-beta, 1.0.0+5, 1.0.0-beta+5, etc.
187
+ return /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?(\+[a-zA-Z0-9._-]+)?$/.test(label);
188
+ }
189
+ /** Read upflux.json from project root. Returns null if not found. */
190
+ function readRootUpfluxJson() {
191
+ const filePath = path.resolve('upflux.json');
192
+ if (!fs.existsSync(filePath))
193
+ return null;
194
+ try {
195
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
196
+ }
197
+ catch (_a) {
198
+ return null;
199
+ }
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;
208
+ }
209
+ /**
210
+ * Generate an auto release label.
211
+ * With runtimeVersion: "1.0.0+6" (semver + build counter)
212
+ * Without: "v0.0.6" (bare semver fallback — passes validator)
213
+ */
214
+ function generateReleaseLabel(buildNumber, runtimeVersion) {
215
+ return runtimeVersion
216
+ ? `${runtimeVersion}+${buildNumber}`
217
+ : `v0.0.${buildNumber}`;
143
218
  }
144
219
  /**
145
220
  * Validate version range format
@@ -256,28 +331,25 @@ function collectAssets(dir) {
256
331
  return assets;
257
332
  }
258
333
  /**
259
- * Write upflux.json config file to the bundle output directory
334
+ * Write upflux.json config file to the bundle output directory.
260
335
  *
261
336
  * This file embeds the OTA version into the app bundle at build time.
262
- * The SDK reads this on fresh install to know which version the app ships with,
263
- * preventing unnecessary OTA downloads when the embedded bundle is already latest.
264
- *
265
- * @param outputDir - Directory to write upflux.json to
266
- * @param releaseLabel - The OTA version (e.g., "v1.0.0")
267
- * @param appId - The app ID
337
+ * `releaseLabel` is used by the SDK for identity check (am I running the latest?).
338
+ * `buildNumber` is stored as metadata for display/debugging.
268
339
  */
269
- function writeUpfluxConfig(outputDir, releaseLabel, appId) {
340
+ function writeUpfluxConfig(outputDir, releaseLabel, appId, buildNumber) {
270
341
  const config = {
271
342
  releaseLabel,
272
343
  appId,
273
344
  createdAt: new Date().toISOString(),
274
345
  };
346
+ if (buildNumber !== undefined) {
347
+ config.buildNumber = buildNumber;
348
+ }
275
349
  const configContent = JSON.stringify(config, null, 2);
276
350
  const configPath = path.join(outputDir, "upflux.json");
277
351
  fs.writeFileSync(configPath, configContent);
278
- console.log(chalk_1.default.gray(` Created upflux.json with version: ${releaseLabel}`));
279
- // Also copy to native asset directories so the SDK can read the
280
- // embedded version at runtime and skip unnecessary OTA downloads
352
+ console.log(chalk_1.default.gray(` Created upflux.json (label: ${releaseLabel}${buildNumber !== undefined ? `, build: ${buildNumber}` : ''})`));
281
353
  copyUpfluxConfigToNativeAssets(configContent);
282
354
  }
283
355
  /**
@@ -317,7 +389,7 @@ const release = new commander_1.Command("release")
317
389
  .description("Bundle and upload a new release to Upflux")
318
390
  .option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
319
391
  .option("-d, --deployment <deploymentId>", "Deployment ID (uses stored value if not provided)")
320
- .requiredOption("-l, --label <label>", "Release label (e.g., v1.0.0)")
392
+ .option("-l, --label <label>", "Release label (e.g., v1.0.0) — omit to auto-increment buildNumber from upflux.json")
321
393
  .option("--platform <platform>", "Target platform for the release: all, ios, or android (defaults to all)", "all")
322
394
  .option("-e, --entry-file <path>", "Entry file for bundler", DEFAULT_ENTRY_FILE)
323
395
  .option("-o, --output-dir <path>", "Output directory for bundle", DEFAULT_OUTPUT_DIR)
@@ -331,10 +403,11 @@ const release = new commander_1.Command("release")
331
403
  .option("--schedule", "Create as draft, set schedule time in dashboard")
332
404
  .option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
333
405
  .option("--skip-native-check", "Skip automatic native change detection (useful in CI/CD)", false)
406
+ .option("--expo", "Force Expo mode (auto-detected if app.json with expo exists)")
334
407
  .option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
335
408
  .option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
336
409
  .action(async (opts) => {
337
- var _a, _b;
410
+ var _a, _b, _c, _d, _e, _f;
338
411
  try {
339
412
  // ========================================
340
413
  // VALIDATION PHASE
@@ -405,11 +478,51 @@ const release = new commander_1.Command("release")
405
478
  console.error(chalk_1.default.gray("Deployment ID should be a 24-character MongoDB ObjectId"));
406
479
  process.exit(1);
407
480
  }
408
- // Validation 5: Release label format
409
- if (!isValidReleaseLabel(opts.label)) {
410
- console.error(chalk_1.default.red("✗ Invalid release label format"));
411
- console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, etc."));
412
- process.exit(1);
481
+ // ── Label Resolution ─────────────────────────────────────────────
482
+ // Two paths: manual (--label provided) or auto (buildNumber from upflux.json)
483
+ let resolvedLabel;
484
+ const rootJson = readRootUpfluxJson();
485
+ const currentBuildNumber = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber;
486
+ if (opts.label) {
487
+ // Manual mode: validate semver (now allows + notation), still increment buildNumber for tracking
488
+ if (!isValidReleaseLabel(opts.label)) {
489
+ console.error(chalk_1.default.red("✗ Invalid release label format"));
490
+ console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, 1.0.0+5, etc."));
491
+ process.exit(1);
492
+ }
493
+ 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
+ console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (manual)`));
499
+ }
500
+ 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
+ }
413
526
  }
414
527
  // Validation 6: Rollout percentage
415
528
  const rolloutValidation = isValidRollout(opts.rollout);
@@ -438,9 +551,118 @@ const release = new commander_1.Command("release")
438
551
  let assetsBaseDir;
439
552
  let iosAssetsBaseDir;
440
553
  let androidAssetsBaseDir;
554
+ // Expo-specific tracking
555
+ let expoAssetsConfig = [];
556
+ let iosExpoAssetsConfig = [];
557
+ let androidExpoAssetsConfig = [];
558
+ let expoBundleExt = "hbc";
441
559
  // Detect project type
442
560
  const isCapacitor = opts.capacitor || isCapacitorProject();
443
- if (isCapacitor) {
561
+ const isExpo = opts.expo || isExpoProject();
562
+ if (isExpo) {
563
+ // ========================================
564
+ // EXPO BUNDLING
565
+ // ========================================
566
+ console.log(chalk_1.default.blue("📱 Detected Expo project"));
567
+ const outputDir = path.resolve(opts.outputDir);
568
+ if (!opts.skipBundle) {
569
+ console.log(chalk_1.default.blue("📦 Running Expo export..."));
570
+ const platformFlag = platform !== 'all' ? ` --platform ${platform}` : '';
571
+ try {
572
+ (0, child_process_1.execSync)(`npx expo export --output-dir ${opts.outputDir}${platformFlag}`, {
573
+ stdio: "inherit",
574
+ cwd: process.cwd(),
575
+ });
576
+ }
577
+ catch (error) {
578
+ throw new Error("Expo export failed. Check the error above.");
579
+ }
580
+ }
581
+ else {
582
+ console.log(chalk_1.default.blue("⏭ Skipping bundle, using existing Expo export..."));
583
+ }
584
+ const metadataPath = path.join(outputDir, "metadata.json");
585
+ if (!fs.existsSync(metadataPath)) {
586
+ console.error(chalk_1.default.red(`✗ Expo metadata not found: ${metadataPath}`));
587
+ process.exit(1);
588
+ }
589
+ const expoMeta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
590
+ 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)) {
592
+ console.error(chalk_1.default.red("✗ Expo metadata must contain fileMetadata for both ios and android when platform is 'all'"));
593
+ process.exit(1);
594
+ }
595
+ iosBundlePath = path.join(outputDir, expoMeta.fileMetadata.ios.bundle);
596
+ androidBundlePath = path.join(outputDir, expoMeta.fileMetadata.android.bundle);
597
+ iosAssetsBaseDir = outputDir;
598
+ androidAssetsBaseDir = outputDir;
599
+ const seenIosPaths = new Set();
600
+ iosAssetFiles = [];
601
+ iosExpoAssetsConfig = expoMeta.fileMetadata.ios.assets || [];
602
+ for (const a of iosExpoAssetsConfig) {
603
+ const fullPath = path.join(outputDir, a.path);
604
+ if (!seenIosPaths.has(a.path)) {
605
+ seenIosPaths.add(a.path);
606
+ iosAssetFiles.push(fullPath);
607
+ }
608
+ }
609
+ const seenAndroidPaths = new Set();
610
+ androidAssetFiles = [];
611
+ androidExpoAssetsConfig = expoMeta.fileMetadata.android.assets || [];
612
+ for (const a of androidExpoAssetsConfig) {
613
+ const fullPath = path.join(outputDir, a.path);
614
+ if (!seenAndroidPaths.has(a.path)) {
615
+ seenAndroidPaths.add(a.path);
616
+ androidAssetFiles.push(fullPath);
617
+ }
618
+ }
619
+ bundlePath = iosBundlePath;
620
+ assetFiles = [...iosAssetFiles, ...androidAssetFiles];
621
+ assetsBaseDir = outputDir;
622
+ expoAssetsConfig = [...iosExpoAssetsConfig];
623
+ console.log(chalk_1.default.green(`✓ Expo bundles ready:`));
624
+ console.log(chalk_1.default.gray(` iOS: ${iosBundlePath}`));
625
+ console.log(chalk_1.default.gray(` Android: ${androidBundlePath}`));
626
+ }
627
+ else {
628
+ const platformMeta = (_d = expoMeta.fileMetadata) === null || _d === void 0 ? void 0 : _d[platform];
629
+ if (!platformMeta) {
630
+ console.error(chalk_1.default.red(`✗ No Expo metadata found for platform: ${platform}`));
631
+ process.exit(1);
632
+ }
633
+ bundlePath = path.join(outputDir, platformMeta.bundle);
634
+ expoBundleExt = path.extname(bundlePath).replace('.', '') || "hbc";
635
+ assetsBaseDir = outputDir;
636
+ if (platform === 'ios') {
637
+ iosBundlePath = bundlePath;
638
+ iosAssetsBaseDir = outputDir;
639
+ }
640
+ else if (platform === 'android') {
641
+ androidBundlePath = bundlePath;
642
+ androidAssetsBaseDir = outputDir;
643
+ }
644
+ const seenPaths = new Set();
645
+ assetFiles = [];
646
+ expoAssetsConfig = platformMeta.assets || [];
647
+ for (const a of expoAssetsConfig) {
648
+ const fullPath = path.join(outputDir, a.path);
649
+ if (!seenPaths.has(a.path)) {
650
+ seenPaths.add(a.path);
651
+ assetFiles.push(fullPath);
652
+ }
653
+ }
654
+ if (platform === 'ios') {
655
+ iosAssetFiles = [...assetFiles];
656
+ iosExpoAssetsConfig = expoAssetsConfig;
657
+ }
658
+ else if (platform === 'android') {
659
+ androidAssetFiles = [...assetFiles];
660
+ androidExpoAssetsConfig = expoAssetsConfig;
661
+ }
662
+ console.log(chalk_1.default.green(`✓ Expo bundle ready: ${bundlePath}`));
663
+ }
664
+ }
665
+ else if (isCapacitor) {
444
666
  // ========================================
445
667
  // CAPACITOR BUNDLING
446
668
  // ========================================
@@ -467,10 +689,10 @@ const release = new commander_1.Command("release")
467
689
  }
468
690
  // Write upflux.json to web directory BEFORE zipping
469
691
  // This embeds the OTA version into the Capacitor app bundle
470
- writeUpfluxConfig(webDirPath, opts.label, appId);
692
+ writeUpfluxConfig(webDirPath, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
471
693
  // Zip the web directory
472
694
  console.log(chalk_1.default.blue(`📦 Zipping ${webDir}/ directory...`));
473
- const zipPath = path.join(outputDir, `bundle-${opts.label}.zip`);
695
+ const zipPath = path.join(outputDir, `bundle-${resolvedLabel}.zip`);
474
696
  bundlePath = await zipDirectory(webDirPath, zipPath);
475
697
  console.log(chalk_1.default.green(`✓ Bundle created: ${bundlePath}`));
476
698
  // No separate assets for Capacitor - all in zip
@@ -488,7 +710,7 @@ const release = new commander_1.Command("release")
488
710
  try {
489
711
  fs.accessSync(bundlePath, fs.constants.R_OK);
490
712
  }
491
- catch (_c) {
713
+ catch (_g) {
492
714
  console.error(chalk_1.default.red(`✗ Cannot read bundle file: ${bundlePath}`));
493
715
  process.exit(1);
494
716
  }
@@ -572,8 +794,8 @@ const release = new commander_1.Command("release")
572
794
  : collectAssets(androidAssetsPath);
573
795
  // Write upflux.json to BOTH platform output directories
574
796
  // React Native reads this from bundled assets to know embedded OTA version
575
- writeUpfluxConfig(iosOutputDir, opts.label, appId);
576
- writeUpfluxConfig(androidOutputDir, opts.label, appId);
797
+ writeUpfluxConfig(iosOutputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
798
+ writeUpfluxConfig(androidOutputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
577
799
  // Set bundlePath and assetFiles for display (use iOS as primary for logging)
578
800
  bundlePath = iosBundlePath;
579
801
  assetFiles = [...iosAssetFiles, ...androidAssetFiles];
@@ -618,7 +840,7 @@ const release = new commander_1.Command("release")
618
840
  }
619
841
  // Write upflux.json to output directory
620
842
  // React Native reads this from bundled assets to know embedded OTA version
621
- writeUpfluxConfig(outputDir, opts.label, appId);
843
+ writeUpfluxConfig(outputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
622
844
  }
623
845
  }
624
846
  // ========================================
@@ -702,7 +924,7 @@ const release = new commander_1.Command("release")
702
924
  console.log(` App: ${appId}`);
703
925
  console.log(` Deployment: ${deploymentId}`);
704
926
  console.log(` Deployment Name: ${deploymentName}`);
705
- console.log(` Label: ${opts.label}`);
927
+ console.log(` Label: ${resolvedLabel}`);
706
928
  console.log(` Platform: ${platform}`);
707
929
  console.log(` Bundle: ${bundlePath}`);
708
930
  console.log(` Assets: ${assetFiles.length} file(s)`);
@@ -734,7 +956,7 @@ const release = new commander_1.Command("release")
734
956
  const initResponse = await apiClient_1.apiClient.post("/cli/releases/upload/init", {
735
957
  appId,
736
958
  deploymentId,
737
- releaseLabel: opts.label,
959
+ releaseLabel: resolvedLabel,
738
960
  platform,
739
961
  files: fileManifest.map(f => ({
740
962
  type: f.type,
@@ -780,10 +1002,88 @@ const release = new commander_1.Command("release")
780
1002
  }
781
1003
  }
782
1004
  }
1005
+ // Build expoMetadata if applicable
1006
+ let expoMetadata;
1007
+ let iosExpoMetadata;
1008
+ let androidExpoMetadata;
1009
+ let runtimeVersion = undefined;
1010
+ 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";
1012
+ const outputDir = path.resolve(opts.outputDir);
1013
+ const buildPlatformExpoMetadata = (targetPlatform, targetBundlePath, targetAssetsConfig) => {
1014
+ const manifestId = crypto.randomUUID();
1015
+ const bundleManifestItem = fileManifest.find(f => f.type === 'bundle' && (!targetPlatform || f.platform === targetPlatform));
1016
+ const isDual = platform === 'all';
1017
+ const bundleFilename = path.basename(targetBundlePath);
1018
+ const bundleLookupKey = (bundleManifestItem === null || bundleManifestItem === void 0 ? void 0 : bundleManifestItem.relativePath) || (bundleManifestItem === null || bundleManifestItem === void 0 ? void 0 : bundleManifestItem.filename) || bundleFilename;
1019
+ const bundleUrlItem = initResponse.data.urls.find(u => {
1020
+ if (isDual && targetPlatform && !u.key.includes(`/${targetPlatform}/`)) {
1021
+ return false;
1022
+ }
1023
+ return u.key.endsWith(bundleLookupKey) || path.posix.basename(u.key) === bundleFilename;
1024
+ });
1025
+ const launchAssetHash = computeBase64urlHash(targetBundlePath);
1026
+ const expoAssets = [];
1027
+ for (const config of targetAssetsConfig) {
1028
+ const localPath = path.join(outputDir, config.path);
1029
+ const manifestItem = fileManifest.find(f => {
1030
+ if (targetPlatform && f.platform && f.platform !== targetPlatform)
1031
+ return false;
1032
+ return f.relativePath === config.path || f.filename === path.basename(config.path);
1033
+ });
1034
+ const assetFilename = path.basename(config.path);
1035
+ const assetLookupKey = (manifestItem === null || manifestItem === void 0 ? void 0 : manifestItem.relativePath) || (manifestItem === null || manifestItem === void 0 ? void 0 : manifestItem.filename) || config.path;
1036
+ const urlItem = initResponse.data.urls.find(u => {
1037
+ if (isDual && targetPlatform && !u.key.includes(`/${targetPlatform}/`)) {
1038
+ return false;
1039
+ }
1040
+ return u.key.endsWith(assetLookupKey) || path.posix.basename(u.key) === assetFilename;
1041
+ });
1042
+ if (urlItem && fs.existsSync(localPath)) {
1043
+ const rawExt = config.ext || path.extname(localPath).replace(/^\./, '');
1044
+ const fileExtension = rawExt.startsWith('.') ? rawExt : `.${rawExt}`;
1045
+ expoAssets.push({
1046
+ key: path.basename(localPath),
1047
+ hash: computeBase64urlHash(localPath),
1048
+ contentType: (manifestItem === null || manifestItem === void 0 ? void 0 : manifestItem.contentType) || 'application/octet-stream',
1049
+ fileExtension,
1050
+ storageKey: urlItem.publicUrl,
1051
+ });
1052
+ }
1053
+ }
1054
+ return {
1055
+ manifestId,
1056
+ launchAsset: {
1057
+ key: path.basename(targetBundlePath),
1058
+ hash: launchAssetHash,
1059
+ contentType: 'application/javascript',
1060
+ storageKey: (bundleUrlItem === null || bundleUrlItem === void 0 ? void 0 : bundleUrlItem.publicUrl) || '',
1061
+ },
1062
+ assets: expoAssets,
1063
+ };
1064
+ };
1065
+ if (platform === 'all') {
1066
+ iosExpoMetadata = buildPlatformExpoMetadata('ios', iosBundlePath, iosExpoAssetsConfig);
1067
+ androidExpoMetadata = buildPlatformExpoMetadata('android', androidBundlePath, androidExpoAssetsConfig);
1068
+ expoMetadata = iosExpoMetadata;
1069
+ }
1070
+ else if (platform === 'ios') {
1071
+ iosExpoMetadata = buildPlatformExpoMetadata('ios', bundlePath, expoAssetsConfig);
1072
+ expoMetadata = iosExpoMetadata;
1073
+ }
1074
+ else if (platform === 'android') {
1075
+ androidExpoMetadata = buildPlatformExpoMetadata('android', bundlePath, expoAssetsConfig);
1076
+ expoMetadata = androidExpoMetadata;
1077
+ }
1078
+ else {
1079
+ expoMetadata = buildPlatformExpoMetadata(undefined, bundlePath, expoAssetsConfig);
1080
+ }
1081
+ }
783
1082
  // Step 3: Complete upload and create release record
784
- const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", Object.assign(Object.assign({ uploadId: initResponse.data.uploadId, releaseLabel: opts.label, mandatory: opts.mandatory, rolloutPercentage: rolloutValidation.value, binaryVersionRange: opts.versionRange }, (opts.minBinaryVersion ? { minBinaryVersion: opts.minBinaryVersion } : {})), { platform, isDraft: opts.schedule,
1083
+ const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", Object.assign(Object.assign(Object.assign({ uploadId: initResponse.data.uploadId, releaseLabel: resolvedLabel, mandatory: opts.mandatory, rolloutPercentage: rolloutValidation.value, binaryVersionRange: opts.versionRange }, (opts.minBinaryVersion ? { minBinaryVersion: opts.minBinaryVersion } : {})), { platform, isDraft: opts.schedule,
785
1084
  // Include bundle sizes
786
- iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), {
1085
+ iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), (isExpo ? Object.assign(Object.assign({ updateProtocol: 'expo', runtimeVersion,
1086
+ expoMetadata }, (iosExpoMetadata ? { iosExpoMetadata } : {})), (androidExpoMetadata ? { androidExpoMetadata } : {})) : { updateProtocol: 'upflux' })), {
787
1087
  headers: {
788
1088
  "x-publish-key": authKey,
789
1089
  "Content-Type": "application/json",
@@ -824,13 +1124,13 @@ const release = new commander_1.Command("release")
824
1124
  // Save git hash after successful release for next detection run
825
1125
  const gitStatus = (0, native_detector_1.getGitRepoStatus)();
826
1126
  if (gitStatus) {
827
- configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, opts.label);
1127
+ configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, resolvedLabel);
828
1128
  console.log(chalk_1.default.gray(` ⓘ Saved git snapshot for next release detection`));
829
1129
  }
830
1130
  }
831
1131
  catch (error) {
832
1132
  console.error(chalk_1.default.red(`\n✗ Failed to upload release: ${error.message}`));
833
- if ((_b = error.response) === null || _b === void 0 ? void 0 : _b.data) {
1133
+ if ((_f = error.response) === null || _f === void 0 ? void 0 : _f.data) {
834
1134
  console.error(chalk_1.default.gray(JSON.stringify(error.response.data, null, 2)));
835
1135
  }
836
1136
  process.exit(1);
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ const logout_1 = __importDefault(require("./commands/logout"));
10
10
  const release_1 = __importDefault(require("./commands/release"));
11
11
  const status_1 = __importDefault(require("./commands/status"));
12
12
  const generate_manifest_1 = __importDefault(require("./commands/generate-manifest"));
13
+ const init_1 = __importDefault(require("./commands/init"));
13
14
  const program = new commander_1.Command();
14
15
  program
15
16
  .name("upflux")
16
17
  .description("Upflux CLI for OTA updates")
17
18
  .version("1.0.0");
19
+ program.addCommand(init_1.default);
18
20
  program.addCommand(login_1.default);
19
21
  program.addCommand(logout_1.default);
20
22
  program.addCommand(release_1.default);
@@ -32,6 +32,7 @@ const KNOWN_NATIVE_PREFIXES = [
32
32
  */
33
33
  const NATIVE_FILES = [
34
34
  'android/app/build.gradle',
35
+ 'android/app/build.gradle.kts',
35
36
  'android/app/src/main/AndroidManifest.xml',
36
37
  'ios/Podfile',
37
38
  'ios/Podfile.lock',
@@ -89,22 +90,23 @@ function detectNativeChanges(savedHash) {
89
90
  if (!line.startsWith('+') || line.startsWith('+++'))
90
91
  continue;
91
92
  const added = line.slice(1);
92
- // ── android/app/build.gradle ──────────────────────────────────────
93
- if (currentFile === 'android/app/build.gradle') {
94
- const versionMatch = added.match(/versionName\s+["']([^"']+)["']/);
93
+ // ── android/app/build.gradle(.kts) ────────────────────────────────
94
+ if (currentFile === 'android/app/build.gradle' || currentFile === 'android/app/build.gradle.kts') {
95
+ const gradleFileLabel = currentFile.endsWith('.kts') ? 'build.gradle.kts' : 'build.gradle';
96
+ const versionMatch = added.match(/versionName\s*=?\s*["']([^"']+)["']/);
95
97
  if (versionMatch) {
96
98
  suggestedMinBinaryVersion = versionMatch[1];
97
99
  changes.push({
98
100
  category: 'android',
99
- file: 'build.gradle',
101
+ file: gradleFileLabel,
100
102
  description: `versionName → ${versionMatch[1]}`,
101
103
  });
102
104
  }
103
- const minSdkMatch = added.match(/minSdkVersion\s+(\d+)/);
105
+ const minSdkMatch = added.match(/minSdk(?:Version)?\s*=?\s*(\d+)/);
104
106
  if (minSdkMatch) {
105
107
  changes.push({
106
108
  category: 'android',
107
- file: 'build.gradle',
109
+ file: gradleFileLabel,
108
110
  description: `minSdkVersion → ${minSdkMatch[1]}`,
109
111
  });
110
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upfluxhq/cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Upflux CLI for managing OTA updates",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "upflux": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc -p ."
11
+ "build": "tsc -p tsconfig.build.json",
12
+ "test": "jest"
12
13
  },
13
14
  "keywords": [
14
15
  "upflux",
@@ -44,7 +45,10 @@
44
45
  "@types/archiver": "^7.0.0",
45
46
  "@types/form-data": "^2.5.2",
46
47
  "@types/inquirer": "^9.0.9",
48
+ "@types/jest": "^29.5.11",
47
49
  "@types/node": "^20.10.0",
50
+ "jest": "^29.7.0",
51
+ "ts-jest": "^29.4.15",
48
52
  "typescript": "^5.3.0"
49
53
  }
50
54
  }