@upfluxhq/cli 1.0.1 → 1.1.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
@@ -100,6 +100,8 @@ upflux release [options]
100
100
  | `-m, --mandatory` | | Mark update as mandatory |
101
101
  | `-r, --rollout <number>` | | Rollout percentage (0-100, default: 100) |
102
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) |
103
105
  | `--dev` | | Create development bundle |
104
106
  | `--skip-bundle` | | Skip bundling, use existing files in output dir |
105
107
  | `--schedule` | | Create as draft, set schedule time in dashboard |
@@ -168,6 +170,63 @@ upflux release --label v1.0.0 --capacitor --web-dir dist
168
170
 
169
171
  ---
170
172
 
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
178
+
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`.
182
+
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
196
+
197
+ ```
198
+ ⚠️ Native changes detected since last release:
199
+
200
+ Android:
201
+ • build.gradle: versionName → 2.0.0
202
+ • AndroidManifest.xml: New permission: android.permission.CAMERA
203
+
204
+ 💡 Suggested flags:
205
+ --min-binary-version 2.0.0
206
+ --version-range ">=2.0.0"
207
+
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
+ ```
213
+
214
+ ### Setting `--min-binary-version` Manually
215
+
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
+ ```
220
+
221
+ ### Skipping in CI/CD
222
+
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
+ ```
227
+
228
+ ---
229
+
171
230
  ### `status`
172
231
 
173
232
  Check deployment status and recent releases.
@@ -290,7 +349,9 @@ The CLI stores credentials in `.upflux` in the current directory:
290
349
  "deploymentName": "staging"
291
350
  }
292
351
  ],
293
- "activeIndex": 0
352
+ "activeIndex": 0,
353
+ "lastReleaseHash": "a3f8c12d...",
354
+ "lastReleaseLabel": "v1.5.0"
294
355
  }
295
356
  ```
296
357
 
@@ -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
  */
@@ -325,9 +326,11 @@ const release = new commander_1.Command("release")
325
326
  .option("-m, --mandatory", "Mark update as mandatory", false)
326
327
  .option("-r, --rollout <number>", "Rollout percentage (0-100)", "100")
327
328
  .option("-v, --version-range <range>", "Binary version range (e.g., >=1.0.0)")
329
+ .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
330
  .option("--dev", "Create development bundle", false)
329
331
  .option("--schedule", "Create as draft, set schedule time in dashboard")
330
332
  .option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
333
+ .option("--skip-native-check", "Skip automatic native change detection (useful in CI/CD)", false)
331
334
  .option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
332
335
  .option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
333
336
  .action(async (opts) => {
@@ -619,6 +622,80 @@ const release = new commander_1.Command("release")
619
622
  }
620
623
  }
621
624
  // ========================================
625
+ // NATIVE CHANGE DETECTION PHASE
626
+ // ========================================
627
+ if (!opts.skipNativeCheck) {
628
+ const gitStatus = (0, native_detector_1.getGitRepoStatus)();
629
+ if (!gitStatus) {
630
+ // Not a git repo or git not installed — skip silently
631
+ console.log(chalk_1.default.gray(" ⓘ Git not detected — skipping native change check"));
632
+ }
633
+ else {
634
+ const lastHash = configStore_1.configStore.getLastReleaseHash();
635
+ const lastLabel = configStore_1.configStore.getLastReleaseLabel();
636
+ if (!lastHash) {
637
+ // First release — nothing to compare against
638
+ console.log(chalk_1.default.gray(" ⓘ First release — native change detection will run from next release"));
639
+ }
640
+ else {
641
+ console.log(chalk_1.default.gray(` 🔍 Checking native changes since ${lastLabel || lastHash.slice(0, 7)}...`));
642
+ const detection = (0, native_detector_1.detectNativeChanges)(lastHash);
643
+ if (detection.hasNativeChanges) {
644
+ (0, native_detector_1.displayNativeChangeWarning)(detection);
645
+ // Only prompt if --min-binary-version not already provided
646
+ if (!opts.minBinaryVersion) {
647
+ const choices = [
648
+ ...(detection.suggestedMinBinaryVersion
649
+ ? [{
650
+ name: `Auto-apply suggested flags (--min-binary-version ${detection.suggestedMinBinaryVersion} --version-range "${detection.suggestedVersionRange}")`,
651
+ value: 'auto',
652
+ }]
653
+ : []),
654
+ { name: 'Enter min-binary-version manually', value: 'custom' },
655
+ { name: 'Continue without (not recommended for native changes)', value: 'skip' },
656
+ ];
657
+ const { action } = await inquirer_1.default.prompt([{
658
+ type: 'list',
659
+ name: 'action',
660
+ message: 'How would you like to handle these native changes?',
661
+ choices,
662
+ }]);
663
+ if (action === 'auto') {
664
+ opts.minBinaryVersion = detection.suggestedMinBinaryVersion;
665
+ if (!opts.versionRange && detection.suggestedVersionRange) {
666
+ opts.versionRange = detection.suggestedVersionRange;
667
+ }
668
+ console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
669
+ console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
670
+ }
671
+ else if (action === 'custom') {
672
+ const { version } = await inquirer_1.default.prompt([{
673
+ type: 'input',
674
+ name: 'version',
675
+ message: 'Enter minimum binary version (e.g., 2.0.0):',
676
+ default: detection.suggestedMinBinaryVersion,
677
+ validate: (v) => /^\d+\.\d+\.\d+/.test(v) || 'Enter a valid semver version (e.g., 2.0.0)',
678
+ }]);
679
+ opts.minBinaryVersion = version;
680
+ if (!opts.versionRange) {
681
+ opts.versionRange = `>=${version}`;
682
+ }
683
+ console.log(chalk_1.default.green(` ✓ --min-binary-version ${opts.minBinaryVersion}`));
684
+ console.log(chalk_1.default.green(` ✓ --version-range ${opts.versionRange}`));
685
+ }
686
+ // 'skip' — continue without setting flags
687
+ }
688
+ else {
689
+ console.log(chalk_1.default.gray(` ⓘ --min-binary-version already set to ${opts.minBinaryVersion}`));
690
+ }
691
+ }
692
+ else {
693
+ console.log(chalk_1.default.green(' ✓ No native changes detected'));
694
+ }
695
+ }
696
+ }
697
+ }
698
+ // ========================================
622
699
  // UPLOAD PHASE
623
700
  // ========================================
624
701
  console.log(chalk_1.default.blue("\n⬆ Uploading release..."));
@@ -637,6 +714,9 @@ const release = new commander_1.Command("release")
637
714
  if (opts.versionRange) {
638
715
  console.log(` Version Range: ${opts.versionRange}`);
639
716
  }
717
+ if (opts.minBinaryVersion) {
718
+ console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
719
+ }
640
720
  // ========================================
641
721
  // PRESIGNED URL UPLOAD FLOW
642
722
  // ========================================
@@ -701,18 +781,9 @@ const release = new commander_1.Command("release")
701
781
  }
702
782
  }
703
783
  // 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,
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,
712
785
  // Include bundle sizes
713
- iosBundleSize: bundleSizes.ios,
714
- androidBundleSize: bundleSizes.android || bundleSizes.single,
715
- }, {
786
+ iosBundleSize: bundleSizes.ios, androidBundleSize: bundleSizes.android || bundleSizes.single }), {
716
787
  headers: {
717
788
  "x-publish-key": authKey,
718
789
  "Content-Type": "application/json",
@@ -746,7 +817,16 @@ const release = new commander_1.Command("release")
746
817
  if (releaseData.binaryVersionRange) {
747
818
  console.log(` Version Range: ${releaseData.binaryVersionRange}`);
748
819
  }
820
+ if (opts.minBinaryVersion) {
821
+ console.log(` Min Binary Version: ${chalk_1.default.cyan(opts.minBinaryVersion)}`);
822
+ }
749
823
  console.log(chalk_1.default.gray("──────────────────────────────────"));
824
+ // Save git hash after successful release for next detection run
825
+ const gitStatus = (0, native_detector_1.getGitRepoStatus)();
826
+ if (gitStatus) {
827
+ configStore_1.configStore.saveLastReleaseHash(gitStatus.currentHash, opts.label);
828
+ console.log(chalk_1.default.gray(` ⓘ Saved git snapshot for next release detection`));
829
+ }
750
830
  }
751
831
  catch (error) {
752
832
  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
  };
@@ -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.1",
3
+ "version": "1.1.0",
4
4
  "description": "Upflux CLI for managing OTA updates",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",