@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.
- package/README.md +117 -248
- package/dist/client/apiClient.js +0 -1
- package/dist/client/directUpload.js +25 -3
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +196 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/release.js +334 -34
- package/dist/index.js +2 -0
- package/dist/utils/native-detector.js +8 -6
- package/package.json +6 -2
package/dist/commands/release.js
CHANGED
|
@@ -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
|
-
*
|
|
263
|
-
*
|
|
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
|
|
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
|
-
.
|
|
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
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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,
|
|
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-${
|
|
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 (
|
|
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,
|
|
576
|
-
writeUpfluxConfig(androidOutputDir,
|
|
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,
|
|
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: ${
|
|
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:
|
|
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:
|
|
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,
|
|
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 ((
|
|
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
|
|
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:
|
|
101
|
+
file: gradleFileLabel,
|
|
100
102
|
description: `versionName → ${versionMatch[1]}`,
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
|
-
const minSdkMatch = added.match(/
|
|
105
|
+
const minSdkMatch = added.match(/minSdk(?:Version)?\s*=?\s*(\d+)/);
|
|
104
106
|
if (minSdkMatch) {
|
|
105
107
|
changes.push({
|
|
106
108
|
category: 'android',
|
|
107
|
-
file:
|
|
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.
|
|
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
|
}
|