@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.
package/dist/client/apiClient.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
196
|
+
contentType: guessContentType(androidBundlePath),
|
|
175
197
|
size: stats.size,
|
|
176
198
|
});
|
|
177
199
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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" +
|
package/dist/commands/login.js
CHANGED
|
@@ -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);
|
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
|
*/
|
|
@@ -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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ((
|
|
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
|
|
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
|
}
|