@upfluxhq/cli 0.1.0-beta.1 ā 0.1.0-beta.3
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 +156 -19
- package/dist/client/apiClient.js +2 -1
- package/dist/client/directUpload.d.ts +48 -0
- package/dist/client/directUpload.js +234 -0
- package/dist/commands/login.js +0 -5
- package/dist/commands/release.js +216 -79
- package/dist/commands/status.js +163 -59
- package/dist/config/configStore.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +10 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @upfluxhq/cli
|
|
2
2
|
|
|
3
|
-
Command-line interface for managing Upflux OTA updates.
|
|
3
|
+
Command-line interface for managing Upflux OTA updates for React Native and Capacitor apps.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ upflux login --key <staging-publish-key>
|
|
|
18
18
|
upflux login --key <production-publish-key>
|
|
19
19
|
|
|
20
20
|
# 2. Upload a release (will prompt if multiple deployments)
|
|
21
|
-
upflux release --
|
|
21
|
+
upflux release --label v1.0.0 --mandatory
|
|
22
22
|
|
|
23
23
|
# 3. Check release status
|
|
24
24
|
upflux status
|
|
@@ -40,9 +40,9 @@ upflux login --key <publishKey>
|
|
|
40
40
|
|
|
41
41
|
**Example output with multiple logins:**
|
|
42
42
|
```
|
|
43
|
+
š Verifying with server...
|
|
43
44
|
ā Login successful!
|
|
44
45
|
App ID: app-970384d3
|
|
45
|
-
Platform: android
|
|
46
46
|
Deployment: production
|
|
47
47
|
|
|
48
48
|
Stored credentials (2):
|
|
@@ -79,6 +79,10 @@ upflux logout --all
|
|
|
79
79
|
|
|
80
80
|
Bundle and upload a new release. **Prompts to select deployment** if multiple are stored.
|
|
81
81
|
|
|
82
|
+
Automatically detects project type:
|
|
83
|
+
- **React Native**: Uses `react-native bundle` command
|
|
84
|
+
- **Capacitor**: Zips the web directory (detected via `capacitor.config.*`)
|
|
85
|
+
|
|
82
86
|
```bash
|
|
83
87
|
upflux release [options]
|
|
84
88
|
```
|
|
@@ -86,8 +90,8 @@ upflux release [options]
|
|
|
86
90
|
| Option | Required | Description |
|
|
87
91
|
|--------|----------|-------------|
|
|
88
92
|
| `-l, --label <label>` | ā | Release label (e.g., v1.0.0) |
|
|
89
|
-
|
|
|
90
|
-
| `-a, --app <appId>` | | Application ID (auto-detected) |
|
|
93
|
+
| `--platform <platform>` | | Target platform: `all`, `ios`, or `android` (defaults to `all`) |
|
|
94
|
+
| `-a, --app <appId>` | | Application ID (auto-detected from credentials) |
|
|
91
95
|
| `-d, --deployment <deploymentId>` | | Deployment ID (prompts if multiple) |
|
|
92
96
|
| `-e, --entry-file <path>` | | Entry file for bundler (default: `index.js`) |
|
|
93
97
|
| `-o, --output-dir <path>` | | Output directory for bundle (default: `./dist`) |
|
|
@@ -98,21 +102,75 @@ upflux release [options]
|
|
|
98
102
|
| `-v, --version-range <range>` | | Binary version range (e.g., >=1.0.0) |
|
|
99
103
|
| `--dev` | | Create development bundle |
|
|
100
104
|
| `--skip-bundle` | | Skip bundling, use existing files in output dir |
|
|
105
|
+
| `--schedule` | | Create as draft, set schedule time in dashboard |
|
|
106
|
+
| `--capacitor` | | Force Capacitor mode (auto-detected if capacitor.config exists) |
|
|
107
|
+
| `--web-dir <path>` | | Web directory to bundle for Capacitor (default: from config or www) |
|
|
108
|
+
|
|
109
|
+
**Examples:**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Release to selected deployment (prompts if multiple)
|
|
113
|
+
upflux release --label v1.2.0
|
|
114
|
+
|
|
115
|
+
# iOS-only hotfix
|
|
116
|
+
upflux release --label v1.2.1-ios-fix --platform ios
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
# Android-only release with 50% rollout
|
|
119
|
+
upflux release --label v1.2.2 --platform android --rollout 50
|
|
120
|
+
|
|
121
|
+
# Mandatory update for all platforms
|
|
122
|
+
upflux release --label v1.3.0 --mandatory
|
|
123
|
+
|
|
124
|
+
# Schedule release (draft mode)
|
|
125
|
+
upflux release --label v1.4.0 --schedule
|
|
126
|
+
|
|
127
|
+
# Capacitor project with custom web directory
|
|
128
|
+
upflux release --label v1.0.0 --capacitor --web-dir dist
|
|
103
129
|
```
|
|
104
|
-
$ upflux release --platform android --label v1.2.0
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
**Example output:**
|
|
132
|
+
```
|
|
133
|
+
š¦ Bundling for iOS...
|
|
134
|
+
ā iOS bundle created
|
|
135
|
+
š¦ Bundling for Android...
|
|
136
|
+
ā Android bundle created
|
|
137
|
+
|
|
138
|
+
⬠Uploading release...
|
|
139
|
+
App: app-970384d3
|
|
140
|
+
Deployment: 6933e683aa32fe18a1311bbb
|
|
141
|
+
Deployment Name: production
|
|
142
|
+
Label: v1.2.0
|
|
143
|
+
Platform: all
|
|
144
|
+
Bundle: ./dist/ios/index.ios.bundle
|
|
145
|
+
Assets: 24 file(s)
|
|
146
|
+
Rollout: 100%
|
|
147
|
+
Mandatory: No
|
|
148
|
+
|
|
149
|
+
š Step 1/3: Requesting upload URLs...
|
|
150
|
+
ā Got 26 presigned URLs
|
|
151
|
+
š¤ Step 2/3: Uploading files directly to storage...
|
|
152
|
+
[1/26] index.ios.bundle
|
|
153
|
+
...
|
|
154
|
+
ā Uploaded 26 files
|
|
155
|
+
ā
Step 3/3: Confirming release...
|
|
156
|
+
|
|
157
|
+
ā Release uploaded successfully!
|
|
158
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
159
|
+
ID: 693507a19f6d6a79267633e8
|
|
160
|
+
Label: v1.2.0
|
|
161
|
+
iOS Bundle: uploaded
|
|
162
|
+
Android Bundle: uploaded
|
|
163
|
+
Rollout: 100%
|
|
164
|
+
Mandatory: No
|
|
165
|
+
Status: active
|
|
166
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
109
167
|
```
|
|
110
168
|
|
|
111
169
|
---
|
|
112
170
|
|
|
113
171
|
### `status`
|
|
114
172
|
|
|
115
|
-
Check
|
|
173
|
+
Check deployment status and recent releases.
|
|
116
174
|
|
|
117
175
|
```bash
|
|
118
176
|
upflux status [options]
|
|
@@ -120,9 +178,28 @@ upflux status [options]
|
|
|
120
178
|
|
|
121
179
|
| Option | Description |
|
|
122
180
|
|--------|-------------|
|
|
123
|
-
| `-
|
|
124
|
-
|
|
125
|
-
|
|
181
|
+
| `-n, --limit <number>` | Number of releases to show (default: 5) |
|
|
182
|
+
|
|
183
|
+
**Example output:**
|
|
184
|
+
```
|
|
185
|
+
ā” Fetching deployment status...
|
|
186
|
+
|
|
187
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā®
|
|
188
|
+
ā š± app-970384d3 ā production ā
|
|
189
|
+
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
|
|
190
|
+
ā Current Release: v1.2.0 (iOS & Android) ā
|
|
191
|
+
ā Status: ā active ⢠100% rollout ā
|
|
192
|
+
ā Published: 2h ago ā
|
|
193
|
+
ā°āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāÆ
|
|
194
|
+
|
|
195
|
+
š¦ Recent Releases (15 total)
|
|
196
|
+
|
|
197
|
+
Label Platform Status Rollout Published
|
|
198
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
199
|
+
v1.2.0 all ā active 100% 2h ago
|
|
200
|
+
v1.1.0 all ā expired 100% 3d ago
|
|
201
|
+
v1.0.0 ios ā expired 100% 1w ago
|
|
202
|
+
```
|
|
126
203
|
|
|
127
204
|
---
|
|
128
205
|
|
|
@@ -139,6 +216,50 @@ upflux generate-manifest [options]
|
|
|
139
216
|
| `-r, --release-id <id>` | ā | Release ID (MongoDB ObjectId) |
|
|
140
217
|
| `-o, --output <path>` | | Save manifest to file |
|
|
141
218
|
|
|
219
|
+
**Example:**
|
|
220
|
+
```bash
|
|
221
|
+
# Print manifest to stdout
|
|
222
|
+
upflux generate-manifest --release-id 693507a19f6d6a79267633e8
|
|
223
|
+
|
|
224
|
+
# Save to file
|
|
225
|
+
upflux generate-manifest --release-id 693507a19f6d6a79267633e8 --output manifest.json
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Scheduled Releases
|
|
231
|
+
|
|
232
|
+
Schedule releases to go live at a specific date and time. Perfect for coordinating updates with marketing campaigns, feature launches, or maintenance windows.
|
|
233
|
+
|
|
234
|
+
### How It Works
|
|
235
|
+
|
|
236
|
+
1. Create a release with the `--schedule` flag
|
|
237
|
+
2. The release is uploaded as a **Draft**
|
|
238
|
+
3. Set the scheduled time in the Upflux Dashboard
|
|
239
|
+
4. The release automatically goes live at the scheduled time
|
|
240
|
+
|
|
241
|
+
### Creating a Scheduled Release
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Create a draft release for scheduling
|
|
245
|
+
upflux release --label v2.0.0 --schedule
|
|
246
|
+
|
|
247
|
+
# Output:
|
|
248
|
+
# ā Release uploaded successfully!
|
|
249
|
+
# ...
|
|
250
|
+
# Status: DRAFT
|
|
251
|
+
# š
Set schedule time in dashboard
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Use Cases
|
|
255
|
+
|
|
256
|
+
- **Feature launches** - Coordinate with marketing announcements
|
|
257
|
+
- **Global releases** - Release at optimal times for different regions
|
|
258
|
+
- **Maintenance windows** - Deploy during low-traffic periods
|
|
259
|
+
- **Campaign coordination** - Sync with promotional events
|
|
260
|
+
|
|
261
|
+
> š” **Tip:** Combine scheduled releases with rollout percentages for safer deployments. For example, schedule a 10% rollout first, then manually increase after monitoring.
|
|
262
|
+
|
|
142
263
|
---
|
|
143
264
|
|
|
144
265
|
## Configuration
|
|
@@ -147,7 +268,7 @@ upflux generate-manifest [options]
|
|
|
147
268
|
|
|
148
269
|
| Variable | Description | Default |
|
|
149
270
|
|----------|-------------|---------|
|
|
150
|
-
| `UPFLUX_API_URL` | API server URL | `
|
|
271
|
+
| `UPFLUX_API_URL` | API server URL | `https://api.upflux.in/api` |
|
|
151
272
|
|
|
152
273
|
### Auth File
|
|
153
274
|
|
|
@@ -157,16 +278,14 @@ The CLI stores credentials in `.upflux` in the current directory:
|
|
|
157
278
|
{
|
|
158
279
|
"credentials": [
|
|
159
280
|
{
|
|
160
|
-
"key": "
|
|
281
|
+
"key": "upflux_pub_xxx...",
|
|
161
282
|
"appId": "app-970384d3",
|
|
162
|
-
"platform": "android",
|
|
163
283
|
"deploymentId": "6933e683aa32fe18a1311bbb",
|
|
164
284
|
"deploymentName": "production"
|
|
165
285
|
},
|
|
166
286
|
{
|
|
167
|
-
"key": "
|
|
287
|
+
"key": "upflux_pub_yyy...",
|
|
168
288
|
"appId": "app-970384d3",
|
|
169
|
-
"platform": "android",
|
|
170
289
|
"deploymentId": "6933e683aa32fe18a1311ccc",
|
|
171
290
|
"deploymentName": "staging"
|
|
172
291
|
}
|
|
@@ -175,6 +294,18 @@ The CLI stores credentials in `.upflux` in the current directory:
|
|
|
175
294
|
}
|
|
176
295
|
```
|
|
177
296
|
|
|
297
|
+
> ā ļø **Important:** Never commit the `.upflux` file to version control. It contains your publish keys. Add it to your `.gitignore`.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Publish Keys
|
|
302
|
+
|
|
303
|
+
Publish keys are used to authenticate the CLI and can be found in the Upflux dashboard under **Settings ā Keys**.
|
|
304
|
+
|
|
305
|
+
Key format: `upflux_pub_<prefix>_<secret>`
|
|
306
|
+
|
|
307
|
+
Example: `upflux_pub_prod_a1b2c3d4e5f6...`
|
|
308
|
+
|
|
178
309
|
---
|
|
179
310
|
|
|
180
311
|
## Development
|
|
@@ -190,6 +321,12 @@ pnpm run build
|
|
|
190
321
|
node dist/index.js --help
|
|
191
322
|
```
|
|
192
323
|
|
|
324
|
+
## Requirements
|
|
325
|
+
|
|
326
|
+
- Node.js >= 18.0.0
|
|
327
|
+
- For React Native projects: `react-native` CLI installed
|
|
328
|
+
- For Capacitor projects: Built web assets in configured `webDir`
|
|
329
|
+
|
|
193
330
|
## License
|
|
194
331
|
|
|
195
332
|
MIT
|
package/dist/client/apiClient.js
CHANGED
|
@@ -9,7 +9,8 @@ const configStore_1 = require("../config/configStore");
|
|
|
9
9
|
/**
|
|
10
10
|
* Default API base URL (can be overridden via environment variable)
|
|
11
11
|
*/
|
|
12
|
-
const DEFAULT_BASE_URL = "
|
|
12
|
+
// const DEFAULT_BASE_URL = "http://localhost:3000/api";
|
|
13
|
+
const DEFAULT_BASE_URL = "https://api.upflux.in/api";
|
|
13
14
|
console.log(process.env.UPFLUX_API_URL || DEFAULT_BASE_URL);
|
|
14
15
|
exports.apiClient = axios_1.default.create({
|
|
15
16
|
baseURL: process.env.UPFLUX_API_URL || DEFAULT_BASE_URL,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Upload Module
|
|
3
|
+
*
|
|
4
|
+
* Uploads files directly to R2/S3 using presigned URLs,
|
|
5
|
+
* bypassing Vercel's 4.5MB body size limit.
|
|
6
|
+
*/
|
|
7
|
+
interface UploadProgress {
|
|
8
|
+
filename: string;
|
|
9
|
+
loaded: number;
|
|
10
|
+
total: number;
|
|
11
|
+
percent: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Upload a single file to a presigned URL
|
|
15
|
+
*/
|
|
16
|
+
export declare function uploadToPresignedUrl(filePath: string, presignedUrl: string, contentType?: string, onProgress?: (progress: UploadProgress) => void): Promise<void>;
|
|
17
|
+
interface FileManifestItem {
|
|
18
|
+
type: "bundle" | "asset";
|
|
19
|
+
platform?: "ios" | "android";
|
|
20
|
+
filename: string;
|
|
21
|
+
relativePath?: string;
|
|
22
|
+
localPath: string;
|
|
23
|
+
contentType?: string;
|
|
24
|
+
size?: number;
|
|
25
|
+
}
|
|
26
|
+
interface PresignedUrlResponse {
|
|
27
|
+
key: string;
|
|
28
|
+
uploadUrl: string;
|
|
29
|
+
publicUrl: string;
|
|
30
|
+
expiresIn: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Upload multiple files in parallel using presigned URLs
|
|
34
|
+
*/
|
|
35
|
+
export declare function uploadFilesWithPresignedUrls(files: FileManifestItem[], urls: PresignedUrlResponse[], options?: {
|
|
36
|
+
concurrency?: number;
|
|
37
|
+
onFileComplete?: (filename: string, index: number, total: number) => void;
|
|
38
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Guess content type from filename
|
|
42
|
+
*/
|
|
43
|
+
export declare function guessContentType(filename: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build file manifest from local bundle and asset paths
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildFileManifest(platform: "ios" | "android" | "all", bundlePath?: string, assetFiles?: string[], iosBundlePath?: string, androidBundlePath?: string, iosAssetFiles?: string[], androidAssetFiles?: string[], assetsBaseDir?: string, iosAssetsBaseDir?: string, androidAssetsBaseDir?: string): FileManifestItem[];
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Direct Upload Module
|
|
4
|
+
*
|
|
5
|
+
* Uploads files directly to R2/S3 using presigned URLs,
|
|
6
|
+
* bypassing Vercel's 4.5MB body size limit.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.uploadToPresignedUrl = uploadToPresignedUrl;
|
|
46
|
+
exports.uploadFilesWithPresignedUrls = uploadFilesWithPresignedUrls;
|
|
47
|
+
exports.guessContentType = guessContentType;
|
|
48
|
+
exports.buildFileManifest = buildFileManifest;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const axios_1 = __importDefault(require("axios"));
|
|
52
|
+
/**
|
|
53
|
+
* Upload a single file to a presigned URL
|
|
54
|
+
*/
|
|
55
|
+
async function uploadToPresignedUrl(filePath, presignedUrl, contentType = "application/octet-stream", onProgress) {
|
|
56
|
+
const stats = fs.statSync(filePath);
|
|
57
|
+
const fileStream = fs.createReadStream(filePath);
|
|
58
|
+
const filename = path.basename(filePath);
|
|
59
|
+
await axios_1.default.put(presignedUrl, fileStream, {
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": contentType,
|
|
62
|
+
"Content-Length": stats.size,
|
|
63
|
+
},
|
|
64
|
+
maxContentLength: Infinity,
|
|
65
|
+
maxBodyLength: Infinity,
|
|
66
|
+
onUploadProgress: (progressEvent) => {
|
|
67
|
+
if (onProgress && progressEvent.total) {
|
|
68
|
+
onProgress({
|
|
69
|
+
filename,
|
|
70
|
+
loaded: progressEvent.loaded,
|
|
71
|
+
total: progressEvent.total,
|
|
72
|
+
percent: Math.round((progressEvent.loaded / progressEvent.total) * 100),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Upload multiple files in parallel using presigned URLs
|
|
80
|
+
*/
|
|
81
|
+
async function uploadFilesWithPresignedUrls(files, urls, options) {
|
|
82
|
+
const { concurrency = 3, onFileComplete, onProgress } = options || {};
|
|
83
|
+
// Create a map of relativePath (or filename) to URL
|
|
84
|
+
// This handles duplicate filenames in different folders (e.g., drawable-mdpi/logo.png vs drawable-xhdpi/logo.png)
|
|
85
|
+
const urlMap = new Map();
|
|
86
|
+
for (const url of urls) {
|
|
87
|
+
// Extract relativePath from key (everything after release path)
|
|
88
|
+
// Key format: releases/appId/deployment/release/assets/drawable-mdpi/logo.png
|
|
89
|
+
const parts = url.key.split("/");
|
|
90
|
+
// Find where assets/ or android/ or ios/ starts to get the relativePath
|
|
91
|
+
let relativePath = parts[parts.length - 1]; // fallback to filename
|
|
92
|
+
for (let i = 0; i < parts.length; i++) {
|
|
93
|
+
if (parts[i] === 'assets' || parts[i] === 'android' || parts[i] === 'ios') {
|
|
94
|
+
relativePath = parts.slice(i).join('/');
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
urlMap.set(relativePath, url);
|
|
99
|
+
}
|
|
100
|
+
// Upload files in batches
|
|
101
|
+
let completed = 0;
|
|
102
|
+
const total = files.length;
|
|
103
|
+
const uploadFile = async (file) => {
|
|
104
|
+
// Use relativePath for lookup if available, fallback to filename
|
|
105
|
+
const lookupKey = file.relativePath || file.filename;
|
|
106
|
+
const urlInfo = urlMap.get(lookupKey);
|
|
107
|
+
if (!urlInfo) {
|
|
108
|
+
throw new Error(`No presigned URL found for file: ${lookupKey}`);
|
|
109
|
+
}
|
|
110
|
+
await uploadToPresignedUrl(file.localPath, urlInfo.uploadUrl, file.contentType, onProgress);
|
|
111
|
+
completed++;
|
|
112
|
+
if (onFileComplete) {
|
|
113
|
+
onFileComplete(file.filename, completed, total);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Process in batches for controlled concurrency
|
|
117
|
+
for (let i = 0; i < files.length; i += concurrency) {
|
|
118
|
+
const batch = files.slice(i, i + concurrency);
|
|
119
|
+
await Promise.all(batch.map(uploadFile));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Guess content type from filename
|
|
124
|
+
*/
|
|
125
|
+
function guessContentType(filename) {
|
|
126
|
+
var _a;
|
|
127
|
+
const ext = (_a = filename.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
128
|
+
const contentTypes = {
|
|
129
|
+
bundle: "application/javascript",
|
|
130
|
+
js: "application/javascript",
|
|
131
|
+
json: "application/json",
|
|
132
|
+
zip: "application/zip",
|
|
133
|
+
png: "image/png",
|
|
134
|
+
jpg: "image/jpeg",
|
|
135
|
+
jpeg: "image/jpeg",
|
|
136
|
+
gif: "image/gif",
|
|
137
|
+
svg: "image/svg+xml",
|
|
138
|
+
webp: "image/webp",
|
|
139
|
+
ttf: "font/ttf",
|
|
140
|
+
woff: "font/woff",
|
|
141
|
+
woff2: "font/woff2",
|
|
142
|
+
css: "text/css",
|
|
143
|
+
html: "text/html",
|
|
144
|
+
};
|
|
145
|
+
return contentTypes[ext || ""] || "application/octet-stream";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build file manifest from local bundle and asset paths
|
|
149
|
+
*/
|
|
150
|
+
function buildFileManifest(platform, bundlePath, assetFiles, iosBundlePath, androidBundlePath, iosAssetFiles, androidAssetFiles,
|
|
151
|
+
// Base directories for calculating relative paths
|
|
152
|
+
assetsBaseDir, iosAssetsBaseDir, androidAssetsBaseDir) {
|
|
153
|
+
const files = [];
|
|
154
|
+
if (platform === "all") {
|
|
155
|
+
// Dual-bundle mode
|
|
156
|
+
if (iosBundlePath) {
|
|
157
|
+
const stats = fs.statSync(iosBundlePath);
|
|
158
|
+
files.push({
|
|
159
|
+
type: "bundle",
|
|
160
|
+
platform: "ios",
|
|
161
|
+
filename: path.basename(iosBundlePath),
|
|
162
|
+
localPath: iosBundlePath,
|
|
163
|
+
contentType: "application/javascript",
|
|
164
|
+
size: stats.size,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (androidBundlePath) {
|
|
168
|
+
const stats = fs.statSync(androidBundlePath);
|
|
169
|
+
files.push({
|
|
170
|
+
type: "bundle",
|
|
171
|
+
platform: "android",
|
|
172
|
+
filename: path.basename(androidBundlePath),
|
|
173
|
+
localPath: androidBundlePath,
|
|
174
|
+
contentType: "application/javascript",
|
|
175
|
+
size: stats.size,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
for (const assetPath of iosAssetFiles || []) {
|
|
179
|
+
// Calculate relative path from assets base directory
|
|
180
|
+
const relativePath = iosAssetsBaseDir
|
|
181
|
+
? path.relative(iosAssetsBaseDir, assetPath)
|
|
182
|
+
: path.basename(assetPath);
|
|
183
|
+
files.push({
|
|
184
|
+
type: "asset",
|
|
185
|
+
platform: "ios",
|
|
186
|
+
filename: path.basename(assetPath),
|
|
187
|
+
relativePath,
|
|
188
|
+
localPath: assetPath,
|
|
189
|
+
contentType: guessContentType(assetPath),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const assetPath of androidAssetFiles || []) {
|
|
193
|
+
// Calculate relative path from assets base directory
|
|
194
|
+
const relativePath = androidAssetsBaseDir
|
|
195
|
+
? path.relative(androidAssetsBaseDir, assetPath)
|
|
196
|
+
: path.basename(assetPath);
|
|
197
|
+
files.push({
|
|
198
|
+
type: "asset",
|
|
199
|
+
platform: "android",
|
|
200
|
+
filename: path.basename(assetPath),
|
|
201
|
+
relativePath,
|
|
202
|
+
localPath: assetPath,
|
|
203
|
+
contentType: guessContentType(assetPath),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// Single-bundle mode
|
|
209
|
+
if (bundlePath) {
|
|
210
|
+
const stats = fs.statSync(bundlePath);
|
|
211
|
+
files.push({
|
|
212
|
+
type: "bundle",
|
|
213
|
+
filename: path.basename(bundlePath),
|
|
214
|
+
localPath: bundlePath,
|
|
215
|
+
contentType: "application/javascript",
|
|
216
|
+
size: stats.size,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const assetPath of assetFiles || []) {
|
|
220
|
+
// Calculate relative path from assets base directory
|
|
221
|
+
const relativePath = assetsBaseDir
|
|
222
|
+
? path.relative(assetsBaseDir, assetPath)
|
|
223
|
+
: path.basename(assetPath);
|
|
224
|
+
files.push({
|
|
225
|
+
type: "asset",
|
|
226
|
+
filename: path.basename(assetPath),
|
|
227
|
+
relativePath,
|
|
228
|
+
localPath: assetPath,
|
|
229
|
+
contentType: guessContentType(assetPath),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return files;
|
|
234
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -40,7 +40,6 @@ async function verifyKeyWithServer(key) {
|
|
|
40
40
|
return {
|
|
41
41
|
valid: true,
|
|
42
42
|
appId: response.data.appId,
|
|
43
|
-
platform: response.data.platform,
|
|
44
43
|
deploymentId: response.data.deploymentId,
|
|
45
44
|
deploymentName: response.data.deploymentName,
|
|
46
45
|
};
|
|
@@ -90,7 +89,6 @@ const login = new commander_1.Command("login")
|
|
|
90
89
|
const credential = {
|
|
91
90
|
key,
|
|
92
91
|
appId: verification.appId,
|
|
93
|
-
platform: verification.platform,
|
|
94
92
|
deploymentId: verification.deploymentId,
|
|
95
93
|
deploymentName: verification.deploymentName,
|
|
96
94
|
};
|
|
@@ -101,9 +99,6 @@ const login = new commander_1.Command("login")
|
|
|
101
99
|
? `${verification.deploymentName}`
|
|
102
100
|
: verification.deploymentId;
|
|
103
101
|
console.log(chalk_1.default.gray(` App ID: ${verification.appId}`));
|
|
104
|
-
if (verification.platform) {
|
|
105
|
-
console.log(chalk_1.default.gray(` Platform: ${verification.platform}`));
|
|
106
|
-
}
|
|
107
102
|
console.log(chalk_1.default.gray(` Deployment: ${deploymentDisplay}`));
|
|
108
103
|
// Show all stored credentials
|
|
109
104
|
const allCreds = configStore_1.configStore.getCredentials();
|
package/dist/commands/release.js
CHANGED
|
@@ -40,7 +40,6 @@ const commander_1 = require("commander");
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
|
-
const form_data_1 = __importDefault(require("form-data"));
|
|
44
43
|
const chalk_1 = __importDefault(require("chalk"));
|
|
45
44
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
46
45
|
const archiver_1 = __importDefault(require("archiver"));
|
|
@@ -215,7 +214,6 @@ function runBundler(options) {
|
|
|
215
214
|
if (!fs.existsSync(outputDir)) {
|
|
216
215
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
217
216
|
}
|
|
218
|
-
console.log(chalk_1.default.blue(`š¦ Bundling for ${platform}...`));
|
|
219
217
|
const command = [
|
|
220
218
|
"npx react-native bundle",
|
|
221
219
|
`--platform ${platform}`,
|
|
@@ -261,7 +259,7 @@ const release = new commander_1.Command("release")
|
|
|
261
259
|
.option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
|
|
262
260
|
.option("-d, --deployment <deploymentId>", "Deployment ID (uses stored value if not provided)")
|
|
263
261
|
.requiredOption("-l, --label <label>", "Release label (e.g., v1.0.0)")
|
|
264
|
-
.option("
|
|
262
|
+
.option("--platform <platform>", "Target platform for the release: all, ios, or android (defaults to all)", "all")
|
|
265
263
|
.option("-e, --entry-file <path>", "Entry file for bundler", DEFAULT_ENTRY_FILE)
|
|
266
264
|
.option("-o, --output-dir <path>", "Output directory for bundle", DEFAULT_OUTPUT_DIR)
|
|
267
265
|
.option("-b, --bundle <path>", "Skip bundling, use existing bundle file")
|
|
@@ -275,7 +273,7 @@ const release = new commander_1.Command("release")
|
|
|
275
273
|
.option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
|
|
276
274
|
.option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
|
|
277
275
|
.action(async (opts) => {
|
|
278
|
-
var _a;
|
|
276
|
+
var _a, _b;
|
|
279
277
|
try {
|
|
280
278
|
// ========================================
|
|
281
279
|
// VALIDATION PHASE
|
|
@@ -312,30 +310,23 @@ const release = new commander_1.Command("release")
|
|
|
312
310
|
else {
|
|
313
311
|
// Multiple credentials - prompt for selection
|
|
314
312
|
selectedCredential = await selectDeployment(credentials);
|
|
313
|
+
// Update activeIndex so subsequent commands use the same deployment
|
|
314
|
+
const selectedIndex = credentials.findIndex(c => c.deploymentId === selectedCredential.deploymentId);
|
|
315
|
+
if (selectedIndex >= 0) {
|
|
316
|
+
configStore_1.configStore.setActiveIndex(selectedIndex);
|
|
317
|
+
}
|
|
315
318
|
}
|
|
316
319
|
const appId = selectedCredential.appId;
|
|
317
320
|
const deploymentId = selectedCredential.deploymentId;
|
|
318
321
|
const authKey = selectedCredential.key;
|
|
319
322
|
const deploymentName = selectedCredential.deploymentName;
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
if (!platform) {
|
|
323
|
-
if (selectedCredential.platform) {
|
|
324
|
-
platform = selectedCredential.platform;
|
|
325
|
-
console.log(chalk_1.default.gray(`Using stored platform: ${platform}`));
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
console.error(chalk_1.default.red("ā Platform is required"));
|
|
329
|
-
console.error(chalk_1.default.gray("Provide --platform <ios|android> or re-login to store it."));
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (!VALID_PLATFORMS.includes(platform.toLowerCase())) {
|
|
323
|
+
// Validate platform
|
|
324
|
+
const platform = ((_a = opts.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || 'all';
|
|
325
|
+
if (!['all', 'ios', 'android'].includes(platform)) {
|
|
334
326
|
console.error(chalk_1.default.red(`ā Invalid platform: ${platform}`));
|
|
335
|
-
console.error(chalk_1.default.gray(
|
|
327
|
+
console.error(chalk_1.default.gray("Valid values: all, ios, android"));
|
|
336
328
|
process.exit(1);
|
|
337
329
|
}
|
|
338
|
-
platform = platform.toLowerCase();
|
|
339
330
|
// Validate app ID if provided matches selected credential
|
|
340
331
|
if (opts.app && opts.app !== appId) {
|
|
341
332
|
console.error(chalk_1.default.red(`ā App ID mismatch`));
|
|
@@ -376,6 +367,16 @@ const release = new commander_1.Command("release")
|
|
|
376
367
|
// ========================================
|
|
377
368
|
let bundlePath;
|
|
378
369
|
let assetFiles = [];
|
|
370
|
+
// Dual-bundle variables
|
|
371
|
+
let iosBundlePath;
|
|
372
|
+
let androidBundlePath;
|
|
373
|
+
let iosAssetFiles = [];
|
|
374
|
+
let androidAssetFiles = [];
|
|
375
|
+
// Asset directories for calculating relative paths (preserves drawable-* structure)
|
|
376
|
+
// These point to the platform output directory (e.g., dist/android/) so paths include assets/
|
|
377
|
+
let assetsBaseDir;
|
|
378
|
+
let iosAssetsBaseDir;
|
|
379
|
+
let androidAssetsBaseDir;
|
|
379
380
|
// Detect project type
|
|
380
381
|
const isCapacitor = opts.capacitor || isCapacitorProject();
|
|
381
382
|
if (isCapacitor) {
|
|
@@ -423,7 +424,7 @@ const release = new commander_1.Command("release")
|
|
|
423
424
|
try {
|
|
424
425
|
fs.accessSync(bundlePath, fs.constants.R_OK);
|
|
425
426
|
}
|
|
426
|
-
catch (
|
|
427
|
+
catch (_c) {
|
|
427
428
|
console.error(chalk_1.default.red(`ā Cannot read bundle file: ${bundlePath}`));
|
|
428
429
|
process.exit(1);
|
|
429
430
|
}
|
|
@@ -449,37 +450,104 @@ const release = new commander_1.Command("release")
|
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
const outputDir = path.resolve(opts.outputDir);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
453
|
+
if (platform === 'all') {
|
|
454
|
+
// ========================================
|
|
455
|
+
// DUAL BUNDLING (iOS + Android)
|
|
456
|
+
// ========================================
|
|
457
|
+
if (!opts.skipBundle) {
|
|
458
|
+
// Bundle for iOS
|
|
459
|
+
console.log(chalk_1.default.blue("š¦ Bundling for iOS..."));
|
|
460
|
+
const iosOutputDir = path.join(outputDir, 'ios');
|
|
461
|
+
fs.mkdirSync(iosOutputDir, { recursive: true });
|
|
462
|
+
const iosResult = runBundler({
|
|
463
|
+
platform: 'ios',
|
|
464
|
+
entryFile: opts.entryFile,
|
|
465
|
+
outputDir: iosOutputDir,
|
|
466
|
+
dev: opts.dev,
|
|
467
|
+
});
|
|
468
|
+
iosBundlePath = iosResult.bundlePath;
|
|
469
|
+
console.log(chalk_1.default.green(` ā iOS bundle created`));
|
|
470
|
+
// Bundle for Android
|
|
471
|
+
console.log(chalk_1.default.blue("š¦ Bundling for Android..."));
|
|
472
|
+
const androidOutputDir = path.join(outputDir, 'android');
|
|
473
|
+
fs.mkdirSync(androidOutputDir, { recursive: true });
|
|
474
|
+
const androidResult = runBundler({
|
|
475
|
+
platform: 'android',
|
|
476
|
+
entryFile: opts.entryFile,
|
|
477
|
+
outputDir: androidOutputDir,
|
|
478
|
+
dev: opts.dev,
|
|
479
|
+
});
|
|
480
|
+
androidBundlePath = androidResult.bundlePath;
|
|
481
|
+
console.log(chalk_1.default.green(` ā Android bundle created`));
|
|
459
482
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
483
|
+
else {
|
|
484
|
+
// Skip bundle - use existing
|
|
485
|
+
const iosOutputDir = path.join(outputDir, 'ios');
|
|
486
|
+
const androidOutputDir = path.join(outputDir, 'android');
|
|
487
|
+
iosBundlePath = path.join(iosOutputDir, 'index.ios.bundle');
|
|
488
|
+
androidBundlePath = path.join(androidOutputDir, 'index.android.bundle');
|
|
489
|
+
if (!fs.existsSync(iosBundlePath) || !fs.existsSync(androidBundlePath)) {
|
|
490
|
+
console.error(chalk_1.default.red(`ā Bundle not found for both platforms`));
|
|
491
|
+
console.error(chalk_1.default.gray(`Expected: ${iosBundlePath} and ${androidBundlePath}`));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Collect assets for iOS - base dir is the ios output dir so paths include assets/
|
|
496
|
+
const iosOutputDir = path.dirname(iosBundlePath);
|
|
497
|
+
iosAssetsBaseDir = iosOutputDir;
|
|
498
|
+
const iosAssetsPath = path.join(iosOutputDir, 'assets');
|
|
499
|
+
iosAssetFiles = opts.assets
|
|
500
|
+
? opts.assets.map((p) => path.resolve(p))
|
|
501
|
+
: collectAssets(iosAssetsPath);
|
|
502
|
+
// Collect assets for Android - base dir is the android output dir so paths include assets/
|
|
503
|
+
const androidOutputDir = path.dirname(androidBundlePath);
|
|
504
|
+
androidAssetsBaseDir = androidOutputDir;
|
|
505
|
+
const androidAssetsPath = path.join(androidOutputDir, 'assets');
|
|
506
|
+
androidAssetFiles = opts.assets
|
|
507
|
+
? opts.assets.map((p) => path.resolve(p))
|
|
508
|
+
: collectAssets(androidAssetsPath);
|
|
509
|
+
// Set bundlePath and assetFiles for display (use iOS as primary for logging)
|
|
510
|
+
bundlePath = iosBundlePath;
|
|
511
|
+
assetFiles = [...iosAssetFiles, ...androidAssetFiles];
|
|
474
512
|
}
|
|
475
513
|
else {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
514
|
+
// ========================================
|
|
515
|
+
// SINGLE PLATFORM BUNDLING
|
|
516
|
+
// ========================================
|
|
517
|
+
const expectedBundle = path.join(outputDir, `index.${platform}.bundle`);
|
|
518
|
+
// Base dir is outputDir so relative paths include assets/ folder
|
|
519
|
+
assetsBaseDir = outputDir;
|
|
520
|
+
const assetsPath = path.join(outputDir, "assets");
|
|
521
|
+
if (opts.skipBundle) {
|
|
522
|
+
if (!fs.existsSync(expectedBundle)) {
|
|
523
|
+
console.error(chalk_1.default.red(`ā Bundle not found: ${expectedBundle}`));
|
|
524
|
+
console.error(chalk_1.default.gray("Run without --skip-bundle to create a new bundle."));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
bundlePath = expectedBundle;
|
|
528
|
+
console.log(chalk_1.default.blue("ā Skipping bundle, using existing files..."));
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const result = runBundler({
|
|
532
|
+
platform: platform,
|
|
533
|
+
entryFile: opts.entryFile,
|
|
534
|
+
outputDir,
|
|
535
|
+
dev: opts.dev,
|
|
536
|
+
});
|
|
537
|
+
bundlePath = result.bundlePath;
|
|
538
|
+
}
|
|
539
|
+
if (opts.assets) {
|
|
540
|
+
assetFiles = opts.assets.map((p) => path.resolve(p));
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
assetFiles = collectAssets(assetsPath);
|
|
544
|
+
}
|
|
545
|
+
// Validate asset files
|
|
546
|
+
const invalidAssets = assetFiles.filter(p => !fs.existsSync(p));
|
|
547
|
+
if (invalidAssets.length > 0) {
|
|
548
|
+
console.warn(chalk_1.default.yellow(`ā ${invalidAssets.length} asset(s) not found, will be skipped`));
|
|
549
|
+
assetFiles = assetFiles.filter(p => fs.existsSync(p));
|
|
550
|
+
}
|
|
483
551
|
}
|
|
484
552
|
}
|
|
485
553
|
// ========================================
|
|
@@ -501,51 +569,120 @@ const release = new commander_1.Command("release")
|
|
|
501
569
|
if (opts.versionRange) {
|
|
502
570
|
console.log(` Version Range: ${opts.versionRange}`);
|
|
503
571
|
}
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
572
|
+
// ========================================
|
|
573
|
+
// PRESIGNED URL UPLOAD FLOW
|
|
574
|
+
// ========================================
|
|
575
|
+
// This flow bypasses Vercel's 4.5MB body size limit by:
|
|
576
|
+
// 1. Requesting presigned URLs from the API
|
|
577
|
+
// 2. Uploading files directly to R2
|
|
578
|
+
// 3. Confirming upload completion
|
|
579
|
+
const { buildFileManifest, uploadFilesWithPresignedUrls } = await Promise.resolve().then(() => __importStar(require("../client/directUpload")));
|
|
580
|
+
// Build file manifest for presigned URL request
|
|
581
|
+
const fileManifest = buildFileManifest(platform, platform !== 'all' ? bundlePath : undefined, platform !== 'all' ? assetFiles : undefined, platform === 'all' ? iosBundlePath : undefined, platform === 'all' ? androidBundlePath : undefined, platform === 'all' ? iosAssetFiles : undefined, platform === 'all' ? androidAssetFiles : undefined,
|
|
582
|
+
// Asset base directories for relative path calculation
|
|
583
|
+
platform !== 'all' ? assetsBaseDir : undefined, platform === 'all' ? iosAssetsBaseDir : undefined, platform === 'all' ? androidAssetsBaseDir : undefined);
|
|
584
|
+
console.log(chalk_1.default.blue("\nš Step 1/3: Requesting upload URLs..."));
|
|
585
|
+
// Step 1: Request presigned URLs
|
|
586
|
+
const initResponse = await apiClient_1.apiClient.post("/cli/releases/upload/init", {
|
|
587
|
+
appId,
|
|
588
|
+
deploymentId,
|
|
589
|
+
releaseLabel: opts.label,
|
|
590
|
+
platform,
|
|
591
|
+
files: fileManifest.map(f => ({
|
|
592
|
+
type: f.type,
|
|
593
|
+
platform: f.platform,
|
|
594
|
+
filename: f.filename,
|
|
595
|
+
relativePath: f.relativePath, // Preserves folder structure like drawable-mdpi/logo.png
|
|
596
|
+
contentType: f.contentType,
|
|
597
|
+
})),
|
|
598
|
+
}, {
|
|
599
|
+
headers: {
|
|
600
|
+
"x-publish-key": authKey,
|
|
601
|
+
"Content-Type": "application/json",
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
if (!initResponse.data.success) {
|
|
605
|
+
throw new Error("Failed to initialize upload");
|
|
518
606
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
607
|
+
console.log(chalk_1.default.green(` ā Got ${initResponse.data.urls.length} presigned URLs`));
|
|
608
|
+
console.log(chalk_1.default.blue("\nš¤ Step 2/3: Uploading files directly to storage..."));
|
|
609
|
+
// Step 2: Upload files directly to R2
|
|
610
|
+
let uploadedCount = 0;
|
|
611
|
+
await uploadFilesWithPresignedUrls(fileManifest, initResponse.data.urls, {
|
|
612
|
+
concurrency: 3,
|
|
613
|
+
onFileComplete: (filename, index, total) => {
|
|
614
|
+
uploadedCount++;
|
|
615
|
+
console.log(chalk_1.default.gray(` [${uploadedCount}/${total}] ${filename}`));
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
console.log(chalk_1.default.green(` ā Uploaded ${fileManifest.length} files`));
|
|
619
|
+
console.log(chalk_1.default.blue("\nā
Step 3/3: Confirming release..."));
|
|
620
|
+
// Calculate bundle sizes from manifest
|
|
621
|
+
const bundleSizes = {};
|
|
622
|
+
for (const file of fileManifest) {
|
|
623
|
+
if (file.type === 'bundle' && file.size) {
|
|
624
|
+
if (file.platform === 'ios') {
|
|
625
|
+
bundleSizes.ios = file.size;
|
|
626
|
+
}
|
|
627
|
+
else if (file.platform === 'android') {
|
|
628
|
+
bundleSizes.android = file.size;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
bundleSizes.single = file.size;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
522
634
|
}
|
|
523
|
-
//
|
|
524
|
-
const
|
|
525
|
-
|
|
635
|
+
// Step 3: Complete upload and create release record
|
|
636
|
+
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", {
|
|
637
|
+
uploadId: initResponse.data.uploadId,
|
|
638
|
+
releaseLabel: opts.label,
|
|
639
|
+
mandatory: opts.mandatory,
|
|
640
|
+
rolloutPercentage: rolloutValidation.value,
|
|
641
|
+
binaryVersionRange: opts.versionRange,
|
|
642
|
+
platform,
|
|
643
|
+
isDraft: opts.schedule,
|
|
644
|
+
// Include bundle sizes
|
|
645
|
+
iosBundleSize: bundleSizes.ios,
|
|
646
|
+
androidBundleSize: bundleSizes.android || bundleSizes.single,
|
|
647
|
+
}, {
|
|
648
|
+
headers: {
|
|
649
|
+
"x-publish-key": authKey,
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
},
|
|
526
652
|
});
|
|
653
|
+
if (!completeResponse.data.success) {
|
|
654
|
+
throw new Error("Failed to complete upload");
|
|
655
|
+
}
|
|
656
|
+
const releaseData = completeResponse.data.data;
|
|
527
657
|
console.log(chalk_1.default.green("\nā Release uploaded successfully!"));
|
|
528
658
|
console.log(chalk_1.default.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
529
|
-
console.log(` ID: ${
|
|
530
|
-
console.log(` Label: ${
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
659
|
+
console.log(` ID: ${releaseData.id}`);
|
|
660
|
+
console.log(` Label: ${releaseData.releaseLabel}`);
|
|
661
|
+
// Display bundle URLs based on platform
|
|
662
|
+
if (platform === 'all') {
|
|
663
|
+
console.log(` iOS Bundle: ${releaseData.iosBundleUrl || 'uploaded'}`);
|
|
664
|
+
console.log(` Android Bundle: ${releaseData.androidBundleUrl || 'uploaded'}`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
console.log(` Bundle URL: ${releaseData.bundleUrl || releaseData.iosBundleUrl || releaseData.androidBundleUrl}`);
|
|
668
|
+
}
|
|
669
|
+
console.log(` Rollout: ${releaseData.rolloutPercentage}%`);
|
|
670
|
+
console.log(` Mandatory: ${releaseData.mandatory ? "Yes" : "No"}`);
|
|
671
|
+
if (releaseData.status === 'draft') {
|
|
535
672
|
console.log(chalk_1.default.yellow(` Status: DRAFT`));
|
|
536
673
|
console.log(chalk_1.default.gray(` š
Set schedule time in dashboard`));
|
|
537
674
|
}
|
|
538
675
|
else {
|
|
539
|
-
console.log(` Status: ${
|
|
676
|
+
console.log(` Status: ${releaseData.status || 'active'}`);
|
|
540
677
|
}
|
|
541
|
-
if (
|
|
542
|
-
console.log(` Version Range: ${
|
|
678
|
+
if (releaseData.binaryVersionRange) {
|
|
679
|
+
console.log(` Version Range: ${releaseData.binaryVersionRange}`);
|
|
543
680
|
}
|
|
544
681
|
console.log(chalk_1.default.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
545
682
|
}
|
|
546
683
|
catch (error) {
|
|
547
684
|
console.error(chalk_1.default.red(`\nā Failed to upload release: ${error.message}`));
|
|
548
|
-
if ((
|
|
685
|
+
if ((_b = error.response) === null || _b === void 0 ? void 0 : _b.data) {
|
|
549
686
|
console.error(chalk_1.default.gray(JSON.stringify(error.response.data, null, 2)));
|
|
550
687
|
}
|
|
551
688
|
process.exit(1);
|
package/dist/commands/status.js
CHANGED
|
@@ -5,85 +5,189 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const commander_1 = require("commander");
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
9
|
const apiClient_1 = require("../client/apiClient");
|
|
10
|
+
const configStore_1 = require("../config/configStore");
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
12
|
+
* Format relative time (e.g., "2 hours ago")
|
|
11
13
|
*/
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
+
function formatRelativeTime(dateStr) {
|
|
15
|
+
const date = new Date(dateStr);
|
|
16
|
+
const now = new Date();
|
|
17
|
+
const diffMs = now.getTime() - date.getTime();
|
|
18
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
19
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
20
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
21
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
22
|
+
if (diffDays > 0)
|
|
23
|
+
return `${diffDays}d ago`;
|
|
24
|
+
if (diffHours > 0)
|
|
25
|
+
return `${diffHours}h ago`;
|
|
26
|
+
if (diffMins > 0)
|
|
27
|
+
return `${diffMins}m ago`;
|
|
28
|
+
return "just now";
|
|
14
29
|
}
|
|
15
30
|
/**
|
|
16
|
-
*
|
|
31
|
+
* Select deployment from multiple credentials
|
|
17
32
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
33
|
+
async function selectDeployment(credentials) {
|
|
34
|
+
if (credentials.length === 1) {
|
|
35
|
+
return credentials[0];
|
|
36
|
+
}
|
|
37
|
+
const choices = credentials.map((cred) => ({
|
|
38
|
+
name: `${cred.deploymentName || cred.deploymentId} (${cred.appId})`,
|
|
39
|
+
value: cred,
|
|
40
|
+
}));
|
|
41
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: "list",
|
|
44
|
+
name: "selected",
|
|
45
|
+
message: "Select deployment to check status:",
|
|
46
|
+
choices,
|
|
47
|
+
}
|
|
48
|
+
]);
|
|
49
|
+
return selected;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Draw a box around text
|
|
53
|
+
*/
|
|
54
|
+
function drawBox(lines, width = 50) {
|
|
55
|
+
const output = [];
|
|
56
|
+
const border = "ā".repeat(width - 2);
|
|
57
|
+
output.push(chalk_1.default.gray(`ā${border}ā®`));
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
// Strip ANSI codes for length calculation
|
|
60
|
+
const plainLine = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
61
|
+
const padding = width - 4 - plainLine.length;
|
|
62
|
+
output.push(chalk_1.default.gray("ā") + " " + line + " ".repeat(Math.max(0, padding)) + chalk_1.default.gray("ā"));
|
|
63
|
+
}
|
|
64
|
+
output.push(chalk_1.default.gray(`ā°${border}āÆ`));
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get status indicator
|
|
69
|
+
*/
|
|
70
|
+
function getStatusIndicator(status) {
|
|
71
|
+
switch (status) {
|
|
72
|
+
case "active":
|
|
73
|
+
return chalk_1.default.green("ā");
|
|
74
|
+
case "scheduled":
|
|
75
|
+
return chalk_1.default.yellow("ā");
|
|
76
|
+
case "expired":
|
|
77
|
+
return chalk_1.default.gray("ā");
|
|
78
|
+
case "draft":
|
|
79
|
+
return chalk_1.default.blue("ā");
|
|
80
|
+
default:
|
|
81
|
+
return chalk_1.default.gray("ā");
|
|
82
|
+
}
|
|
20
83
|
}
|
|
21
84
|
const status = new commander_1.Command("status")
|
|
22
|
-
.description("Check
|
|
23
|
-
.option("-
|
|
24
|
-
.option("-d, --deployment <deploymentId>", "Filter by deployment ID")
|
|
25
|
-
.option("-n, --limit <number>", "Number of releases to show", "10")
|
|
85
|
+
.description("Check deployment status and releases")
|
|
86
|
+
.option("-n, --limit <number>", "Number of releases to show", "5")
|
|
26
87
|
.action(async (opts) => {
|
|
27
88
|
try {
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.error(chalk_1.default.
|
|
89
|
+
// Check for credentials
|
|
90
|
+
const credentials = configStore_1.configStore.getCredentials();
|
|
91
|
+
if (credentials.length === 0) {
|
|
92
|
+
console.error(chalk_1.default.red("ā Not authenticated"));
|
|
93
|
+
console.error(chalk_1.default.gray("Run 'upflux login --key <publishKey>' first."));
|
|
32
94
|
process.exit(1);
|
|
33
95
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
process.exit(1);
|
|
96
|
+
// Select or determine credential to use
|
|
97
|
+
let selectedCredential;
|
|
98
|
+
if (credentials.length === 1) {
|
|
99
|
+
selectedCredential = credentials[0];
|
|
39
100
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
101
|
+
else {
|
|
102
|
+
selectedCredential = await selectDeployment(credentials);
|
|
103
|
+
// Update activeIndex
|
|
104
|
+
const selectedIndex = credentials.findIndex(c => c.deploymentId === selectedCredential.deploymentId);
|
|
105
|
+
if (selectedIndex >= 0) {
|
|
106
|
+
configStore_1.configStore.setActiveIndex(selectedIndex);
|
|
107
|
+
}
|
|
46
108
|
}
|
|
47
|
-
console.log(chalk_1.default.blue("ā” Fetching
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
109
|
+
console.log(chalk_1.default.blue("\nā” Fetching deployment status...\n"));
|
|
110
|
+
// Fetch status from API
|
|
111
|
+
const response = await apiClient_1.apiClient.get("/cli/status");
|
|
112
|
+
const data = response.data;
|
|
113
|
+
// ========================================
|
|
114
|
+
// DEPLOYMENT HEADER
|
|
115
|
+
// ========================================
|
|
116
|
+
const headerLines = [];
|
|
117
|
+
headerLines.push(chalk_1.default.bold(`š± ${data.deployment.appId} ā ${data.deployment.name}`));
|
|
118
|
+
headerLines.push(chalk_1.default.gray("ā".repeat(40)));
|
|
119
|
+
// Show current releases
|
|
120
|
+
if (data.current.ios || data.current.android) {
|
|
121
|
+
if (data.current.ios && data.current.android) {
|
|
122
|
+
// Dual platform
|
|
123
|
+
if (data.current.ios.releaseLabel === data.current.android.releaseLabel) {
|
|
124
|
+
// Same version for both
|
|
125
|
+
headerLines.push(`Current Release: ${chalk_1.default.cyan(data.current.ios.releaseLabel)} ${chalk_1.default.gray("(iOS & Android)")}`);
|
|
126
|
+
headerLines.push(`Status: ${getStatusIndicator(data.current.ios.status)} ${data.current.ios.status} ⢠${data.current.ios.rolloutPercentage}% rollout`);
|
|
127
|
+
headerLines.push(`Published: ${formatRelativeTime(data.current.ios.publishedAt)}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Different versions
|
|
131
|
+
headerLines.push(`iOS: ${chalk_1.default.cyan(data.current.ios.releaseLabel)} ${getStatusIndicator(data.current.ios.status)} ${data.current.ios.rolloutPercentage}%`);
|
|
132
|
+
headerLines.push(`Android: ${chalk_1.default.cyan(data.current.android.releaseLabel)} ${getStatusIndicator(data.current.android.status)} ${data.current.android.rolloutPercentage}%`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (data.current.ios) {
|
|
136
|
+
headerLines.push(`iOS: ${chalk_1.default.cyan(data.current.ios.releaseLabel)} ${getStatusIndicator(data.current.ios.status)}`);
|
|
137
|
+
headerLines.push(`${data.current.ios.rolloutPercentage}% rollout ⢠${formatRelativeTime(data.current.ios.publishedAt)}`);
|
|
138
|
+
}
|
|
139
|
+
else if (data.current.android) {
|
|
140
|
+
headerLines.push(`Android: ${chalk_1.default.cyan(data.current.android.releaseLabel)} ${getStatusIndicator(data.current.android.status)}`);
|
|
141
|
+
headerLines.push(`${data.current.android.rolloutPercentage}% rollout ⢠${formatRelativeTime(data.current.android.publishedAt)}`);
|
|
60
142
|
}
|
|
61
|
-
return;
|
|
62
143
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.log(chalk_1.default.
|
|
76
|
-
|
|
77
|
-
|
|
144
|
+
else {
|
|
145
|
+
headerLines.push(chalk_1.default.yellow("No active release"));
|
|
146
|
+
}
|
|
147
|
+
// Draw the header box
|
|
148
|
+
const headerBox = drawBox(headerLines, 52);
|
|
149
|
+
headerBox.forEach(line => console.log(line));
|
|
150
|
+
// ========================================
|
|
151
|
+
// RELEASES TABLE
|
|
152
|
+
// ========================================
|
|
153
|
+
const limit = parseInt(opts.limit, 10) || 5;
|
|
154
|
+
const releases = data.releases.slice(0, limit);
|
|
155
|
+
if (releases.length > 0) {
|
|
156
|
+
console.log(chalk_1.default.bold(`\nš¦ Recent Releases (${data.totalReleases} total)\n`));
|
|
157
|
+
// Table header
|
|
158
|
+
console.log(chalk_1.default.gray(" ") +
|
|
159
|
+
chalk_1.default.white.bold("Label".padEnd(12)) +
|
|
160
|
+
chalk_1.default.white.bold("Platform".padEnd(10)) +
|
|
161
|
+
chalk_1.default.white.bold("Status".padEnd(12)) +
|
|
162
|
+
chalk_1.default.white.bold("Rollout".padEnd(10)) +
|
|
163
|
+
chalk_1.default.white.bold("Published"));
|
|
164
|
+
console.log(chalk_1.default.gray(" " + "ā".repeat(60)));
|
|
165
|
+
// Table rows
|
|
166
|
+
releases.forEach((release, index) => {
|
|
167
|
+
const statusIcon = getStatusIndicator(release.status);
|
|
168
|
+
const statusText = release.status.padEnd(10);
|
|
169
|
+
const platformText = release.platform.padEnd(10);
|
|
170
|
+
const rolloutText = `${release.rolloutPercentage}%`.padEnd(10);
|
|
171
|
+
const mandatoryFlag = release.mandatory ? chalk_1.default.red(" !") : "";
|
|
172
|
+
console.log(chalk_1.default.gray(" ") +
|
|
173
|
+
chalk_1.default.cyan(release.releaseLabel.padEnd(12)) +
|
|
174
|
+
chalk_1.default.gray(platformText) +
|
|
175
|
+
statusIcon + " " + chalk_1.default.white(statusText) +
|
|
176
|
+
chalk_1.default.yellow(rolloutText) +
|
|
177
|
+
chalk_1.default.gray(formatRelativeTime(release.createdAt)) +
|
|
178
|
+
mandatoryFlag);
|
|
179
|
+
});
|
|
180
|
+
if (data.totalReleases > limit) {
|
|
181
|
+
console.log(chalk_1.default.gray(`\n ... and ${data.totalReleases - limit} more. Use --limit to see more.`));
|
|
78
182
|
}
|
|
79
|
-
console.log("");
|
|
80
|
-
});
|
|
81
|
-
if (releases.length > limit) {
|
|
82
|
-
console.log(chalk_1.default.gray(`... and ${releases.length - limit} more. Use --limit to see more.`));
|
|
83
183
|
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(chalk_1.default.yellow("\nNo releases found."));
|
|
186
|
+
}
|
|
187
|
+
console.log(""); // Empty line at end
|
|
84
188
|
}
|
|
85
189
|
catch (error) {
|
|
86
|
-
console.error(chalk_1.default.red(`\nā Failed to fetch
|
|
190
|
+
console.error(chalk_1.default.red(`\nā Failed to fetch status: ${error.message}`));
|
|
87
191
|
process.exit(1);
|
|
88
192
|
}
|
|
89
193
|
});
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const program = new commander_1.Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name("upflux")
|
|
16
16
|
.description("Upflux CLI for OTA updates")
|
|
17
|
-
.version("0.0.1");
|
|
17
|
+
.version("0.1.0-beta.1");
|
|
18
18
|
program.addCommand(login_1.default);
|
|
19
19
|
program.addCommand(logout_1.default);
|
|
20
20
|
program.addCommand(release_1.default);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -20,8 +20,15 @@ export interface ReleaseManifest {
|
|
|
20
20
|
export interface ReleaseResponse {
|
|
21
21
|
id: string;
|
|
22
22
|
releaseLabel: string;
|
|
23
|
-
bundleUrl
|
|
24
|
-
assets
|
|
23
|
+
bundleUrl?: string;
|
|
24
|
+
assets?: string[];
|
|
25
|
+
iosBundleUrl?: string;
|
|
26
|
+
androidBundleUrl?: string;
|
|
27
|
+
iosBundleSize?: number;
|
|
28
|
+
androidBundleSize?: number;
|
|
29
|
+
iosAssets?: string[];
|
|
30
|
+
androidAssets?: string[];
|
|
31
|
+
platform?: 'all' | 'ios' | 'android';
|
|
25
32
|
rolloutPercentage: number;
|
|
26
33
|
mandatory: boolean;
|
|
27
34
|
binaryVersionRange?: string;
|
|
@@ -73,7 +80,7 @@ export interface AppResponse {
|
|
|
73
80
|
id: string;
|
|
74
81
|
appId: string;
|
|
75
82
|
name: string;
|
|
76
|
-
|
|
83
|
+
sdkType: string;
|
|
77
84
|
createdAt: string;
|
|
78
85
|
updatedAt?: string;
|
|
79
86
|
}
|