@upfluxhq/cli 1.1.0 → 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;
@@ -135,11 +135,41 @@ function isValidAppId(appId) {
135
135
  return /^[a-zA-Z0-9-_]+$/.test(appId) && appId.length >= 3;
136
136
  }
137
137
  /**
138
- * Validate release label format (semver-like)
138
+ * Validate release label format (semver-like, allows + build metadata)
139
139
  */
140
140
  function isValidReleaseLabel(label) {
141
- // Allow v1.0.0, 1.0.0, v1.0.0-beta, etc.
142
- 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}`;
143
173
  }
144
174
  /**
145
175
  * Validate version range format
@@ -256,28 +286,25 @@ function collectAssets(dir) {
256
286
  return assets;
257
287
  }
258
288
  /**
259
- * Write upflux.json config file to the bundle output directory
289
+ * Write upflux.json config file to the bundle output directory.
260
290
  *
261
291
  * This file embeds the OTA version into the app bundle at build time.
262
- * The SDK reads this on fresh install to know which version the app ships with,
263
- * preventing unnecessary OTA downloads when the embedded bundle is already latest.
264
- *
265
- * @param outputDir - Directory to write upflux.json to
266
- * @param releaseLabel - The OTA version (e.g., "v1.0.0")
267
- * @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.
268
294
  */
269
- function writeUpfluxConfig(outputDir, releaseLabel, appId) {
295
+ function writeUpfluxConfig(outputDir, releaseLabel, appId, buildNumber) {
270
296
  const config = {
271
297
  releaseLabel,
272
298
  appId,
273
299
  createdAt: new Date().toISOString(),
274
300
  };
301
+ if (buildNumber !== undefined) {
302
+ config.buildNumber = buildNumber;
303
+ }
275
304
  const configContent = JSON.stringify(config, null, 2);
276
305
  const configPath = path.join(outputDir, "upflux.json");
277
306
  fs.writeFileSync(configPath, configContent);
278
- console.log(chalk_1.default.gray(` Created upflux.json with version: ${releaseLabel}`));
279
- // Also copy to native asset directories so the SDK can read the
280
- // 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}` : ''})`));
281
308
  copyUpfluxConfigToNativeAssets(configContent);
282
309
  }
283
310
  /**
@@ -317,7 +344,7 @@ const release = new commander_1.Command("release")
317
344
  .description("Bundle and upload a new release to Upflux")
318
345
  .option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
319
346
  .option("-d, --deployment <deploymentId>", "Deployment ID (uses stored value if not provided)")
320
- .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")
321
348
  .option("--platform <platform>", "Target platform for the release: all, ios, or android (defaults to all)", "all")
322
349
  .option("-e, --entry-file <path>", "Entry file for bundler", DEFAULT_ENTRY_FILE)
323
350
  .option("-o, --output-dir <path>", "Output directory for bundle", DEFAULT_OUTPUT_DIR)
@@ -405,11 +432,41 @@ const release = new commander_1.Command("release")
405
432
  console.error(chalk_1.default.gray("Deployment ID should be a 24-character MongoDB ObjectId"));
406
433
  process.exit(1);
407
434
  }
408
- // Validation 5: Release label format
409
- if (!isValidReleaseLabel(opts.label)) {
410
- console.error(chalk_1.default.red("✗ Invalid release label format"));
411
- console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, etc."));
412
- 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)`));
413
470
  }
414
471
  // Validation 6: Rollout percentage
415
472
  const rolloutValidation = isValidRollout(opts.rollout);
@@ -467,10 +524,10 @@ const release = new commander_1.Command("release")
467
524
  }
468
525
  // Write upflux.json to web directory BEFORE zipping
469
526
  // This embeds the OTA version into the Capacitor app bundle
470
- writeUpfluxConfig(webDirPath, opts.label, appId);
527
+ writeUpfluxConfig(webDirPath, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
471
528
  // Zip the web directory
472
529
  console.log(chalk_1.default.blue(`📦 Zipping ${webDir}/ directory...`));
473
- const zipPath = path.join(outputDir, `bundle-${opts.label}.zip`);
530
+ const zipPath = path.join(outputDir, `bundle-${resolvedLabel}.zip`);
474
531
  bundlePath = await zipDirectory(webDirPath, zipPath);
475
532
  console.log(chalk_1.default.green(`✓ Bundle created: ${bundlePath}`));
476
533
  // No separate assets for Capacitor - all in zip
@@ -572,8 +629,8 @@ const release = new commander_1.Command("release")
572
629
  : collectAssets(androidAssetsPath);
573
630
  // Write upflux.json to BOTH platform output directories
574
631
  // React Native reads this from bundled assets to know embedded OTA version
575
- writeUpfluxConfig(iosOutputDir, opts.label, appId);
576
- 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);
577
634
  // Set bundlePath and assetFiles for display (use iOS as primary for logging)
578
635
  bundlePath = iosBundlePath;
579
636
  assetFiles = [...iosAssetFiles, ...androidAssetFiles];
@@ -618,7 +675,7 @@ const release = new commander_1.Command("release")
618
675
  }
619
676
  // Write upflux.json to output directory
620
677
  // React Native reads this from bundled assets to know embedded OTA version
621
- writeUpfluxConfig(outputDir, opts.label, appId);
678
+ writeUpfluxConfig(outputDir, resolvedLabel, appId, rootJson === null || rootJson === void 0 ? void 0 : rootJson.app.buildNumber);
622
679
  }
623
680
  }
624
681
  // ========================================
@@ -702,7 +759,7 @@ const release = new commander_1.Command("release")
702
759
  console.log(` App: ${appId}`);
703
760
  console.log(` Deployment: ${deploymentId}`);
704
761
  console.log(` Deployment Name: ${deploymentName}`);
705
- console.log(` Label: ${opts.label}`);
762
+ console.log(` Label: ${resolvedLabel}`);
706
763
  console.log(` Platform: ${platform}`);
707
764
  console.log(` Bundle: ${bundlePath}`);
708
765
  console.log(` Assets: ${assetFiles.length} file(s)`);
@@ -734,7 +791,7 @@ const release = new commander_1.Command("release")
734
791
  const initResponse = await apiClient_1.apiClient.post("/cli/releases/upload/init", {
735
792
  appId,
736
793
  deploymentId,
737
- releaseLabel: opts.label,
794
+ releaseLabel: resolvedLabel,
738
795
  platform,
739
796
  files: fileManifest.map(f => ({
740
797
  type: f.type,
@@ -781,7 +838,7 @@ const release = new commander_1.Command("release")
781
838
  }
782
839
  }
783
840
  // Step 3: Complete upload and create release record
784
- const completeResponse = await apiClient_1.apiClient.post("/cli/releases/upload/complete", Object.assign(Object.assign({ uploadId: initResponse.data.uploadId, releaseLabel: opts.label, mandatory: opts.mandatory, rolloutPercentage: rolloutValidation.value, binaryVersionRange: opts.versionRange }, (opts.minBinaryVersion ? { minBinaryVersion: opts.minBinaryVersion } : {})), { platform, 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,
785
842
  // Include bundle sizes
786
843
  iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), {
787
844
  headers: {
@@ -824,7 +881,7 @@ const release = new commander_1.Command("release")
824
881
  // Save git hash after successful release for next detection run
825
882
  const gitStatus = (0, native_detector_1.getGitRepoStatus)();
826
883
  if (gitStatus) {
827
- configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, opts.label);
884
+ configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, resolvedLabel);
828
885
  console.log(chalk_1.default.gray(` ⓘ Saved git snapshot for next release detection`));
829
886
  }
830
887
  }
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upfluxhq/cli",
3
- "version": "1.1.0",
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",