@upfluxhq/cli 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -248
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +149 -0
- package/dist/commands/release.js +175 -38
- package/dist/config/configStore.d.ts +14 -0
- package/dist/config/configStore.js +26 -0
- package/dist/index.js +2 -0
- package/dist/utils/native-detector.d.ts +48 -0
- package/dist/utils/native-detector.js +198 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,35 +10,61 @@ npm install -g @upfluxhq/cli
|
|
|
10
10
|
pnpm add -g @upfluxhq/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
---
|
|
14
|
+
|
|
13
15
|
## Quick Start
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
# 1.
|
|
17
|
-
upflux
|
|
18
|
-
|
|
18
|
+
# 1. Initialize project config (creates upflux.json)
|
|
19
|
+
upflux init
|
|
20
|
+
|
|
21
|
+
# 2. Authenticate with your publish key (from Upflux dashboard)
|
|
22
|
+
upflux login --key <publish-key>
|
|
19
23
|
|
|
20
|
-
#
|
|
21
|
-
upflux release
|
|
24
|
+
# 3. Publish a release (auto-increments buildNumber)
|
|
25
|
+
upflux release
|
|
22
26
|
|
|
23
|
-
#
|
|
27
|
+
# 4. Check deployment status
|
|
24
28
|
upflux status
|
|
25
29
|
```
|
|
26
30
|
|
|
31
|
+
---
|
|
32
|
+
|
|
27
33
|
## Commands
|
|
28
34
|
|
|
29
|
-
### `
|
|
35
|
+
### `init`
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
Interactive wizard to configure `upflux.json` in your project root. Replaces manual configuration in your app code with declarative deployment profiles.
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
upflux
|
|
40
|
+
upflux init [options]
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
| Option | Description |
|
|
38
44
|
|--------|-------------|
|
|
39
|
-
|
|
|
45
|
+
| `--force` | Overwrite existing `upflux.json` without prompting |
|
|
46
|
+
|
|
47
|
+
**What it sets up:**
|
|
48
|
+
- App ID & server URL
|
|
49
|
+
- Initial `buildNumber` (`0`) and `runtimeVersion` (e.g. `1.0.0`)
|
|
50
|
+
- Deployment profiles (`development`, `production`, etc.) with client keys (`upx_cli_*`)
|
|
51
|
+
- Safe to commit to git — client keys are read-only
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### `login`
|
|
56
|
+
|
|
57
|
+
Authenticate with the Upflux server using your publish key. **Supports multiple credentials** — each login registers another deployment target.
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
```bash
|
|
60
|
+
upflux login --key <publishKey>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Option | Required | Description |
|
|
64
|
+
|--------|----------|-------------|
|
|
65
|
+
| `-k, --key <publishKey>` | ✓ | Publish key (`upx_pub_*`) from the dashboard |
|
|
66
|
+
|
|
67
|
+
**Example output with multiple credentials:**
|
|
42
68
|
```
|
|
43
69
|
🔐 Verifying with server...
|
|
44
70
|
✓ Login successful!
|
|
@@ -54,7 +80,7 @@ upflux login --key <publishKey>
|
|
|
54
80
|
|
|
55
81
|
### `logout`
|
|
56
82
|
|
|
57
|
-
Remove stored credentials.
|
|
83
|
+
Remove stored publish credentials.
|
|
58
84
|
|
|
59
85
|
```bash
|
|
60
86
|
upflux logout [options]
|
|
@@ -62,14 +88,14 @@ upflux logout [options]
|
|
|
62
88
|
|
|
63
89
|
| Option | Description |
|
|
64
90
|
|--------|-------------|
|
|
65
|
-
| `--all` | Remove all credentials |
|
|
91
|
+
| `--all` | Remove all stored credentials |
|
|
66
92
|
|
|
67
93
|
**Example:**
|
|
68
94
|
```bash
|
|
69
|
-
# Interactive selection
|
|
95
|
+
# Interactive selection:
|
|
70
96
|
upflux logout
|
|
71
97
|
|
|
72
|
-
#
|
|
98
|
+
# Clear all credentials:
|
|
73
99
|
upflux logout --all
|
|
74
100
|
```
|
|
75
101
|
|
|
@@ -77,159 +103,105 @@ upflux logout --all
|
|
|
77
103
|
|
|
78
104
|
### `release`
|
|
79
105
|
|
|
80
|
-
Bundle and upload a new release.
|
|
106
|
+
Bundle and upload a new OTA release. Prompts for target deployment if multiple are configured.
|
|
81
107
|
|
|
82
108
|
Automatically detects project type:
|
|
83
|
-
- **React Native**:
|
|
84
|
-
- **Capacitor**:
|
|
109
|
+
- **React Native**: Runs `react-native bundle` for iOS and Android
|
|
110
|
+
- **Capacitor**: Archives the web assets folder (detected from `capacitor.config.*`)
|
|
85
111
|
|
|
86
112
|
```bash
|
|
87
113
|
upflux release [options]
|
|
88
114
|
```
|
|
89
115
|
|
|
90
|
-
| Option |
|
|
91
|
-
|
|
92
|
-
| `-l, --label <label>` |
|
|
93
|
-
| `--platform <platform>` |
|
|
94
|
-
| `-a, --app <appId>` |
|
|
95
|
-
| `-d, --deployment <deploymentId>` |
|
|
96
|
-
| `-
|
|
97
|
-
| `-
|
|
98
|
-
| `-
|
|
99
|
-
| `--
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
| `-
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
| `--
|
|
106
|
-
| `--
|
|
107
|
-
| `--
|
|
108
|
-
| `--capacitor` |
|
|
109
|
-
| `--web-dir <path>` |
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
| Option | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `-l, --label <label>` | Release label (e.g. `v1.0.0`). **Optional** — if omitted, auto-increments `buildNumber` in `upflux.json` (e.g. `1.0.0+1`) |
|
|
119
|
+
| `--platform <platform>` | Target platform: `all`, `ios`, or `android` (default: `all`) |
|
|
120
|
+
| `-a, --app <appId>` | Application ID (auto-detected from stored credentials) |
|
|
121
|
+
| `-d, --deployment <deploymentId>` | Target deployment ID (prompts if multiple exist) |
|
|
122
|
+
| `-m, --mandatory` | Mark update as mandatory |
|
|
123
|
+
| `-r, --rollout <number>` | Rollout percentage: `0`–`100` (default: `100`) |
|
|
124
|
+
| `-v, --version-range <range>` | Target binary version semver range (e.g. `>=1.0.0`) |
|
|
125
|
+
| `--min-binary-version <version>` | Minimum native binary version required. Devices below this receive store update prompts. |
|
|
126
|
+
| `--skip-native-check` | Skip automatic git native change inspection |
|
|
127
|
+
| `--schedule` | Create release as a draft to schedule release time via dashboard |
|
|
128
|
+
| `-e, --entry-file <path>` | React Native entry file (default: `index.js`) |
|
|
129
|
+
| `-o, --output-dir <path>` | Output directory for bundles (default: `./dist`) |
|
|
130
|
+
| `-b, --bundle <path>` | Skip bundler, upload prebuilt bundle file |
|
|
131
|
+
| `--assets <paths...>` | Explicit asset files or directories to include |
|
|
132
|
+
| `--dev` | Create development bundle |
|
|
133
|
+
| `--skip-bundle` | Skip bundling, reuse existing files in output directory |
|
|
134
|
+
| `--capacitor` | Force Capacitor bundling mode |
|
|
135
|
+
| `--web-dir <path>` | Custom web asset directory for Capacitor (default: from config or `www`) |
|
|
136
|
+
|
|
137
|
+
#### Examples
|
|
112
138
|
|
|
113
139
|
```bash
|
|
114
|
-
#
|
|
115
|
-
upflux release
|
|
140
|
+
# Recommended: Auto-increment buildNumber from upflux.json (e.g. 1.0.0+1 -> 1.0.0+2)
|
|
141
|
+
upflux release
|
|
116
142
|
|
|
117
|
-
#
|
|
118
|
-
upflux release --label v1.2.
|
|
119
|
-
|
|
120
|
-
# Android-only release with 50% rollout
|
|
121
|
-
upflux release --label v1.2.2 --platform android --rollout 50
|
|
143
|
+
# Release with explicit custom label
|
|
144
|
+
upflux release --label v1.2.0
|
|
122
145
|
|
|
123
|
-
# Mandatory
|
|
124
|
-
upflux release --
|
|
146
|
+
# Mandatory release with gradual 25% rollout
|
|
147
|
+
upflux release --mandatory --rollout 25
|
|
125
148
|
|
|
126
|
-
#
|
|
127
|
-
upflux release --
|
|
149
|
+
# iOS-only emergency hotfix
|
|
150
|
+
upflux release --platform ios
|
|
128
151
|
|
|
129
|
-
# Capacitor
|
|
130
|
-
upflux release --
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
**Example output:**
|
|
134
|
-
```
|
|
135
|
-
📦 Bundling for iOS...
|
|
136
|
-
✓ iOS bundle created
|
|
137
|
-
📦 Bundling for Android...
|
|
138
|
-
✓ Android bundle created
|
|
139
|
-
|
|
140
|
-
⬆ Uploading release...
|
|
141
|
-
App: app-970384d3
|
|
142
|
-
Deployment: 6933e683aa32fe18a1311bbb
|
|
143
|
-
Deployment Name: production
|
|
144
|
-
Label: v1.2.0
|
|
145
|
-
Platform: all
|
|
146
|
-
Bundle: ./dist/ios/index.ios.bundle
|
|
147
|
-
Assets: 24 file(s)
|
|
148
|
-
Rollout: 100%
|
|
149
|
-
Mandatory: No
|
|
150
|
-
|
|
151
|
-
📋 Step 1/3: Requesting upload URLs...
|
|
152
|
-
✓ Got 26 presigned URLs
|
|
153
|
-
📤 Step 2/3: Uploading files directly to storage...
|
|
154
|
-
[1/26] index.ios.bundle
|
|
155
|
-
...
|
|
156
|
-
✓ Uploaded 26 files
|
|
157
|
-
✅ Step 3/3: Confirming release...
|
|
158
|
-
|
|
159
|
-
✓ Release uploaded successfully!
|
|
160
|
-
──────────────────────────────────
|
|
161
|
-
ID: 693507a19f6d6a79267633e8
|
|
162
|
-
Label: v1.2.0
|
|
163
|
-
iOS Bundle: uploaded
|
|
164
|
-
Android Bundle: uploaded
|
|
165
|
-
Rollout: 100%
|
|
166
|
-
Mandatory: No
|
|
167
|
-
Status: active
|
|
168
|
-
──────────────────────────────────
|
|
152
|
+
# Capacitor release with custom build folder
|
|
153
|
+
upflux release --capacitor --web-dir build
|
|
169
154
|
```
|
|
170
155
|
|
|
171
156
|
---
|
|
172
157
|
|
|
173
|
-
##
|
|
174
|
-
|
|
175
|
-
The CLI automatically detects native code changes since your last release and warns you when a store update may be required. This helps prevent devices from receiving OTA updates that depend on native features not yet available in their installed binary.
|
|
176
|
-
|
|
177
|
-
### How It Works
|
|
158
|
+
## Auto-incrementing Build Numbers
|
|
178
159
|
|
|
179
|
-
|
|
180
|
-
2. On the next release, it runs `git diff <lastHash>..HEAD` against native files.
|
|
181
|
-
3. If changes are found, it warns you and offers to auto-set `--min-binary-version`.
|
|
160
|
+
When releasing without `--label`, the CLI reads `upflux.json` at your project root:
|
|
182
161
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|---|---|
|
|
189
|
-
| `android/app/build.gradle` | `versionName` bump, `minSdkVersion` change |
|
|
190
|
-
| `android/app/src/main/AndroidManifest.xml` | New permissions |
|
|
191
|
-
| `ios/Podfile` | New native pods |
|
|
192
|
-
| `ios/Podfile.lock` | Any pod version change |
|
|
193
|
-
| `package.json` | New native dependency packages |
|
|
194
|
-
|
|
195
|
-
### Example Interaction
|
|
162
|
+
1. Reads `runtimeVersion` (e.g., `"1.0.0"`) and current `buildNumber` (e.g., `0`)
|
|
163
|
+
2. Increments `buildNumber` by 1 (`1`)
|
|
164
|
+
3. Generates release label: `1.0.0+1`
|
|
165
|
+
4. Writes updated `buildNumber` back to `upflux.json`
|
|
166
|
+
5. Embeds updated `upflux.json` into the native assets directory
|
|
196
167
|
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"version": 1,
|
|
171
|
+
"app": {
|
|
172
|
+
"id": "app-970384d3",
|
|
173
|
+
"buildNumber": 1,
|
|
174
|
+
"runtimeVersion": "1.0.0"
|
|
175
|
+
},
|
|
176
|
+
"deployments": { ... }
|
|
177
|
+
}
|
|
197
178
|
```
|
|
198
|
-
⚠️ Native changes detected since last release:
|
|
199
179
|
|
|
200
|
-
|
|
201
|
-
• build.gradle: versionName → 2.0.0
|
|
202
|
-
• AndroidManifest.xml: New permission: android.permission.CAMERA
|
|
180
|
+
> 💡 **Best Practice:** Commit `upflux.json` to git after running `upflux release` so the build counter is synchronized across your team and CI/CD pipelines.
|
|
203
181
|
|
|
204
|
-
|
|
205
|
-
--min-binary-version 2.0.0
|
|
206
|
-
--version-range ">=2.0.0"
|
|
182
|
+
---
|
|
207
183
|
|
|
208
|
-
|
|
209
|
-
❯ Auto-apply suggested flags (--min-binary-version 2.0.0 --version-range ">=2.0.0")
|
|
210
|
-
Enter min-binary-version manually
|
|
211
|
-
Continue without (not recommended for native changes)
|
|
212
|
-
```
|
|
184
|
+
## Native Change Detection
|
|
213
185
|
|
|
214
|
-
|
|
186
|
+
The CLI automatically inspects native changes since your last release to prevent pushing OTA updates that depend on unreleased native code.
|
|
215
187
|
|
|
216
|
-
|
|
217
|
-
# Require devices to have at least v2.0.0 binary before receiving this OTA
|
|
218
|
-
upflux release --label v2.0.0-ota --min-binary-version 2.0.0 --version-range ">=2.0.0"
|
|
219
|
-
```
|
|
188
|
+
### Files Monitored
|
|
220
189
|
|
|
221
|
-
|
|
190
|
+
| File | Monitored Changes |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `android/app/build.gradle` | `versionName` changes, `minSdkVersion` bumps |
|
|
193
|
+
| `android/app/src/main/AndroidManifest.xml` | Added/removed Android permissions |
|
|
194
|
+
| `ios/Podfile` | Added native CocoaPods |
|
|
195
|
+
| `ios/Podfile.lock` | Updated CocoaPod versions |
|
|
196
|
+
| `package.json` | Newly added native dependencies |
|
|
222
197
|
|
|
223
|
-
|
|
224
|
-
# In CI, skip interactive detection and pass flag explicitly
|
|
225
|
-
upflux release --label v2.0.0 --min-binary-version 2.0.0 --skip-native-check
|
|
226
|
-
```
|
|
198
|
+
If changes are detected, the CLI prompts you to automatically set `--min-binary-version`. In CI/CD environments, use `--skip-native-check` or specify `--min-binary-version` directly.
|
|
227
199
|
|
|
228
200
|
---
|
|
229
201
|
|
|
230
202
|
### `status`
|
|
231
203
|
|
|
232
|
-
|
|
204
|
+
View deployment status, active release, rollout percentage, and recent releases.
|
|
233
205
|
|
|
234
206
|
```bash
|
|
235
207
|
upflux status [options]
|
|
@@ -237,34 +209,26 @@ upflux status [options]
|
|
|
237
209
|
|
|
238
210
|
| Option | Description |
|
|
239
211
|
|--------|-------------|
|
|
240
|
-
| `-n, --limit <number>` | Number of releases to
|
|
212
|
+
| `-n, --limit <number>` | Number of recent releases to display (default: 5) |
|
|
241
213
|
|
|
242
|
-
**Example
|
|
214
|
+
**Example:**
|
|
243
215
|
```
|
|
244
216
|
⚡ Fetching deployment status...
|
|
245
217
|
|
|
246
218
|
╭────────────────────────────────────────────────╮
|
|
247
219
|
│ 📱 app-970384d3 → production │
|
|
248
220
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
|
249
|
-
│ Current Release:
|
|
221
|
+
│ Current Release: 1.0.0+4 (iOS & Android) │
|
|
250
222
|
│ Status: ● active • 100% rollout │
|
|
251
|
-
│ Published:
|
|
223
|
+
│ Published: 15m ago │
|
|
252
224
|
╰────────────────────────────────────────────────╯
|
|
253
|
-
|
|
254
|
-
📦 Recent Releases (15 total)
|
|
255
|
-
|
|
256
|
-
Label Platform Status Rollout Published
|
|
257
|
-
────────────────────────────────────────────────────────
|
|
258
|
-
v1.2.0 all ● active 100% 2h ago
|
|
259
|
-
v1.1.0 all ○ expired 100% 3d ago
|
|
260
|
-
v1.0.0 ios ○ expired 100% 1w ago
|
|
261
225
|
```
|
|
262
226
|
|
|
263
227
|
---
|
|
264
228
|
|
|
265
229
|
### `generate-manifest`
|
|
266
230
|
|
|
267
|
-
Fetch and display the manifest for a specific release.
|
|
231
|
+
Fetch and display or export the update manifest for a specific release ID.
|
|
268
232
|
|
|
269
233
|
```bash
|
|
270
234
|
upflux generate-manifest [options]
|
|
@@ -273,51 +237,7 @@ upflux generate-manifest [options]
|
|
|
273
237
|
| Option | Required | Description |
|
|
274
238
|
|--------|----------|-------------|
|
|
275
239
|
| `-r, --release-id <id>` | ✓ | Release ID (MongoDB ObjectId) |
|
|
276
|
-
| `-o, --output <path>` | | Save manifest to file |
|
|
277
|
-
|
|
278
|
-
**Example:**
|
|
279
|
-
```bash
|
|
280
|
-
# Print manifest to stdout
|
|
281
|
-
upflux generate-manifest --release-id 693507a19f6d6a79267633e8
|
|
282
|
-
|
|
283
|
-
# Save to file
|
|
284
|
-
upflux generate-manifest --release-id 693507a19f6d6a79267633e8 --output manifest.json
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## Scheduled Releases
|
|
290
|
-
|
|
291
|
-
Schedule releases to go live at a specific date and time. Perfect for coordinating updates with marketing campaigns, feature launches, or maintenance windows.
|
|
292
|
-
|
|
293
|
-
### How It Works
|
|
294
|
-
|
|
295
|
-
1. Create a release with the `--schedule` flag
|
|
296
|
-
2. The release is uploaded as a **Draft**
|
|
297
|
-
3. Set the scheduled time in the Upflux Dashboard
|
|
298
|
-
4. The release automatically goes live at the scheduled time
|
|
299
|
-
|
|
300
|
-
### Creating a Scheduled Release
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
# Create a draft release for scheduling
|
|
304
|
-
upflux release --label v2.0.0 --schedule
|
|
305
|
-
|
|
306
|
-
# Output:
|
|
307
|
-
# ✓ Release uploaded successfully!
|
|
308
|
-
# ...
|
|
309
|
-
# Status: DRAFT
|
|
310
|
-
# 📅 Set schedule time in dashboard
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Use Cases
|
|
314
|
-
|
|
315
|
-
- **Feature launches** - Coordinate with marketing announcements
|
|
316
|
-
- **Global releases** - Release at optimal times for different regions
|
|
317
|
-
- **Maintenance windows** - Deploy during low-traffic periods
|
|
318
|
-
- **Campaign coordination** - Sync with promotional events
|
|
319
|
-
|
|
320
|
-
> 💡 **Tip:** Combine scheduled releases with rollout percentages for safer deployments. For example, schedule a 10% rollout first, then manually increase after monitoring.
|
|
240
|
+
| `-o, --output <path>` | | Save manifest JSON to file |
|
|
321
241
|
|
|
322
242
|
---
|
|
323
243
|
|
|
@@ -327,67 +247,16 @@ upflux release --label v2.0.0 --schedule
|
|
|
327
247
|
|
|
328
248
|
| Variable | Description | Default |
|
|
329
249
|
|----------|-------------|---------|
|
|
330
|
-
| `UPFLUX_API_URL` | API server URL | `https://api.upflux.in/api` |
|
|
331
|
-
|
|
332
|
-
### Auth File
|
|
333
|
-
|
|
334
|
-
The CLI stores credentials in `.upflux` in the current directory:
|
|
335
|
-
|
|
336
|
-
```json
|
|
337
|
-
{
|
|
338
|
-
"credentials": [
|
|
339
|
-
{
|
|
340
|
-
"key": "upflux_pub_xxx...",
|
|
341
|
-
"appId": "app-970384d3",
|
|
342
|
-
"deploymentId": "6933e683aa32fe18a1311bbb",
|
|
343
|
-
"deploymentName": "production"
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
"key": "upflux_pub_yyy...",
|
|
347
|
-
"appId": "app-970384d3",
|
|
348
|
-
"deploymentId": "6933e683aa32fe18a1311ccc",
|
|
349
|
-
"deploymentName": "staging"
|
|
350
|
-
}
|
|
351
|
-
],
|
|
352
|
-
"activeIndex": 0,
|
|
353
|
-
"lastReleaseHash": "a3f8c12d...",
|
|
354
|
-
"lastReleaseLabel": "v1.5.0"
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
> ⚠️ **Important:** Never commit the `.upflux` file to version control. It contains your publish keys. Add it to your `.gitignore`.
|
|
250
|
+
| `UPFLUX_API_URL` | Upflux API server URL | `https://api.upflux.in/api` |
|
|
359
251
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
## Publish Keys
|
|
363
|
-
|
|
364
|
-
Publish keys are used to authenticate the CLI and can be found in the Upflux dashboard under **Settings → Keys**.
|
|
252
|
+
### Auth Storage (`.upflux`)
|
|
365
253
|
|
|
366
|
-
|
|
254
|
+
Stored credentials and release metadata are saved in `.upflux` in the current project directory.
|
|
367
255
|
|
|
368
|
-
|
|
256
|
+
> ⚠️ **Important:** Add `.upflux` to your `.gitignore`. It contains your publish keys (`upx_pub_*`).
|
|
369
257
|
|
|
370
258
|
---
|
|
371
259
|
|
|
372
|
-
## Development
|
|
373
|
-
|
|
374
|
-
```bash
|
|
375
|
-
# Install dependencies
|
|
376
|
-
pnpm install
|
|
377
|
-
|
|
378
|
-
# Build
|
|
379
|
-
pnpm run build
|
|
380
|
-
|
|
381
|
-
# Run locally
|
|
382
|
-
node dist/index.js --help
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Requirements
|
|
386
|
-
|
|
387
|
-
- Node.js >= 18.0.0
|
|
388
|
-
- For React Native projects: `react-native` CLI installed
|
|
389
|
-
- For Capacitor projects: Built web assets in configured `webDir`
|
|
390
|
-
|
|
391
260
|
## License
|
|
392
261
|
|
|
393
262
|
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const commander_1 = require("commander");
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const UPFLUX_JSON_FILE = path_1.default.join(process.cwd(), "upflux.json");
|
|
12
|
+
const DEFAULT_SERVER_URL = "https://api.upflux.in";
|
|
13
|
+
/**
|
|
14
|
+
* Build the upflux.json content from collected answers
|
|
15
|
+
*/
|
|
16
|
+
function buildUpfluxJson(appId, serverUrl, runtimeVersion, profiles) {
|
|
17
|
+
const deployments = {};
|
|
18
|
+
for (const profile of profiles) {
|
|
19
|
+
const entry = {
|
|
20
|
+
clientKey: profile.clientKey,
|
|
21
|
+
};
|
|
22
|
+
if (profile.extendsProfile) {
|
|
23
|
+
entry.extends = profile.extendsProfile;
|
|
24
|
+
}
|
|
25
|
+
// Set sensible defaults per profile name
|
|
26
|
+
if (profile.name === "development") {
|
|
27
|
+
entry.checkOnLaunch = "always";
|
|
28
|
+
entry.installMode = "on_next_restart";
|
|
29
|
+
}
|
|
30
|
+
else if (profile.name === "production") {
|
|
31
|
+
entry.checkOnLaunch = "always";
|
|
32
|
+
entry.installMode = "immediate";
|
|
33
|
+
}
|
|
34
|
+
deployments[profile.name] = entry;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
app: Object.assign({ id: appId, buildNumber: 0, runtimeVersion }, (serverUrl !== DEFAULT_SERVER_URL ? { serverUrl } : {})),
|
|
39
|
+
deployments,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const init = new commander_1.Command("init")
|
|
43
|
+
.description("Scaffold upflux.json in your project root")
|
|
44
|
+
.option("--force", "Overwrite existing upflux.json")
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
console.log(chalk_1.default.bold("\n🚀 Upflux Project Setup\n"));
|
|
47
|
+
// Check if upflux.json already exists
|
|
48
|
+
if (fs_1.default.existsSync(UPFLUX_JSON_FILE) && !opts.force) {
|
|
49
|
+
console.log(chalk_1.default.yellow("⚠️ upflux.json already exists."));
|
|
50
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: "confirm",
|
|
53
|
+
name: "overwrite",
|
|
54
|
+
message: "Overwrite it?",
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
if (!overwrite) {
|
|
59
|
+
console.log(chalk_1.default.gray("Aborted."));
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Step 1: App ID
|
|
64
|
+
const { appId } = await inquirer_1.default.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: "input",
|
|
67
|
+
name: "appId",
|
|
68
|
+
message: "App ID:",
|
|
69
|
+
validate: (val) => val.trim() ? true : "App ID is required",
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
// Step 2: Server URL
|
|
73
|
+
const { serverUrl } = await inquirer_1.default.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: "input",
|
|
76
|
+
name: "serverUrl",
|
|
77
|
+
message: `Server URL (press Enter for ${DEFAULT_SERVER_URL}):`,
|
|
78
|
+
default: DEFAULT_SERVER_URL,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
// Step 3: Runtime version (semver, used to form auto release labels)
|
|
82
|
+
const { runtimeVersion } = await inquirer_1.default.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: "input",
|
|
85
|
+
name: "runtimeVersion",
|
|
86
|
+
message: "Runtime version (e.g. 1.0.0 — update when shipping a new binary):",
|
|
87
|
+
default: "1.0.0",
|
|
88
|
+
validate: (val) => {
|
|
89
|
+
if (!val.trim())
|
|
90
|
+
return "Runtime version is required";
|
|
91
|
+
if (!/^\d+\.\d+\.\d+/.test(val.trim()))
|
|
92
|
+
return "Use semver format, e.g. 1.0.0";
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
const { selectedProfiles } = await inquirer_1.default.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: "checkbox",
|
|
100
|
+
name: "selectedProfiles",
|
|
101
|
+
message: "Deployment profiles to create:",
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: "production", checked: true },
|
|
104
|
+
{ name: "staging", checked: true },
|
|
105
|
+
{ name: "development", checked: true },
|
|
106
|
+
],
|
|
107
|
+
validate: (selected) => selected.length > 0 ? true : "Select at least one profile",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
// Step 4: Client keys per profile
|
|
111
|
+
const profiles = [];
|
|
112
|
+
for (const profileName of selectedProfiles) {
|
|
113
|
+
const { clientKey } = await inquirer_1.default.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: "input",
|
|
116
|
+
name: "clientKey",
|
|
117
|
+
message: `Client key for "${chalk_1.default.cyan(profileName)}" ${chalk_1.default.gray("(upx_cli_xxxxx)")}:`,
|
|
118
|
+
validate: (val) => {
|
|
119
|
+
if (!val.trim())
|
|
120
|
+
return "Client key is required";
|
|
121
|
+
if (val.trim().length < 10)
|
|
122
|
+
return "Client key looks too short";
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
profiles.push({ name: profileName, clientKey: clientKey.trim() });
|
|
128
|
+
}
|
|
129
|
+
// Build and write upflux.json
|
|
130
|
+
const content = buildUpfluxJson(appId.trim(), serverUrl.trim(), runtimeVersion.trim(), profiles);
|
|
131
|
+
fs_1.default.writeFileSync(UPFLUX_JSON_FILE, JSON.stringify(content, null, 2));
|
|
132
|
+
console.log(chalk_1.default.green("\n✓ Created upflux.json\n"));
|
|
133
|
+
// Show what was created
|
|
134
|
+
console.log(chalk_1.default.gray("Preview:"));
|
|
135
|
+
console.log(chalk_1.default.gray(JSON.stringify(content, null, 2)));
|
|
136
|
+
// Usage hint
|
|
137
|
+
console.log(chalk_1.default.bold("\n📦 SDK Usage:\n"));
|
|
138
|
+
console.log(chalk_1.default.white(` import { Upflux } from '@upfluxhq/react-native';\n\n` +
|
|
139
|
+
` // Auto-selects 'development' in dev, 'production' in release\n` +
|
|
140
|
+
` // Auto-increments buildNumber on every release\n` +
|
|
141
|
+
` await Upflux.init();\n\n` +
|
|
142
|
+
` // Or specify a profile:\n` +
|
|
143
|
+
` await Upflux.init('production');`));
|
|
144
|
+
// Security note about committing the file
|
|
145
|
+
console.log(chalk_1.default.cyan("\n💡 Tip: upflux.json is safe to commit to git.\n" +
|
|
146
|
+
" upx_cli_* keys are read-only — they cannot publish releases.\n" +
|
|
147
|
+
" Only upx_pub_* publish keys should be kept secret.\n"));
|
|
148
|
+
});
|
|
149
|
+
exports.default = init;
|
package/dist/commands/release.js
CHANGED
|
@@ -45,6 +45,7 @@ const inquirer_1 = __importDefault(require("inquirer"));
|
|
|
45
45
|
const archiver_1 = __importDefault(require("archiver"));
|
|
46
46
|
const apiClient_1 = require("../client/apiClient");
|
|
47
47
|
const configStore_1 = require("../config/configStore");
|
|
48
|
+
const native_detector_1 = require("../utils/native-detector");
|
|
48
49
|
/**
|
|
49
50
|
* Default output directory for React Native bundle
|
|
50
51
|
*/
|
|
@@ -134,11 +135,41 @@ function isValidAppId(appId) {
|
|
|
134
135
|
return /^[a-zA-Z0-9-_]+$/.test(appId) && appId.length >= 3;
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
137
|
-
* Validate release label format (semver-like)
|
|
138
|
+
* Validate release label format (semver-like, allows + build metadata)
|
|
138
139
|
*/
|
|
139
140
|
function isValidReleaseLabel(label) {
|
|
140
|
-
// Allow v1.0.0, 1.0.0, v1.0.0-beta, etc.
|
|
141
|
-
return /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?$/.test(label);
|
|
141
|
+
// Allow v1.0.0, 1.0.0, v1.0.0-beta, 1.0.0+5, 1.0.0-beta+5, etc.
|
|
142
|
+
return /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?(\+[a-zA-Z0-9._-]+)?$/.test(label);
|
|
143
|
+
}
|
|
144
|
+
/** Read upflux.json from project root. Returns null if not found. */
|
|
145
|
+
function readRootUpfluxJson() {
|
|
146
|
+
const filePath = path.resolve('upflux.json');
|
|
147
|
+
if (!fs.existsSync(filePath))
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
151
|
+
}
|
|
152
|
+
catch (_a) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Increment app.buildNumber, write back to upflux.json, return new value. */
|
|
157
|
+
function incrementBuildNumber(json) {
|
|
158
|
+
var _a;
|
|
159
|
+
const next = ((_a = json.app.buildNumber) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
160
|
+
json.app.buildNumber = next;
|
|
161
|
+
fs.writeFileSync(path.resolve('upflux.json'), JSON.stringify(json, null, 2));
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Generate an auto release label.
|
|
166
|
+
* With runtimeVersion: "1.0.0+6" (semver + build counter)
|
|
167
|
+
* Without: "v0.0.6" (bare semver fallback — passes validator)
|
|
168
|
+
*/
|
|
169
|
+
function generateReleaseLabel(buildNumber, runtimeVersion) {
|
|
170
|
+
return runtimeVersion
|
|
171
|
+
? `${runtimeVersion}+${buildNumber}`
|
|
172
|
+
: `v0.0.${buildNumber}`;
|
|
142
173
|
}
|
|
143
174
|
/**
|
|
144
175
|
* Validate version range format
|
|
@@ -255,28 +286,25 @@ function collectAssets(dir) {
|
|
|
255
286
|
return assets;
|
|
256
287
|
}
|
|
257
288
|
/**
|
|
258
|
-
* Write upflux.json config file to the bundle output directory
|
|
289
|
+
* Write upflux.json config file to the bundle output directory.
|
|
259
290
|
*
|
|
260
291
|
* This file embeds the OTA version into the app bundle at build time.
|
|
261
|
-
*
|
|
262
|
-
*
|
|
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
|
|
292
|
+
* `releaseLabel` is used by the SDK for identity check (am I running the latest?).
|
|
293
|
+
* `buildNumber` is stored as metadata for display/debugging.
|
|
267
294
|
*/
|
|
268
|
-
function writeUpfluxConfig(outputDir, releaseLabel, appId) {
|
|
295
|
+
function writeUpfluxConfig(outputDir, releaseLabel, appId, buildNumber) {
|
|
269
296
|
const config = {
|
|
270
297
|
releaseLabel,
|
|
271
298
|
appId,
|
|
272
299
|
createdAt: new Date().toISOString(),
|
|
273
300
|
};
|
|
301
|
+
if (buildNumber !== undefined) {
|
|
302
|
+
config.buildNumber = buildNumber;
|
|
303
|
+
}
|
|
274
304
|
const configContent = JSON.stringify(config, null, 2);
|
|
275
305
|
const configPath = path.join(outputDir, "upflux.json");
|
|
276
306
|
fs.writeFileSync(configPath, configContent);
|
|
277
|
-
console.log(chalk_1.default.gray(` Created upflux.json
|
|
278
|
-
// Also copy to native asset directories so the SDK can read the
|
|
279
|
-
// embedded version at runtime and skip unnecessary OTA downloads
|
|
307
|
+
console.log(chalk_1.default.gray(` Created upflux.json (label: ${releaseLabel}${buildNumber !== undefined ? `, build: ${buildNumber}` : ''})`));
|
|
280
308
|
copyUpfluxConfigToNativeAssets(configContent);
|
|
281
309
|
}
|
|
282
310
|
/**
|
|
@@ -316,7 +344,7 @@ const release = new commander_1.Command("release")
|
|
|
316
344
|
.description("Bundle and upload a new release to Upflux")
|
|
317
345
|
.option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
|
|
318
346
|
.option("-d, --deployment <deploymentId>", "Deployment ID (uses stored value if not provided)")
|
|
319
|
-
.
|
|
347
|
+
.option("-l, --label <label>", "Release label (e.g., v1.0.0) — omit to auto-increment buildNumber from upflux.json")
|
|
320
348
|
.option("--platform <platform>", "Target platform for the release: all, ios, or android (defaults to all)", "all")
|
|
321
349
|
.option("-e, --entry-file <path>", "Entry file for bundler", DEFAULT_ENTRY_FILE)
|
|
322
350
|
.option("-o, --output-dir <path>", "Output directory for bundle", DEFAULT_OUTPUT_DIR)
|
|
@@ -325,9 +353,11 @@ const release = new commander_1.Command("release")
|
|
|
325
353
|
.option("-m, --mandatory", "Mark update as mandatory", false)
|
|
326
354
|
.option("-r, --rollout <number>", "Rollout percentage (0-100)", "100")
|
|
327
355
|
.option("-v, --version-range <range>", "Binary version range (e.g., >=1.0.0)")
|
|
356
|
+
.option("--min-binary-version <version>", "Minimum native binary version devices must have before receiving this OTA update. Devices below this version will be directed to update from the app store.")
|
|
328
357
|
.option("--dev", "Create development bundle", false)
|
|
329
358
|
.option("--schedule", "Create as draft, set schedule time in dashboard")
|
|
330
359
|
.option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
|
|
360
|
+
.option("--skip-native-check", "Skip automatic native change detection (useful in CI/CD)", false)
|
|
331
361
|
.option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
|
|
332
362
|
.option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
|
|
333
363
|
.action(async (opts) => {
|
|
@@ -402,11 +432,41 @@ const release = new commander_1.Command("release")
|
|
|
402
432
|
console.error(chalk_1.default.gray("Deployment ID should be a 24-character MongoDB ObjectId"));
|
|
403
433
|
process.exit(1);
|
|
404
434
|
}
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
435
|
+
// ── Label Resolution ─────────────────────────────────────────────
|
|
436
|
+
// Two paths: manual (--label provided) or auto (buildNumber from upflux.json)
|
|
437
|
+
let resolvedLabel;
|
|
438
|
+
const rootJson = readRootUpfluxJson();
|
|
439
|
+
const currentBuildNumber = rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber;
|
|
440
|
+
if (opts.label) {
|
|
441
|
+
// Manual mode: validate semver (now allows + notation), still increment buildNumber for tracking
|
|
442
|
+
if (!isValidReleaseLabel(opts.label)) {
|
|
443
|
+
console.error(chalk_1.default.red("✗ Invalid release label format"));
|
|
444
|
+
console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, 1.0.0+5, etc."));
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
resolvedLabel = opts.label;
|
|
448
|
+
if (rootJson && currentBuildNumber !== undefined) {
|
|
449
|
+
const tracked = incrementBuildNumber(rootJson);
|
|
450
|
+
console.log(chalk_1.default.gray(` buildNumber → ${tracked} (tracking)`));
|
|
451
|
+
}
|
|
452
|
+
console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (manual)`));
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// Auto mode: require upflux.json with buildNumber field
|
|
456
|
+
if (!rootJson) {
|
|
457
|
+
console.error(chalk_1.default.red("✗ No upflux.json found and no --label provided"));
|
|
458
|
+
console.error(chalk_1.default.gray("Run: upflux init OR upflux release --label v1.0.0"));
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
if (rootJson.app.buildNumber === undefined) {
|
|
462
|
+
console.error(chalk_1.default.red("✗ upflux.json is missing app.buildNumber"));
|
|
463
|
+
console.error(chalk_1.default.gray('Add "buildNumber": 0 to the app section, or use --label'));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
const nextBuild = incrementBuildNumber(rootJson);
|
|
467
|
+
resolvedLabel = generateReleaseLabel(nextBuild, rootJson.app.runtimeVersion);
|
|
468
|
+
console.log(chalk_1.default.green(` ✓ buildNumber: ${nextBuild - 1} → ${nextBuild}`));
|
|
469
|
+
console.log(chalk_1.default.gray(` Label: ${resolvedLabel} (auto)`));
|
|
410
470
|
}
|
|
411
471
|
// Validation 6: Rollout percentage
|
|
412
472
|
const rolloutValidation = isValidRollout(opts.rollout);
|
|
@@ -464,10 +524,10 @@ const release = new commander_1.Command("release")
|
|
|
464
524
|
}
|
|
465
525
|
// Write upflux.json to web directory BEFORE zipping
|
|
466
526
|
// This embeds the OTA version into the Capacitor app bundle
|
|
467
|
-
writeUpfluxConfig(webDirPath,
|
|
527
|
+
writeUpfluxConfig(webDirPath, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
|
|
468
528
|
// Zip the web directory
|
|
469
529
|
console.log(chalk_1.default.blue(`📦 Zipping ${webDir}/ directory...`));
|
|
470
|
-
const zipPath = path.join(outputDir, `bundle-${
|
|
530
|
+
const zipPath = path.join(outputDir, `bundle-${resolvedLabel}.zip`);
|
|
471
531
|
bundlePath = await zipDirectory(webDirPath, zipPath);
|
|
472
532
|
console.log(chalk_1.default.green(`✓ Bundle created: ${bundlePath}`));
|
|
473
533
|
// No separate assets for Capacitor - all in zip
|
|
@@ -569,8 +629,8 @@ const release = new commander_1.Command("release")
|
|
|
569
629
|
: collectAssets(androidAssetsPath);
|
|
570
630
|
// Write upflux.json to BOTH platform output directories
|
|
571
631
|
// React Native reads this from bundled assets to know embedded OTA version
|
|
572
|
-
writeUpfluxConfig(iosOutputDir,
|
|
573
|
-
writeUpfluxConfig(androidOutputDir,
|
|
632
|
+
writeUpfluxConfig(iosOutputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
|
|
633
|
+
writeUpfluxConfig(androidOutputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
|
|
574
634
|
// Set bundlePath and assetFiles for display (use iOS as primary for logging)
|
|
575
635
|
bundlePath = iosBundlePath;
|
|
576
636
|
assetFiles = [...iosAssetFiles, ...androidAssetFiles];
|
|
@@ -615,7 +675,81 @@ const release = new commander_1.Command("release")
|
|
|
615
675
|
}
|
|
616
676
|
// Write upflux.json to output directory
|
|
617
677
|
// React Native reads this from bundled assets to know embedded OTA version
|
|
618
|
-
writeUpfluxConfig(outputDir,
|
|
678
|
+
writeUpfluxConfig(outputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// ========================================
|
|
682
|
+
// NATIVE CHANGE DETECTION PHASE
|
|
683
|
+
// ========================================
|
|
684
|
+
if (!opts.skipNativeCheck) {
|
|
685
|
+
const gitStatus = (0, native_detector_1.getGitRepoStatus)();
|
|
686
|
+
if (!gitStatus) {
|
|
687
|
+
// Not a git repo or git not installed — skip silently
|
|
688
|
+
console.log(chalk_1.default.gray(" ⓘ Git not detected — skipping native change check"));
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const lastHash = configStore_1.configStore.getLastReleaseHash();
|
|
692
|
+
const lastLabel = configStore_1.configStore.getLastReleaseLabel();
|
|
693
|
+
if (!lastHash) {
|
|
694
|
+
// First release — nothing to compare against
|
|
695
|
+
console.log(chalk_1.default.gray(" ⓘ First release — native change detection will run from next release"));
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.log(chalk_1.default.gray(` 🔍 Checking native changes since ${lastLabel || lastHash.slice(0, 7)}...`));
|
|
699
|
+
const detection = (0, native_detector_1.detectNativeChanges)(lastHash);
|
|
700
|
+
if (detection.hasNativeChanges) {
|
|
701
|
+
(0, native_detector_1.displayNativeChangeWarning)(detection);
|
|
702
|
+
// Only prompt if --min-binary-version not already provided
|
|
703
|
+
if (!opts.minBinaryVersion) {
|
|
704
|
+
const choices = [
|
|
705
|
+
...(detection.suggestedMinBinaryVersion
|
|
706
|
+
? [{
|
|
707
|
+
name: `Auto-apply suggested flags (--min-binary-version ${detection.suggestedMinBinaryVersion} --version-range "${detection.suggestedVersionRange}")`,
|
|
708
|
+
value: 'auto',
|
|
709
|
+
}]
|
|
710
|
+
: []),
|
|
711
|
+
{ name: 'Enter min-binary-version manually', value: 'custom' },
|
|
712
|
+
{ name: 'Continue without (not recommended for native changes)', value: 'skip' },
|
|
713
|
+
];
|
|
714
|
+
const { action } = await inquirer_1.default.prompt([{
|
|
715
|
+
type: 'list',
|
|
716
|
+
name: 'action',
|
|
717
|
+
message: 'How would you like to handle these native changes?',
|
|
718
|
+
choices,
|
|
719
|
+
}]);
|
|
720
|
+
if (action === 'auto') {
|
|
721
|
+
opts.minBinaryVersion = detection.suggestedMinBinaryVersion;
|
|
722
|
+
if (!opts.versionRange && detection.suggestedVersionRange) {
|
|
723
|
+
opts.versionRange = detection.suggestedVersionRange;
|
|
724
|
+
}
|
|
725
|
+
console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
|
|
726
|
+
console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
|
|
727
|
+
}
|
|
728
|
+
else if (action === 'custom') {
|
|
729
|
+
const { version } = await inquirer_1.default.prompt([{
|
|
730
|
+
type: 'input',
|
|
731
|
+
name: 'version',
|
|
732
|
+
message: 'Enter minimum binary version (e.g., 2.0.0):',
|
|
733
|
+
default: detection.suggestedMinBinaryVersion,
|
|
734
|
+
validate: (v) => /^\d+\.\d+\.\d+/.test(v) || 'Enter a valid semver version (e.g., 2.0.0)',
|
|
735
|
+
}]);
|
|
736
|
+
opts.minBinaryVersion = version;
|
|
737
|
+
if (!opts.versionRange) {
|
|
738
|
+
opts.versionRange = `>=${version}`;
|
|
739
|
+
}
|
|
740
|
+
console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
|
|
741
|
+
console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
|
|
742
|
+
}
|
|
743
|
+
// 'skip' — continue without setting flags
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
console.log(chalk_1.default.gray(` ⓘ --min-binary-version already set to ${opts.minBinaryVersion}`));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
console.log(chalk_1.default.green(' ✓ No native changes detected'));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
619
753
|
}
|
|
620
754
|
}
|
|
621
755
|
// ========================================
|
|
@@ -625,7 +759,7 @@ const release = new commander_1.Command("release")
|
|
|
625
759
|
console.log(` App: ${appId}`);
|
|
626
760
|
console.log(` Deployment: ${deploymentId}`);
|
|
627
761
|
console.log(` Deployment Name: ${deploymentName}`);
|
|
628
|
-
console.log(` Label: ${
|
|
762
|
+
console.log(` Label: ${resolvedLabel}`);
|
|
629
763
|
console.log(` Platform: ${platform}`);
|
|
630
764
|
console.log(` Bundle: ${bundlePath}`);
|
|
631
765
|
console.log(` Assets: ${assetFiles.length} file(s)`);
|
|
@@ -637,6 +771,9 @@ const release = new commander_1.Command("release")
|
|
|
637
771
|
if (opts.versionRange) {
|
|
638
772
|
console.log(` Version Range: ${opts.versionRange}`);
|
|
639
773
|
}
|
|
774
|
+
if (opts.minBinaryVersion) {
|
|
775
|
+
console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
|
|
776
|
+
}
|
|
640
777
|
// ========================================
|
|
641
778
|
// PRESIGNED URL UPLOAD FLOW
|
|
642
779
|
// ========================================
|
|
@@ -654,7 +791,7 @@ const release = new commander_1.Command("release")
|
|
|
654
791
|
const initResponse = await apiClient_1.apiClient.post("/cli/releases/upload/init", {
|
|
655
792
|
appId,
|
|
656
793
|
deploymentId,
|
|
657
|
-
releaseLabel:
|
|
794
|
+
releaseLabel: resolvedLabel,
|
|
658
795
|
platform,
|
|
659
796
|
files: fileManifest.map(f => ({
|
|
660
797
|
type: f.type,
|
|
@@ -701,18 +838,9 @@ const release = new commander_1.Command("release")
|
|
|
701
838
|
}
|
|
702
839
|
}
|
|
703
840
|
// Step 3: Complete upload and create release record
|
|
704
|
-
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", {
|
|
705
|
-
uploadId: initResponse.data.uploadId,
|
|
706
|
-
releaseLabel: opts.label,
|
|
707
|
-
mandatory: opts.mandatory,
|
|
708
|
-
rolloutPercentage: rolloutValidation.value,
|
|
709
|
-
binaryVersionRange: opts.versionRange,
|
|
710
|
-
platform,
|
|
711
|
-
isDraft: opts.schedule,
|
|
841
|
+
const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", Object.assign(Object.assign({ uploadId: initResponse.data.uploadId, releaseLabel: resolvedLabel, mandatory: opts.mandatory, rolloutPercentage: rolloutValidation.value, binaryVersionRange: opts.versionRange }, (opts.minBinaryVersion ? { minBinaryVersion: opts.minBinaryVersion } : {})), { platform, isDraft: opts.schedule,
|
|
712
842
|
// Include bundle sizes
|
|
713
|
-
iosBundleSize: bundleSizes.ios,
|
|
714
|
-
androidBundleSize: bundleSizes.android || bundleSizes.single,
|
|
715
|
-
}, {
|
|
843
|
+
iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), {
|
|
716
844
|
headers: {
|
|
717
845
|
"x-publish-key": authKey,
|
|
718
846
|
"Content-Type": "application/json",
|
|
@@ -746,7 +874,16 @@ const release = new commander_1.Command("release")
|
|
|
746
874
|
if (releaseData.binaryVersionRange) {
|
|
747
875
|
console.log(` Version Range: ${releaseData.binaryVersionRange}`);
|
|
748
876
|
}
|
|
877
|
+
if (opts.minBinaryVersion) {
|
|
878
|
+
console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
|
|
879
|
+
}
|
|
749
880
|
console.log(chalk_1.default.gray("──────────────────────────────────"));
|
|
881
|
+
// Save git hash after successful release for next detection run
|
|
882
|
+
const gitStatus = (0, native_detector_1.getGitRepoStatus)();
|
|
883
|
+
if (gitStatus) {
|
|
884
|
+
configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, resolvedLabel);
|
|
885
|
+
console.log(chalk_1.default.gray(` ⓘ Saved git snapshot for next release detection`));
|
|
886
|
+
}
|
|
750
887
|
}
|
|
751
888
|
catch (error) {
|
|
752
889
|
console.error(chalk_1.default.red(`\n✗ Failed to upload release: ${error.message}`));
|
|
@@ -69,4 +69,18 @@ export declare const configStore: {
|
|
|
69
69
|
* Check if authenticated
|
|
70
70
|
*/
|
|
71
71
|
isAuthenticated(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Save the git commit hash and release label from the last successful release.
|
|
74
|
+
* Used by the native change detector on the next release run.
|
|
75
|
+
*/
|
|
76
|
+
saveLastReleaseHash(hash: string, label: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Get the git commit hash saved from the last release.
|
|
79
|
+
* Returns null if no release has been made yet.
|
|
80
|
+
*/
|
|
81
|
+
getLastReleaseHash(): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Get the release label saved from the last release (for display).
|
|
84
|
+
*/
|
|
85
|
+
getLastReleaseLabel(): string | null;
|
|
72
86
|
};
|
|
@@ -171,4 +171,30 @@ exports.configStore = {
|
|
|
171
171
|
isAuthenticated() {
|
|
172
172
|
return this.getCredentials().length > 0;
|
|
173
173
|
},
|
|
174
|
+
// ============================================
|
|
175
|
+
// Native change detection helpers
|
|
176
|
+
// ============================================
|
|
177
|
+
/**
|
|
178
|
+
* Save the git commit hash and release label from the last successful release.
|
|
179
|
+
* Used by the native change detector on the next release run.
|
|
180
|
+
*/
|
|
181
|
+
saveLastReleaseHash(hash, label) {
|
|
182
|
+
const config = readConfig();
|
|
183
|
+
config.lastReleaseHash = hash;
|
|
184
|
+
config.lastReleaseLabel = label;
|
|
185
|
+
writeConfig(config);
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Get the git commit hash saved from the last release.
|
|
189
|
+
* Returns null if no release has been made yet.
|
|
190
|
+
*/
|
|
191
|
+
getLastReleaseHash() {
|
|
192
|
+
return readConfig().lastReleaseHash || null;
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Get the release label saved from the last release (for display).
|
|
196
|
+
*/
|
|
197
|
+
getLastReleaseLabel() {
|
|
198
|
+
return readConfig().lastReleaseLabel || null;
|
|
199
|
+
},
|
|
174
200
|
};
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,13 @@ const logout_1 = __importDefault(require("./commands/logout"));
|
|
|
10
10
|
const release_1 = __importDefault(require("./commands/release"));
|
|
11
11
|
const status_1 = __importDefault(require("./commands/status"));
|
|
12
12
|
const generate_manifest_1 = __importDefault(require("./commands/generate-manifest"));
|
|
13
|
+
const init_1 = __importDefault(require("./commands/init"));
|
|
13
14
|
const program = new commander_1.Command();
|
|
14
15
|
program
|
|
15
16
|
.name("upflux")
|
|
16
17
|
.description("Upflux CLI for OTA updates")
|
|
17
18
|
.version("1.0.0");
|
|
19
|
+
program.addCommand(init_1.default);
|
|
18
20
|
program.addCommand(login_1.default);
|
|
19
21
|
program.addCommand(logout_1.default);
|
|
20
22
|
program.addCommand(release_1.default);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Change Detector
|
|
3
|
+
*
|
|
4
|
+
* Uses git diff to detect changes in native project files since the last
|
|
5
|
+
* Upflux release. Helps developers know when to set --min-binary-version.
|
|
6
|
+
*
|
|
7
|
+
* If git is not initialized or not available, detection is silently skipped.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A single detected native change
|
|
11
|
+
*/
|
|
12
|
+
export interface NativeChange {
|
|
13
|
+
category: 'android' | 'ios' | 'dependencies';
|
|
14
|
+
file: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Result of native change detection
|
|
19
|
+
*/
|
|
20
|
+
export interface DetectionResult {
|
|
21
|
+
hasNativeChanges: boolean;
|
|
22
|
+
changes: NativeChange[];
|
|
23
|
+
/** Extracted from build.gradle versionName +line — used as suggested min version */
|
|
24
|
+
suggestedMinBinaryVersion?: string;
|
|
25
|
+
/** e.g., ">=2.0.0" derived from suggestedMinBinaryVersion */
|
|
26
|
+
suggestedVersionRange?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if git is available and the CWD is a git repository.
|
|
30
|
+
*
|
|
31
|
+
* Returns the current HEAD hash if git is usable.
|
|
32
|
+
* Returns null if git is not available or the directory is not a git repo —
|
|
33
|
+
* callers should skip detection entirely in this case.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getGitRepoStatus(): {
|
|
36
|
+
currentHash: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Run git diff between savedHash and HEAD for native files.
|
|
40
|
+
* Parses added lines (+) to extract structured, meaningful changes.
|
|
41
|
+
*
|
|
42
|
+
* @param savedHash - The git commit hash from the previous release
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectNativeChanges(savedHash: string): DetectionResult;
|
|
45
|
+
/**
|
|
46
|
+
* Pretty-print the detected native changes to the terminal.
|
|
47
|
+
*/
|
|
48
|
+
export declare function displayNativeChangeWarning(result: DetectionResult): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Native Change Detector
|
|
4
|
+
*
|
|
5
|
+
* Uses git diff to detect changes in native project files since the last
|
|
6
|
+
* Upflux release. Helps developers know when to set --min-binary-version.
|
|
7
|
+
*
|
|
8
|
+
* If git is not initialized or not available, detection is silently skipped.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.getGitRepoStatus = getGitRepoStatus;
|
|
15
|
+
exports.detectNativeChanges = detectNativeChanges;
|
|
16
|
+
exports.displayNativeChangeWarning = displayNativeChangeWarning;
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
/**
|
|
20
|
+
* Known native dependency prefixes for package.json scanning
|
|
21
|
+
*/
|
|
22
|
+
const KNOWN_NATIVE_PREFIXES = [
|
|
23
|
+
'react-native-',
|
|
24
|
+
'@react-native-community/',
|
|
25
|
+
'@react-native/',
|
|
26
|
+
'expo-',
|
|
27
|
+
'@react-native-firebase/',
|
|
28
|
+
'@react-native-async-storage/',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Native files to watch via git diff
|
|
32
|
+
*/
|
|
33
|
+
const NATIVE_FILES = [
|
|
34
|
+
'android/app/build.gradle',
|
|
35
|
+
'android/app/src/main/AndroidManifest.xml',
|
|
36
|
+
'ios/Podfile',
|
|
37
|
+
'ios/Podfile.lock',
|
|
38
|
+
'package.json',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Check if git is available and the CWD is a git repository.
|
|
42
|
+
*
|
|
43
|
+
* Returns the current HEAD hash if git is usable.
|
|
44
|
+
* Returns null if git is not available or the directory is not a git repo —
|
|
45
|
+
* callers should skip detection entirely in this case.
|
|
46
|
+
*/
|
|
47
|
+
function getGitRepoStatus() {
|
|
48
|
+
try {
|
|
49
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
50
|
+
const hash = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: 'pipe',
|
|
53
|
+
}).trim();
|
|
54
|
+
return { currentHash: hash };
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
// Not a git repo or git is not installed
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run git diff between savedHash and HEAD for native files.
|
|
63
|
+
* Parses added lines (+) to extract structured, meaningful changes.
|
|
64
|
+
*
|
|
65
|
+
* @param savedHash - The git commit hash from the previous release
|
|
66
|
+
*/
|
|
67
|
+
function detectNativeChanges(savedHash) {
|
|
68
|
+
let diffOutput = '';
|
|
69
|
+
try {
|
|
70
|
+
diffOutput = (0, child_process_1.execSync)(`git diff ${savedHash}..HEAD -- ${NATIVE_FILES.join(' ')}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
// git diff failed (e.g., hash no longer valid in shallow clone) — skip
|
|
74
|
+
return { hasNativeChanges: false, changes: [] };
|
|
75
|
+
}
|
|
76
|
+
if (!diffOutput.trim()) {
|
|
77
|
+
return { hasNativeChanges: false, changes: [] };
|
|
78
|
+
}
|
|
79
|
+
const changes = [];
|
|
80
|
+
let currentFile = '';
|
|
81
|
+
let suggestedMinBinaryVersion;
|
|
82
|
+
for (const line of diffOutput.split('\n')) {
|
|
83
|
+
// Track which file we are currently diffing
|
|
84
|
+
if (line.startsWith('+++ b/')) {
|
|
85
|
+
currentFile = line.replace('+++ b/', '');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Only inspect added lines; skip diff metadata lines
|
|
89
|
+
if (!line.startsWith('+') || line.startsWith('+++'))
|
|
90
|
+
continue;
|
|
91
|
+
const added = line.slice(1);
|
|
92
|
+
// ── android/app/build.gradle ──────────────────────────────────────
|
|
93
|
+
if (currentFile === 'android/app/build.gradle') {
|
|
94
|
+
const versionMatch = added.match(/versionName\s+["']([^"']+)["']/);
|
|
95
|
+
if (versionMatch) {
|
|
96
|
+
suggestedMinBinaryVersion = versionMatch[1];
|
|
97
|
+
changes.push({
|
|
98
|
+
category: 'android',
|
|
99
|
+
file: 'build.gradle',
|
|
100
|
+
description: `versionName → ${versionMatch[1]}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const minSdkMatch = added.match(/minSdkVersion\s+(\d+)/);
|
|
104
|
+
if (minSdkMatch) {
|
|
105
|
+
changes.push({
|
|
106
|
+
category: 'android',
|
|
107
|
+
file: 'build.gradle',
|
|
108
|
+
description: `minSdkVersion → ${minSdkMatch[1]}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ── android/app/src/main/AndroidManifest.xml ──────────────────────
|
|
113
|
+
if (currentFile === 'android/app/src/main/AndroidManifest.xml') {
|
|
114
|
+
const permMatch = added.match(/uses-permission.*android:name="([^"]+)"/);
|
|
115
|
+
if (permMatch) {
|
|
116
|
+
changes.push({
|
|
117
|
+
category: 'android',
|
|
118
|
+
file: 'AndroidManifest.xml',
|
|
119
|
+
description: `New permission: ${permMatch[1]}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── ios/Podfile ────────────────────────────────────────────────────
|
|
124
|
+
if (currentFile === 'ios/Podfile') {
|
|
125
|
+
const podMatch = added.match(/^\s*pod\s+["']([^"']+)["']/);
|
|
126
|
+
if (podMatch) {
|
|
127
|
+
changes.push({
|
|
128
|
+
category: 'ios',
|
|
129
|
+
file: 'Podfile',
|
|
130
|
+
description: `New pod: ${podMatch[1]}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── ios/Podfile.lock ───────────────────────────────────────────────
|
|
135
|
+
if (currentFile === 'ios/Podfile.lock') {
|
|
136
|
+
// Any change to the lock file is significant — report only once
|
|
137
|
+
if (!changes.some((c) => c.file === 'Podfile.lock')) {
|
|
138
|
+
changes.push({
|
|
139
|
+
category: 'ios',
|
|
140
|
+
file: 'Podfile.lock',
|
|
141
|
+
description: 'Pod versions changed (Podfile.lock updated)',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── package.json ───────────────────────────────────────────────────
|
|
146
|
+
if (currentFile === 'package.json') {
|
|
147
|
+
// Match new native dependency entries on added lines
|
|
148
|
+
const depMatch = added.match(/"([^"]+)"\s*:/);
|
|
149
|
+
if (depMatch &&
|
|
150
|
+
KNOWN_NATIVE_PREFIXES.some((prefix) => depMatch[1].startsWith(prefix))) {
|
|
151
|
+
changes.push({
|
|
152
|
+
category: 'dependencies',
|
|
153
|
+
file: 'package.json',
|
|
154
|
+
description: `New native dependency: ${depMatch[1]}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
hasNativeChanges: changes.length > 0,
|
|
161
|
+
changes,
|
|
162
|
+
suggestedMinBinaryVersion,
|
|
163
|
+
suggestedVersionRange: suggestedMinBinaryVersion
|
|
164
|
+
? `>=${suggestedMinBinaryVersion}`
|
|
165
|
+
: undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Pretty-print the detected native changes to the terminal.
|
|
170
|
+
*/
|
|
171
|
+
function displayNativeChangeWarning(result) {
|
|
172
|
+
var _a;
|
|
173
|
+
console.log(chalk_1.default.yellow('\n⚠️ Native changes detected since last release:\n'));
|
|
174
|
+
// Group by category for cleaner output
|
|
175
|
+
const byCategory = {};
|
|
176
|
+
for (const change of result.changes) {
|
|
177
|
+
(byCategory[_a = change.category] || (byCategory[_a] = [])).push(change);
|
|
178
|
+
}
|
|
179
|
+
for (const [category, items] of Object.entries(byCategory)) {
|
|
180
|
+
const label = category.charAt(0).toUpperCase() + category.slice(1);
|
|
181
|
+
console.log(chalk_1.default.bold(` ${label}:`));
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
console.log(chalk_1.default.yellow(` • ${item.file}: ${item.description}`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
if (result.suggestedMinBinaryVersion) {
|
|
188
|
+
console.log(chalk_1.default.cyan(' 💡 These changes likely require a new binary from the app store.'));
|
|
189
|
+
console.log(chalk_1.default.cyan(' Suggested flags:'));
|
|
190
|
+
console.log(chalk_1.default.cyan(` --min-binary-version ${result.suggestedMinBinaryVersion}`));
|
|
191
|
+
console.log(chalk_1.default.cyan(` --version-range "${result.suggestedVersionRange}"`));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(chalk_1.default.cyan(' 💡 Consider setting --min-binary-version and --version-range'));
|
|
195
|
+
console.log(chalk_1.default.cyan(' if these changes require a new binary from the app store.'));
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
}
|