@upfluxhq/cli 1.2.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.
@@ -11,7 +11,6 @@ const configStore_1 = require("../config/configStore");
11
11
  */
12
12
  // const DEFAULT_BASE_URL = "http://localhost:3000/api";
13
13
  const DEFAULT_BASE_URL = "https://api.upflux.in/api";
14
- console.log(process.env.UPFLUX_API_URL || DEFAULT_BASE_URL);
15
14
  exports.apiClient = axios_1.default.create({
16
15
  baseURL: process.env.UPFLUX_API_URL || DEFAULT_BASE_URL,
17
16
  });
@@ -96,6 +96,16 @@ async function uploadFilesWithPresignedUrls(files, urls, options) {
96
96
  }
97
97
  }
98
98
  urlMap.set(relativePath, url);
99
+ // Also index by key after release path (releases/{appId}/{deploymentName}/{releaseLabel}/...)
100
+ const afterRelease = parts.slice(4).join('/');
101
+ if (afterRelease) {
102
+ urlMap.set(afterRelease, url);
103
+ }
104
+ // Filename fallback
105
+ const basename = path.posix.basename(url.key);
106
+ if (!urlMap.has(basename)) {
107
+ urlMap.set(basename, url);
108
+ }
99
109
  }
100
110
  // Upload files in batches
101
111
  let completed = 0;
@@ -103,7 +113,16 @@ async function uploadFilesWithPresignedUrls(files, urls, options) {
103
113
  const uploadFile = async (file) => {
104
114
  // Use relativePath for lookup if available, fallback to filename
105
115
  const lookupKey = file.relativePath || file.filename;
106
- const urlInfo = urlMap.get(lookupKey);
116
+ let urlInfo;
117
+ if (file.platform && urlMap.has(`${file.platform}/${lookupKey}`)) {
118
+ urlInfo = urlMap.get(`${file.platform}/${lookupKey}`);
119
+ }
120
+ else if (urlMap.has(lookupKey)) {
121
+ urlInfo = urlMap.get(lookupKey);
122
+ }
123
+ else if (file.filename && urlMap.has(file.filename)) {
124
+ urlInfo = urlMap.get(file.filename);
125
+ }
107
126
  if (!urlInfo) {
108
127
  throw new Error(`No presigned URL found for file: ${lookupKey}`);
109
128
  }
@@ -126,6 +145,7 @@ function guessContentType(filename) {
126
145
  var _a;
127
146
  const ext = (_a = filename.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
128
147
  const contentTypes = {
148
+ hbc: "application/javascript",
129
149
  bundle: "application/javascript",
130
150
  js: "application/javascript",
131
151
  json: "application/json",
@@ -159,8 +179,9 @@ assetsBaseDir, iosAssetsBaseDir, androidAssetsBaseDir) {
159
179
  type: "bundle",
160
180
  platform: "ios",
161
181
  filename: path.basename(iosBundlePath),
182
+ relativePath: "ios/" + path.basename(iosBundlePath),
162
183
  localPath: iosBundlePath,
163
- contentType: "application/javascript",
184
+ contentType: guessContentType(iosBundlePath),
164
185
  size: stats.size,
165
186
  });
166
187
  }
@@ -170,8 +191,9 @@ assetsBaseDir, iosAssetsBaseDir, androidAssetsBaseDir) {
170
191
  type: "bundle",
171
192
  platform: "android",
172
193
  filename: path.basename(androidBundlePath),
194
+ relativePath: "android/" + path.basename(androidBundlePath),
173
195
  localPath: androidBundlePath,
174
- contentType: "application/javascript",
196
+ contentType: guessContentType(androidBundlePath),
175
197
  size: stats.size,
176
198
  });
177
199
  }
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const UPFLUX_JSON_FILE = path_1.default.join(process.cwd(), "upflux.json");
12
+ const APP_JSON_FILE = path_1.default.join(process.cwd(), "app.json");
12
13
  const DEFAULT_SERVER_URL = "https://api.upflux.in";
13
14
  /**
14
15
  * Build the upflux.json content from collected answers
@@ -43,7 +44,23 @@ const init = new commander_1.Command("init")
43
44
  .description("Scaffold upflux.json in your project root")
44
45
  .option("--force", "Overwrite existing upflux.json")
45
46
  .action(async (opts) => {
47
+ var _a, _b;
46
48
  console.log(chalk_1.default.bold("\nšŸš€ Upflux Project Setup\n"));
49
+ // Detect Expo project
50
+ let isExpo = false;
51
+ let expoAppJson = null;
52
+ if (fs_1.default.existsSync(APP_JSON_FILE)) {
53
+ try {
54
+ const parsed = JSON.parse(fs_1.default.readFileSync(APP_JSON_FILE, "utf-8"));
55
+ if (parsed && parsed.expo) {
56
+ isExpo = true;
57
+ expoAppJson = parsed;
58
+ }
59
+ }
60
+ catch (_c) {
61
+ // Ignore parse errors
62
+ }
63
+ }
47
64
  // Check if upflux.json already exists
48
65
  if (fs_1.default.existsSync(UPFLUX_JSON_FILE) && !opts.force) {
49
66
  console.log(chalk_1.default.yellow("āš ļø upflux.json already exists."));
@@ -79,12 +96,15 @@ const init = new commander_1.Command("init")
79
96
  },
80
97
  ]);
81
98
  // 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))
100
+ ? expoAppJson.expo.version
101
+ : "1.0.0";
82
102
  const { runtimeVersion } = await inquirer_1.default.prompt([
83
103
  {
84
104
  type: "input",
85
105
  name: "runtimeVersion",
86
106
  message: "Runtime version (e.g. 1.0.0 — update when shipping a new binary):",
87
- default: "1.0.0",
107
+ default: defaultRuntimeVersion,
88
108
  validate: (val) => {
89
109
  if (!val.trim())
90
110
  return "Runtime version is required";
@@ -133,14 +153,41 @@ const init = new commander_1.Command("init")
133
153
  // Show what was created
134
154
  console.log(chalk_1.default.gray("Preview:"));
135
155
  console.log(chalk_1.default.gray(JSON.stringify(content, null, 2)));
136
- // Usage hint
137
- console.log(chalk_1.default.bold("\nšŸ“¦ SDK Usage:\n"));
138
- console.log(chalk_1.default.white(` import { Upflux } from '@upfluxhq/react-native';\n\n` +
139
- ` // Auto-selects 'development' in dev, 'production' in release\n` +
140
- ` // Auto-increments buildNumber on every release\n` +
141
- ` await Upflux.init();\n\n` +
142
- ` // Or specify a profile:\n` +
143
- ` await Upflux.init('production');`));
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({}, (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
+ }
144
191
  // Security note about committing the file
145
192
  console.log(chalk_1.default.cyan("\nšŸ’” Tip: upflux.json is safe to commit to git.\n" +
146
193
  " upx_cli_* keys are read-only — they cannot publish releases.\n" +
@@ -21,7 +21,7 @@ function validateKeyFormat(key) {
21
21
  return false;
22
22
  }
23
23
  // Check for common publish key prefixes
24
- const validPrefixes = ["upflux_pub_", "upflux_cli_", "pk_"];
24
+ const validPrefixes = ["UPX-PUB-", "UPX-CLT-", "upx_pub_", "upx_cli_", "upflux_pub_", "upflux_cli_", "pk_"];
25
25
  const hasValidPrefix = validPrefixes.some(prefix => key.startsWith(prefix));
26
26
  // Also allow generic keys that are alphanumeric with underscores
27
27
  const isAlphanumeric = /^[a-zA-Z0-9_-]+$/.test(key);
@@ -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
  */
@@ -358,10 +403,11 @@ const release = new commander_1.Command("release")
358
403
  .option("--schedule", "Create as draft, set schedule time in dashboard")
359
404
  .option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
360
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)")
361
407
  .option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
362
408
  .option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
363
409
  .action(async (opts) => {
364
- var _a, _b;
410
+ var _a, _b, _c, _d, _e, _f;
365
411
  try {
366
412
  // ========================================
367
413
  // VALIDATION PHASE
@@ -452,21 +498,31 @@ const release = new commander_1.Command("release")
452
498
  console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (manual)`));
453
499
  }
454
500
  else {
455
- // Auto mode: require upflux.json with buildNumber field
501
+ // Auto mode: require upflux.json with buildNumber field (or auto-generate for Expo)
456
502
  if (!rootJson) {
457
- console.error(chalk_1.default.red("āœ— No upflux.json found and no --label provided"));
458
- console.error(chalk_1.default.gray("Run: upflux init OR upflux release --label v1.0.0"));
459
- process.exit(1);
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
+ }
460
513
  }
461
- if (rootJson.app.buildNumber === undefined) {
462
- console.error(chalk_1.default.red("āœ— upflux.json is missing app.buildNumber"));
463
- console.error(chalk_1.default.gray('Add "buildNumber": 0 to the app section, or use --label'));
464
- process.exit(1);
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)`));
465
525
  }
466
- const nextBuild = incrementBuildNumber(rootJson);
467
- resolvedLabel = generateReleaseLabel(nextBuild, rootJson.app.runtimeVersion);
468
- console.log(chalk_1.default.green(` āœ“ buildNumber: ${nextBuild - 1} → ${nextBuild}`));
469
- console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (auto)`));
470
526
  }
471
527
  // Validation 6: Rollout percentage
472
528
  const rolloutValidation = isValidRollout(opts.rollout);
@@ -495,9 +551,118 @@ const release = new commander_1.Command("release")
495
551
  let assetsBaseDir;
496
552
  let iosAssetsBaseDir;
497
553
  let androidAssetsBaseDir;
554
+ // Expo-specific tracking
555
+ let expoAssetsConfig = [];
556
+ let iosExpoAssetsConfig = [];
557
+ let androidExpoAssetsConfig = [];
558
+ let expoBundleExt = "hbc";
498
559
  // Detect project type
499
560
  const isCapacitor = opts.capacitor || isCapacitorProject();
500
- 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) {
501
666
  // ========================================
502
667
  // CAPACITOR BUNDLING
503
668
  // ========================================
@@ -545,7 +710,7 @@ const release = new commander_1.Command("release")
545
710
  try {
546
711
  fs.accessSync(bundlePath, fs.constants.R_OK);
547
712
  }
548
- catch (_c) {
713
+ catch (_g) {
549
714
  console.error(chalk_1.default.red(`āœ— Cannot read bundle file: ${bundlePath}`));
550
715
  process.exit(1);
551
716
  }
@@ -837,10 +1002,88 @@ const release = new commander_1.Command("release")
837
1002
  }
838
1003
  }
839
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
+ }
840
1082
  // Step 3: Complete upload and create release record
841
- const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", 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,
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,
842
1084
  // Include bundle sizes
843
- 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' })), {
844
1087
  headers: {
845
1088
  "x-publish-key": authKey,
846
1089
  "Content-Type": "application/json",
@@ -887,7 +1130,7 @@ const release = new commander_1.Command("release")
887
1130
  }
888
1131
  catch (error) {
889
1132
  console.error(chalk_1.default.red(`\nāœ— Failed to upload release: ${error.message}`));
890
- 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) {
891
1134
  console.error(chalk_1.default.gray(JSON.stringify(error.response.data, null, 2)));
892
1135
  }
893
1136
  process.exit(1);
@@ -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.2.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
  }