@upfluxhq/cli 0.1.0-beta.2 ā 0.1.0-beta.4
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 +153 -18
- 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/release.js +148 -60
- package/dist/commands/status.js +163 -59
- 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
|
|
|
@@ -40,6 +40,7 @@ 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
46
|
Deployment: production
|
|
@@ -78,6 +79,10 @@ upflux logout --all
|
|
|
78
79
|
|
|
79
80
|
Bundle and upload a new release. **Prompts to select deployment** if multiple are stored.
|
|
80
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
|
+
|
|
81
86
|
```bash
|
|
82
87
|
upflux release [options]
|
|
83
88
|
```
|
|
@@ -85,8 +90,8 @@ upflux release [options]
|
|
|
85
90
|
| Option | Required | Description |
|
|
86
91
|
|--------|----------|-------------|
|
|
87
92
|
| `-l, --label <label>` | ā | Release label (e.g., v1.0.0) |
|
|
88
|
-
| `--
|
|
89
|
-
| `-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) |
|
|
90
95
|
| `-d, --deployment <deploymentId>` | | Deployment ID (prompts if multiple) |
|
|
91
96
|
| `-e, --entry-file <path>` | | Entry file for bundler (default: `index.js`) |
|
|
92
97
|
| `-o, --output-dir <path>` | | Output directory for bundle (default: `./dist`) |
|
|
@@ -97,24 +102,75 @@ upflux release [options]
|
|
|
97
102
|
| `-v, --version-range <range>` | | Binary version range (e.g., >=1.0.0) |
|
|
98
103
|
| `--dev` | | Create development bundle |
|
|
99
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) |
|
|
100
108
|
|
|
101
|
-
**
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
staging (app-970384d3)
|
|
118
|
+
# Android-only release with 50% rollout
|
|
119
|
+
upflux release --label v1.2.2 --platform android --rollout 50
|
|
108
120
|
|
|
109
|
-
#
|
|
110
|
-
|
|
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
|
|
129
|
+
```
|
|
130
|
+
|
|
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
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
111
167
|
```
|
|
112
168
|
|
|
113
169
|
---
|
|
114
170
|
|
|
115
171
|
### `status`
|
|
116
172
|
|
|
117
|
-
Check
|
|
173
|
+
Check deployment status and recent releases.
|
|
118
174
|
|
|
119
175
|
```bash
|
|
120
176
|
upflux status [options]
|
|
@@ -122,9 +178,28 @@ upflux status [options]
|
|
|
122
178
|
|
|
123
179
|
| Option | Description |
|
|
124
180
|
|--------|-------------|
|
|
125
|
-
| `-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
```
|
|
128
203
|
|
|
129
204
|
---
|
|
130
205
|
|
|
@@ -141,6 +216,50 @@ upflux generate-manifest [options]
|
|
|
141
216
|
| `-r, --release-id <id>` | ā | Release ID (MongoDB ObjectId) |
|
|
142
217
|
| `-o, --output <path>` | | Save manifest to file |
|
|
143
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
|
+
|
|
144
263
|
---
|
|
145
264
|
|
|
146
265
|
## Configuration
|
|
@@ -149,7 +268,7 @@ upflux generate-manifest [options]
|
|
|
149
268
|
|
|
150
269
|
| Variable | Description | Default |
|
|
151
270
|
|----------|-------------|---------|
|
|
152
|
-
| `UPFLUX_API_URL` | API server URL | `
|
|
271
|
+
| `UPFLUX_API_URL` | API server URL | `https://api.upflux.in/api` |
|
|
153
272
|
|
|
154
273
|
### Auth File
|
|
155
274
|
|
|
@@ -159,13 +278,13 @@ The CLI stores credentials in `.upflux` in the current directory:
|
|
|
159
278
|
{
|
|
160
279
|
"credentials": [
|
|
161
280
|
{
|
|
162
|
-
"key": "
|
|
281
|
+
"key": "upflux_pub_xxx...",
|
|
163
282
|
"appId": "app-970384d3",
|
|
164
283
|
"deploymentId": "6933e683aa32fe18a1311bbb",
|
|
165
284
|
"deploymentName": "production"
|
|
166
285
|
},
|
|
167
286
|
{
|
|
168
|
-
"key": "
|
|
287
|
+
"key": "upflux_pub_yyy...",
|
|
169
288
|
"appId": "app-970384d3",
|
|
170
289
|
"deploymentId": "6933e683aa32fe18a1311ccc",
|
|
171
290
|
"deploymentName": "staging"
|
|
@@ -179,6 +298,16 @@ The CLI stores credentials in `.upflux` in the current directory:
|
|
|
179
298
|
|
|
180
299
|
---
|
|
181
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
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
182
311
|
## Development
|
|
183
312
|
|
|
184
313
|
```bash
|
|
@@ -192,6 +321,12 @@ pnpm run build
|
|
|
192
321
|
node dist/index.js --help
|
|
193
322
|
```
|
|
194
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
|
+
|
|
195
330
|
## License
|
|
196
331
|
|
|
197
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 = "http://localhost:3000/api";
|
|
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/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}`,
|
|
@@ -256,6 +254,27 @@ function collectAssets(dir) {
|
|
|
256
254
|
}
|
|
257
255
|
return assets;
|
|
258
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Write upflux.json config file to the bundle output directory
|
|
259
|
+
*
|
|
260
|
+
* This file embeds the OTA version into the app bundle at build time.
|
|
261
|
+
* The SDK reads this on fresh install to know which version the app ships with,
|
|
262
|
+
* preventing unnecessary OTA downloads when the embedded bundle is already latest.
|
|
263
|
+
*
|
|
264
|
+
* @param outputDir - Directory to write upflux.json to
|
|
265
|
+
* @param releaseLabel - The OTA version (e.g., "v1.0.0")
|
|
266
|
+
* @param appId - The app ID
|
|
267
|
+
*/
|
|
268
|
+
function writeUpfluxConfig(outputDir, releaseLabel, appId) {
|
|
269
|
+
const config = {
|
|
270
|
+
releaseLabel,
|
|
271
|
+
appId,
|
|
272
|
+
createdAt: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
const configPath = path.join(outputDir, "upflux.json");
|
|
275
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
276
|
+
console.log(chalk_1.default.gray(` Created upflux.json with version: ${releaseLabel}`));
|
|
277
|
+
}
|
|
259
278
|
const release = new commander_1.Command("release")
|
|
260
279
|
.description("Bundle and upload a new release to Upflux")
|
|
261
280
|
.option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
|
|
@@ -312,6 +331,11 @@ const release = new commander_1.Command("release")
|
|
|
312
331
|
else {
|
|
313
332
|
// Multiple credentials - prompt for selection
|
|
314
333
|
selectedCredential = await selectDeployment(credentials);
|
|
334
|
+
// Update activeIndex so subsequent commands use the same deployment
|
|
335
|
+
const selectedIndex = credentials.findIndex(c => c.deploymentId === selectedCredential.deploymentId);
|
|
336
|
+
if (selectedIndex >= 0) {
|
|
337
|
+
configStore_1.configStore.setActiveIndex(selectedIndex);
|
|
338
|
+
}
|
|
315
339
|
}
|
|
316
340
|
const appId = selectedCredential.appId;
|
|
317
341
|
const deploymentId = selectedCredential.deploymentId;
|
|
@@ -369,6 +393,11 @@ const release = new commander_1.Command("release")
|
|
|
369
393
|
let androidBundlePath;
|
|
370
394
|
let iosAssetFiles = [];
|
|
371
395
|
let androidAssetFiles = [];
|
|
396
|
+
// Asset directories for calculating relative paths (preserves drawable-* structure)
|
|
397
|
+
// These point to the platform output directory (e.g., dist/android/) so paths include assets/
|
|
398
|
+
let assetsBaseDir;
|
|
399
|
+
let iosAssetsBaseDir;
|
|
400
|
+
let androidAssetsBaseDir;
|
|
372
401
|
// Detect project type
|
|
373
402
|
const isCapacitor = opts.capacitor || isCapacitorProject();
|
|
374
403
|
if (isCapacitor) {
|
|
@@ -396,6 +425,9 @@ const release = new commander_1.Command("release")
|
|
|
396
425
|
if (!fs.existsSync(outputDir)) {
|
|
397
426
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
398
427
|
}
|
|
428
|
+
// Write upflux.json to web directory BEFORE zipping
|
|
429
|
+
// This embeds the OTA version into the Capacitor app bundle
|
|
430
|
+
writeUpfluxConfig(webDirPath, opts.label, appId);
|
|
399
431
|
// Zip the web directory
|
|
400
432
|
console.log(chalk_1.default.blue(`š¦ Zipping ${webDir}/ directory...`));
|
|
401
433
|
const zipPath = path.join(outputDir, `bundle-${opts.label}.zip`);
|
|
@@ -484,16 +516,24 @@ const release = new commander_1.Command("release")
|
|
|
484
516
|
process.exit(1);
|
|
485
517
|
}
|
|
486
518
|
}
|
|
487
|
-
// Collect assets for iOS
|
|
488
|
-
const
|
|
519
|
+
// Collect assets for iOS - base dir is the ios output dir so paths include assets/
|
|
520
|
+
const iosOutputDir = path.dirname(iosBundlePath);
|
|
521
|
+
iosAssetsBaseDir = iosOutputDir;
|
|
522
|
+
const iosAssetsPath = path.join(iosOutputDir, 'assets');
|
|
489
523
|
iosAssetFiles = opts.assets
|
|
490
524
|
? opts.assets.map((p) => path.resolve(p))
|
|
491
|
-
: collectAssets(
|
|
492
|
-
// Collect assets for Android
|
|
493
|
-
const
|
|
525
|
+
: collectAssets(iosAssetsPath);
|
|
526
|
+
// Collect assets for Android - base dir is the android output dir so paths include assets/
|
|
527
|
+
const androidOutputDir = path.dirname(androidBundlePath);
|
|
528
|
+
androidAssetsBaseDir = androidOutputDir;
|
|
529
|
+
const androidAssetsPath = path.join(androidOutputDir, 'assets');
|
|
494
530
|
androidAssetFiles = opts.assets
|
|
495
531
|
? opts.assets.map((p) => path.resolve(p))
|
|
496
|
-
: collectAssets(
|
|
532
|
+
: collectAssets(androidAssetsPath);
|
|
533
|
+
// Write upflux.json to BOTH platform output directories
|
|
534
|
+
// React Native reads this from bundled assets to know embedded OTA version
|
|
535
|
+
writeUpfluxConfig(iosOutputDir, opts.label, appId);
|
|
536
|
+
writeUpfluxConfig(androidOutputDir, opts.label, appId);
|
|
497
537
|
// Set bundlePath and assetFiles for display (use iOS as primary for logging)
|
|
498
538
|
bundlePath = iosBundlePath;
|
|
499
539
|
assetFiles = [...iosAssetFiles, ...androidAssetFiles];
|
|
@@ -503,7 +543,9 @@ const release = new commander_1.Command("release")
|
|
|
503
543
|
// SINGLE PLATFORM BUNDLING
|
|
504
544
|
// ========================================
|
|
505
545
|
const expectedBundle = path.join(outputDir, `index.${platform}.bundle`);
|
|
506
|
-
|
|
546
|
+
// Base dir is outputDir so relative paths include assets/ folder
|
|
547
|
+
assetsBaseDir = outputDir;
|
|
548
|
+
const assetsPath = path.join(outputDir, "assets");
|
|
507
549
|
if (opts.skipBundle) {
|
|
508
550
|
if (!fs.existsSync(expectedBundle)) {
|
|
509
551
|
console.error(chalk_1.default.red(`ā Bundle not found: ${expectedBundle}`));
|
|
@@ -526,7 +568,7 @@ const release = new commander_1.Command("release")
|
|
|
526
568
|
assetFiles = opts.assets.map((p) => path.resolve(p));
|
|
527
569
|
}
|
|
528
570
|
else {
|
|
529
|
-
assetFiles = collectAssets(
|
|
571
|
+
assetFiles = collectAssets(assetsPath);
|
|
530
572
|
}
|
|
531
573
|
// Validate asset files
|
|
532
574
|
const invalidAssets = assetFiles.filter(p => !fs.existsSync(p));
|
|
@@ -534,6 +576,9 @@ const release = new commander_1.Command("release")
|
|
|
534
576
|
console.warn(chalk_1.default.yellow(`ā ${invalidAssets.length} asset(s) not found, will be skipped`));
|
|
535
577
|
assetFiles = assetFiles.filter(p => fs.existsSync(p));
|
|
536
578
|
}
|
|
579
|
+
// Write upflux.json to output directory
|
|
580
|
+
// React Native reads this from bundled assets to know embedded OTA version
|
|
581
|
+
writeUpfluxConfig(outputDir, opts.label, appId);
|
|
537
582
|
}
|
|
538
583
|
}
|
|
539
584
|
// ========================================
|
|
@@ -555,71 +600,114 @@ const release = new commander_1.Command("release")
|
|
|
555
600
|
if (opts.versionRange) {
|
|
556
601
|
console.log(` Version Range: ${opts.versionRange}`);
|
|
557
602
|
}
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
603
|
+
// ========================================
|
|
604
|
+
// PRESIGNED URL UPLOAD FLOW
|
|
605
|
+
// ========================================
|
|
606
|
+
// This flow bypasses Vercel's 4.5MB body size limit by:
|
|
607
|
+
// 1. Requesting presigned URLs from the API
|
|
608
|
+
// 2. Uploading files directly to R2
|
|
609
|
+
// 3. Confirming upload completion
|
|
610
|
+
const { buildFileManifest, uploadFilesWithPresignedUrls } = await Promise.resolve().then(() => __importStar(require("../client/directUpload")));
|
|
611
|
+
// Build file manifest for presigned URL request
|
|
612
|
+
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,
|
|
613
|
+
// Asset base directories for relative path calculation
|
|
614
|
+
platform !== 'all' ? assetsBaseDir : undefined, platform === 'all' ? iosAssetsBaseDir : undefined, platform === 'all' ? androidAssetsBaseDir : undefined);
|
|
615
|
+
console.log(chalk_1.default.blue("\nš Step 1/3: Requesting upload URLs..."));
|
|
616
|
+
// Step 1: Request presigned URLs
|
|
617
|
+
const initResponse = await apiClient_1.apiClient.post("/cli/releases/upload/init", {
|
|
618
|
+
appId,
|
|
619
|
+
deploymentId,
|
|
620
|
+
releaseLabel: opts.label,
|
|
621
|
+
platform,
|
|
622
|
+
files: fileManifest.map(f => ({
|
|
623
|
+
type: f.type,
|
|
624
|
+
platform: f.platform,
|
|
625
|
+
filename: f.filename,
|
|
626
|
+
relativePath: f.relativePath, // Preserves folder structure like drawable-mdpi/logo.png
|
|
627
|
+
contentType: f.contentType,
|
|
628
|
+
})),
|
|
629
|
+
}, {
|
|
630
|
+
headers: {
|
|
631
|
+
"x-publish-key": authKey,
|
|
632
|
+
"Content-Type": "application/json",
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
if (!initResponse.data.success) {
|
|
636
|
+
throw new Error("Failed to initialize upload");
|
|
637
|
+
}
|
|
638
|
+
console.log(chalk_1.default.green(` ā Got ${initResponse.data.urls.length} presigned URLs`));
|
|
639
|
+
console.log(chalk_1.default.blue("\nš¤ Step 2/3: Uploading files directly to storage..."));
|
|
640
|
+
// Step 2: Upload files directly to R2
|
|
641
|
+
let uploadedCount = 0;
|
|
642
|
+
await uploadFilesWithPresignedUrls(fileManifest, initResponse.data.urls, {
|
|
643
|
+
concurrency: 3,
|
|
644
|
+
onFileComplete: (filename, index, total) => {
|
|
645
|
+
uploadedCount++;
|
|
646
|
+
console.log(chalk_1.default.gray(` [${uploadedCount}/${total}] ${filename}`));
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
console.log(chalk_1.default.green(` ā Uploaded ${fileManifest.length} files`));
|
|
650
|
+
console.log(chalk_1.default.blue("\nā
Step 3/3: Confirming release..."));
|
|
651
|
+
// Calculate bundle sizes from manifest
|
|
652
|
+
const bundleSizes = {};
|
|
653
|
+
for (const file of fileManifest) {
|
|
654
|
+
if (file.type === 'bundle' && file.size) {
|
|
655
|
+
if (file.platform === 'ios') {
|
|
656
|
+
bundleSizes.ios = file.size;
|
|
657
|
+
}
|
|
658
|
+
else if (file.platform === 'android') {
|
|
659
|
+
bundleSizes.android = file.size;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
bundleSizes.single = file.size;
|
|
663
|
+
}
|
|
579
664
|
}
|
|
580
665
|
}
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
headers: Object.assign(Object.assign({}, form.getHeaders()), { "x-publish-key": authKey }),
|
|
666
|
+
// Step 3: Complete upload and create release record
|
|
667
|
+
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", {
|
|
668
|
+
uploadId: initResponse.data.uploadId,
|
|
669
|
+
releaseLabel: opts.label,
|
|
670
|
+
mandatory: opts.mandatory,
|
|
671
|
+
rolloutPercentage: rolloutValidation.value,
|
|
672
|
+
binaryVersionRange: opts.versionRange,
|
|
673
|
+
platform,
|
|
674
|
+
isDraft: opts.schedule,
|
|
675
|
+
// Include bundle sizes
|
|
676
|
+
iosBundleSize: bundleSizes.ios,
|
|
677
|
+
androidBundleSize: bundleSizes.android || bundleSizes.single,
|
|
678
|
+
}, {
|
|
679
|
+
headers: {
|
|
680
|
+
"x-publish-key": authKey,
|
|
681
|
+
"Content-Type": "application/json",
|
|
682
|
+
},
|
|
599
683
|
});
|
|
684
|
+
if (!completeResponse.data.success) {
|
|
685
|
+
throw new Error("Failed to complete upload");
|
|
686
|
+
}
|
|
687
|
+
const releaseData = completeResponse.data.data;
|
|
600
688
|
console.log(chalk_1.default.green("\nā Release uploaded successfully!"));
|
|
601
689
|
console.log(chalk_1.default.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
602
|
-
console.log(` ID: ${
|
|
603
|
-
console.log(` Label: ${
|
|
690
|
+
console.log(` ID: ${releaseData.id}`);
|
|
691
|
+
console.log(` Label: ${releaseData.releaseLabel}`);
|
|
604
692
|
// Display bundle URLs based on platform
|
|
605
693
|
if (platform === 'all') {
|
|
606
|
-
console.log(` iOS Bundle: ${
|
|
607
|
-
console.log(` Android Bundle: ${
|
|
694
|
+
console.log(` iOS Bundle: ${releaseData.iosBundleUrl || 'uploaded'}`);
|
|
695
|
+
console.log(` Android Bundle: ${releaseData.androidBundleUrl || 'uploaded'}`);
|
|
608
696
|
}
|
|
609
697
|
else {
|
|
610
|
-
console.log(` Bundle URL: ${
|
|
698
|
+
console.log(` Bundle URL: ${releaseData.bundleUrl || releaseData.iosBundleUrl || releaseData.androidBundleUrl}`);
|
|
611
699
|
}
|
|
612
|
-
console.log(` Rollout: ${
|
|
613
|
-
console.log(` Mandatory: ${
|
|
614
|
-
if (
|
|
700
|
+
console.log(` Rollout: ${releaseData.rolloutPercentage}%`);
|
|
701
|
+
console.log(` Mandatory: ${releaseData.mandatory ? "Yes" : "No"}`);
|
|
702
|
+
if (releaseData.status === 'draft') {
|
|
615
703
|
console.log(chalk_1.default.yellow(` Status: DRAFT`));
|
|
616
704
|
console.log(chalk_1.default.gray(` š
Set schedule time in dashboard`));
|
|
617
705
|
}
|
|
618
706
|
else {
|
|
619
|
-
console.log(` Status: ${
|
|
707
|
+
console.log(` Status: ${releaseData.status || 'active'}`);
|
|
620
708
|
}
|
|
621
|
-
if (
|
|
622
|
-
console.log(` Version Range: ${
|
|
709
|
+
if (releaseData.binaryVersionRange) {
|
|
710
|
+
console.log(` Version Range: ${releaseData.binaryVersionRange}`);
|
|
623
711
|
}
|
|
624
712
|
console.log(chalk_1.default.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
625
713
|
}
|
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
|
});
|