@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 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. Authenticate with your publish keys (supports multiple!)
17
- upflux login --key <staging-publish-key>
18
- upflux login --key <production-publish-key>
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
- # 2. Upload a release (will prompt if multiple deployments)
21
- upflux release --label v1.0.0 --mandatory
24
+ # 3. Publish a release (auto-increments buildNumber)
25
+ upflux release
22
26
 
23
- # 3. Check release status
27
+ # 4. Check deployment status
24
28
  upflux status
25
29
  ```
26
30
 
31
+ ---
32
+
27
33
  ## Commands
28
34
 
29
- ### `login`
35
+ ### `init`
30
36
 
31
- Authenticate with the Upflux server. **Supports multiple credentials** - each login adds a new deployment.
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 login --key <publishKey>
40
+ upflux init [options]
35
41
  ```
36
42
 
37
43
  | Option | Description |
38
44
  |--------|-------------|
39
- | `-k, --key <publishKey>` | Publish key for authentication |
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
- **Example output with multiple logins:**
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 to remove one
95
+ # Interactive selection:
70
96
  upflux logout
71
97
 
72
- # Remove all credentials
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. **Prompts to select deployment** if multiple are stored.
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**: Uses `react-native bundle` command
84
- - **Capacitor**: Zips the web directory (detected via `capacitor.config.*`)
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 | Required | Description |
91
- |--------|----------|-------------|
92
- | `-l, --label <label>` | ✓ | Release label (e.g., v1.0.0) |
93
- | `--platform <platform>` | | Target platform: `all`, `ios`, or `android` (defaults to `all`) |
94
- | `-a, --app <appId>` | | Application ID (auto-detected from credentials) |
95
- | `-d, --deployment <deploymentId>` | | Deployment ID (prompts if multiple) |
96
- | `-e, --entry-file <path>` | | Entry file for bundler (default: `index.js`) |
97
- | `-o, --output-dir <path>` | | Output directory for bundle (default: `./dist`) |
98
- | `-b, --bundle <path>` | | Skip bundling, use existing bundle file |
99
- | `--assets <paths...>` | | Skip auto-detection, use these asset files |
100
- | `-m, --mandatory` | | Mark update as mandatory |
101
- | `-r, --rollout <number>` | | Rollout percentage (0-100, default: 100) |
102
- | `-v, --version-range <range>` | | Binary version range (e.g., >=1.0.0) |
103
- | `--min-binary-version <version>` | | Minimum native binary version required. Devices below this are directed to update from the App Store / Play Store before receiving this OTA. |
104
- | `--skip-native-check` | | Skip automatic native change detection (useful in CI/CD) |
105
- | `--dev` | | Create development bundle |
106
- | `--skip-bundle` | | Skip bundling, use existing files in output dir |
107
- | `--schedule` | | Create as draft, set schedule time in dashboard |
108
- | `--capacitor` | | Force Capacitor mode (auto-detected if capacitor.config exists) |
109
- | `--web-dir <path>` | | Web directory to bundle for Capacitor (default: from config or www) |
110
-
111
- **Examples:**
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
- # Release to selected deployment (prompts if multiple)
115
- upflux release --label v1.2.0
140
+ # Recommended: Auto-increment buildNumber from upflux.json (e.g. 1.0.0+1 -> 1.0.0+2)
141
+ upflux release
116
142
 
117
- # iOS-only hotfix
118
- upflux release --label v1.2.1-ios-fix --platform ios
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 update for all platforms
124
- upflux release --label v1.3.0 --mandatory
146
+ # Mandatory release with gradual 25% rollout
147
+ upflux release --mandatory --rollout 25
125
148
 
126
- # Schedule release (draft mode)
127
- upflux release --label v1.4.0 --schedule
149
+ # iOS-only emergency hotfix
150
+ upflux release --platform ios
128
151
 
129
- # Capacitor project with custom web directory
130
- upflux release --label v1.0.0 --capacitor --web-dir dist
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
- ## Native Change Detection
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
- 1. After each successful release, the CLI saves the current **git commit hash** to `.upflux`.
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
- > **Note:** Detection requires git. If the project is not a git repo, detection is silently skipped.
184
-
185
- ### Files Watched
186
-
187
- | File | What is detected |
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
- Android:
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
- 💡 Suggested flags:
205
- --min-binary-version 2.0.0
206
- --version-range ">=2.0.0"
182
+ ---
207
183
 
208
- ? How would you like to handle these native changes?
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
- ### Setting `--min-binary-version` Manually
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
- ```bash
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
- ### Skipping in CI/CD
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
- ```bash
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
- Check deployment status and recent releases.
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 show (default: 5) |
212
+ | `-n, --limit <number>` | Number of recent releases to display (default: 5) |
241
213
 
242
- **Example output:**
214
+ **Example:**
243
215
  ```
244
216
  ⚡ Fetching deployment status...
245
217
 
246
218
  ╭────────────────────────────────────────────────╮
247
219
  │ 📱 app-970384d3 → production │
248
220
  │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
249
- │ Current Release: v1.2.0 (iOS & Android) │
221
+ │ Current Release: 1.0.0+4 (iOS & Android) │
250
222
  │ Status: ● active • 100% rollout │
251
- │ Published: 2h ago │
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
- Key format: `upflux_pub_<prefix>_<secret>`
254
+ Stored credentials and release metadata are saved in `.upflux` in the current project directory.
367
255
 
368
- Example: `upflux_pub_prod_a1b2c3d4e5f6...`
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,3 @@
1
+ import { Command } from "commander";
2
+ declare const init: Command;
3
+ export default init;
@@ -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;
@@ -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
- * 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
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 with version: ${releaseLabel}`));
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
- .requiredOption("-l, --label <label>", "Release label (e.g., v1.0.0)")
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
- // Validation 5: Release label format
406
- if (!isValidReleaseLabel(opts.label)) {
407
- console.error(chalk_1.default.red("✗ Invalid release label format"));
408
- console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, etc."));
409
- process.exit(1);
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, opts.label, appId);
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-${opts.label}.zip`);
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, opts.label, appId);
573
- writeUpfluxConfig(androidOutputDir, opts.label, appId);
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, opts.label, appId);
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: ${opts.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: opts.label,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upfluxhq/cli",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Upflux CLI for managing OTA updates",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",