@siranjeevan/releaseflow 1.0.0 → 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/MANUAL.md +71 -0
- package/README.md +68 -0
- package/firebase.js +28 -3
- package/index.js +237 -37
- package/package.json +1 -1
- package/test_project/dummy.json +1 -0
package/MANUAL.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 📖 ReleaseFlow Official User Manual
|
|
2
|
+
|
|
3
|
+
Welcome to **ReleaseFlow**, the ultimate automation tool for Flutter developers. This guide will help you master the release process.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🛠️ 1. Setup Phase
|
|
8
|
+
Before pushing your first update, you must link your project to Firebase. Use the `config` subcommand to manage settings.
|
|
9
|
+
|
|
10
|
+
### 📝 Configuration Actions:
|
|
11
|
+
- **`releaseflow config set`**: Interactive menu to configure the current directory or select a subfolder.
|
|
12
|
+
- **`releaseflow config edit`**: Allows updating the Service Account or Bucket name with pre-filled defaults.
|
|
13
|
+
- **`releaseflow config show`**: Displays the path of the current `releaseflow.config.json` and its settings.
|
|
14
|
+
- **`releaseflow config remove`**: Deletes the `releaseflow.config.json` file for the current project.
|
|
15
|
+
|
|
16
|
+
### 🔑 What you need:
|
|
17
|
+
- **Firebase Console**: Go to **Project Settings > Service Accounts > Select Node.js**.
|
|
18
|
+
- **Service Account**: Generate and download your Private Key JSON file.
|
|
19
|
+
- **Storage Bucket**: Copy the bucket name without `gs://` (from Storage > Files).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🚀 2. Releasing an Update
|
|
24
|
+
The `release` command is your main workspace tool. It handles EVERYTHING in one go.
|
|
25
|
+
|
|
26
|
+
- **Run**: `releaseflow release`
|
|
27
|
+
- **What happens**:
|
|
28
|
+
1. **Version Check**: Compares `pubspec.yaml` with Firebase versions.
|
|
29
|
+
2. **Smart Suggestion**: If the version already exists, it suggests a patch bump.
|
|
30
|
+
3. **Automated Build**: Executes `flutter build apk --split-per-abi --target-platform android-arm64`.
|
|
31
|
+
4. **Optimized Upload**: Pushes the ~15MB APK to Firebase Storage.
|
|
32
|
+
5. **Cloud Update**: Updates Firestore so your users instantly see the new version.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ⏪ 3. Rolling Back
|
|
37
|
+
If you find a mistake in your latest version, you can go back quickly.
|
|
38
|
+
|
|
39
|
+
- **Run**: `releaseflow rollback`
|
|
40
|
+
- **What happens**:
|
|
41
|
+
- Displays a list of your most recent uploaded versions.
|
|
42
|
+
- Selecting one instantly points the "Live Version" to that stable build.
|
|
43
|
+
- **Storage Management**: The CLI automatically keeps only the 3 latest versions in your Firebase Storage to save space.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📱 4. Flutter Integration Checklist
|
|
48
|
+
To make the "Force Update" work, your app needs to talk to Firestore.
|
|
49
|
+
|
|
50
|
+
### 🔗 Next Steps for your App:
|
|
51
|
+
1. **Manual UI**: Build a "Force Update" screen or dialog in your Flutter code.
|
|
52
|
+
2. **Firestore**: The CLI manages the `app_config/version` document for you.
|
|
53
|
+
3. **Security**: Set Firestore rules to allow your app to READ `app_config/version`.
|
|
54
|
+
4. **Logic**: Use `package_info_plus` to compare your app version with Firestore's `latest_version`.
|
|
55
|
+
|
|
56
|
+
### ✅ Verification Checklist:
|
|
57
|
+
- **a.** Run `releaseflow release` and set "Force Update" to TRUE.
|
|
58
|
+
- **b.** Open your old app version: It should now show your Force Update screen.
|
|
59
|
+
- **c.** Run `releaseflow rollback`: Your app should return to normal.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🆘 5. Help Center
|
|
64
|
+
If you are stuck or encounter an error, use these resources:
|
|
65
|
+
- **`releaseflow prompt`**: See your integration guide instantly.
|
|
66
|
+
- **`releaseflow --help`**: View quick usage commands.
|
|
67
|
+
- **GitHub**: https://github.com/siranjeevan/releaseflow-cli
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
© 2026 ReleaseFlow | Designed for Professional Flutter Developers 🚀
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 🚀 ReleaseFlow CLI
|
|
2
|
+
|
|
3
|
+
**The Professional Flutter Release Automator.**
|
|
4
|
+
Effortlessly build, version, and deploy Flutter APKs to Firebase with a single command. Designed for developers who value speed, consistency, and a clean release history.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 📦 Installation
|
|
9
|
+
|
|
10
|
+
Install globally via NPM to access the `releaseflow` command anywhere:
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @siranjeevan/releaseflow
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🎮 Command Overview
|
|
18
|
+
|
|
19
|
+
| Command | Description |
|
|
20
|
+
| :--- | :--- |
|
|
21
|
+
| `releaseflow release` | **All-in-One**: Builds optimized APK, increments version, and uploads to Firebase. |
|
|
22
|
+
| `releaseflow rollback`| **Safety Net**: Instantly revert the live app to a previous stable version. |
|
|
23
|
+
| `releaseflow config set` | **Setup**: Link a folder/project to its Firebase credentials. |
|
|
24
|
+
| `releaseflow prompt` | **Guide**: Show the Flutter integration checklist and next steps. |
|
|
25
|
+
| `releaseflow manual` | **Full Help**: Open the detailed user manual in your terminal. |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ✨ Why ReleaseFlow?
|
|
30
|
+
|
|
31
|
+
* **Project-Aware**: Automatically detects and uses unique Firebase settings for each of your apps.
|
|
32
|
+
* **Smart Versioning**: Reads/increments `pubspec.yaml` and prevents duplicate version releases.
|
|
33
|
+
* **Optimized Builds**: Automatically builds `arm64` APKs to keep your file size small (< 15MB).
|
|
34
|
+
* **Auto-Cleanup**: Keeps your Firebase Storage tidy by only maintaining the 3 most recent APKs.
|
|
35
|
+
* **Force Update Support**: Toggle mandatory updates for any version directly from the CLI.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Quick Start (3 Steps)
|
|
40
|
+
|
|
41
|
+
### 1. Link Your Project
|
|
42
|
+
Go to your Flutter project root and run:
|
|
43
|
+
```bash
|
|
44
|
+
releaseflow config set
|
|
45
|
+
```
|
|
46
|
+
Follow the prompts to select your project folder and provide your Firebase Service Account JSON.
|
|
47
|
+
|
|
48
|
+
### 2. Prepare Your Flutter App
|
|
49
|
+
Ensure your app is set up to read from your Firestore `app_config/version` document. Run `releaseflow prompt` to see the code requirements.
|
|
50
|
+
|
|
51
|
+
### 3. Launch Your Release
|
|
52
|
+
When you're ready to deploy, just run:
|
|
53
|
+
```bash
|
|
54
|
+
releaseflow release
|
|
55
|
+
```
|
|
56
|
+
The CLI will build, version, and upload your APK. Your app is now LIVE!
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🛠️ Requirements
|
|
61
|
+
- **Flutter SDK**: Must be installed and available in your PATH.
|
|
62
|
+
- **Node.js**: Version 16 or higher.
|
|
63
|
+
- **Firebase Project**: A project with Firestore and Storage enabled.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
Created with ❤️ by [Jeevith](https://github.com/siranjeevan)
|
|
68
|
+
[GitHub Repository](https://github.com/siranjeevan/releaseflow-cli) | [Report a Bug](https://github.com/siranjeevan/releaseflow-cli/issues)
|
package/firebase.js
CHANGED
|
@@ -4,9 +4,33 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
let db, storage;
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const GLOBAL_CONFIG_PATH = path.join(__dirname, 'config.json');
|
|
8
|
+
|
|
9
|
+
function getConfigPath() {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
|
|
12
|
+
// 1. Current Directory
|
|
13
|
+
const p1 = path.join(cwd, 'releaseflow.config.json');
|
|
14
|
+
// 2. flutter_app folder (if in project root)
|
|
15
|
+
const p2 = path.join(cwd, 'flutter_app', 'releaseflow.config.json');
|
|
16
|
+
// 3. Parent Directory (if inside flutter_app/lib or similar)
|
|
17
|
+
const p3 = path.join(cwd, '..', 'releaseflow.config.json');
|
|
18
|
+
|
|
19
|
+
if (fs.existsSync(p1)) return p1;
|
|
20
|
+
if (fs.existsSync(p2)) return p2;
|
|
21
|
+
if (fs.existsSync(p3)) return p3;
|
|
22
|
+
|
|
23
|
+
// Fallback to absolute legacy path if it exists
|
|
24
|
+
if (fs.existsSync(GLOBAL_CONFIG_PATH)) return GLOBAL_CONFIG_PATH;
|
|
25
|
+
|
|
26
|
+
// Default to project root for NEW configurations
|
|
27
|
+
return p1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CONFIG_PATH = getConfigPath();
|
|
8
31
|
|
|
9
32
|
function isConfigured() {
|
|
33
|
+
const CONFIG_PATH = getConfigPath();
|
|
10
34
|
if (!fs.existsSync(CONFIG_PATH)) return false;
|
|
11
35
|
try {
|
|
12
36
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
@@ -18,9 +42,10 @@ function isConfigured() {
|
|
|
18
42
|
|
|
19
43
|
function initFirebase() {
|
|
20
44
|
if (!isConfigured()) {
|
|
21
|
-
throw new Error('Firebase is not configured. Run "
|
|
45
|
+
throw new Error('Firebase is not configured. Run "releaseflow configure" first.');
|
|
22
46
|
}
|
|
23
47
|
|
|
48
|
+
const CONFIG_PATH = getConfigPath();
|
|
24
49
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
25
50
|
const serviceAccount = require(path.resolve(config.serviceAccountPath));
|
|
26
51
|
|
|
@@ -43,4 +68,4 @@ async function getLiveVersion() {
|
|
|
43
68
|
return doc.exists ? doc.data().latest_version : '0.0.0';
|
|
44
69
|
}
|
|
45
70
|
|
|
46
|
-
module.exports = { initFirebase, isConfigured,
|
|
71
|
+
module.exports = { initFirebase, isConfigured, getConfigPath, getLiveVersion, GLOBAL_CONFIG_PATH };
|
package/index.js
CHANGED
|
@@ -12,15 +12,29 @@ const {
|
|
|
12
12
|
listAvailableVersions,
|
|
13
13
|
rollbackToVersion
|
|
14
14
|
} = require('./uploader');
|
|
15
|
-
const { isConfigured,
|
|
15
|
+
const { isConfigured, getConfigPath, getLiveVersion, GLOBAL_CONFIG_PATH } = require('./firebase');
|
|
16
16
|
|
|
17
17
|
const yargs = require('yargs/yargs');
|
|
18
18
|
const { hideBin } = require('yargs/helpers');
|
|
19
|
+
const axios = require('axios');
|
|
20
|
+
const pkg = require('./package.json');
|
|
21
|
+
|
|
22
|
+
async function checkForUpdates() {
|
|
23
|
+
try {
|
|
24
|
+
const response = await axios.get(`https://registry.npmjs.org/${pkg.name}/latest`, { timeout: 1000 });
|
|
25
|
+
const latestVersion = response.data.version;
|
|
26
|
+
if (semver.gt(latestVersion, pkg.version)) {
|
|
27
|
+
console.log('\x1b[33m%s\x1b[0m', `\n--- UPDATE AVAILABLE: ${latestVersion} ---`);
|
|
28
|
+
console.log(`Run 'npm install -g ${pkg.name}' to update.\n`);
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
async function main() {
|
|
34
|
+
await checkForUpdates();
|
|
21
35
|
yargs(hideBin(process.argv))
|
|
22
36
|
.command('release', 'Build, Detect and Upload in ONE command!', {}, async () => {
|
|
23
|
-
console.log('\x1b[36m%s\x1b[0m', '\n
|
|
37
|
+
console.log('\x1b[36m%s\x1b[0m', '\n--- ReleaseFlow All-in-One Generator ---');
|
|
24
38
|
if (!isConfigured()) {
|
|
25
39
|
console.log('\x1b[33mFirebase is not configured yet.\x1b[0m');
|
|
26
40
|
await runConfigureFlow();
|
|
@@ -28,55 +42,187 @@ async function main() {
|
|
|
28
42
|
await runFullReleaseFlow();
|
|
29
43
|
})
|
|
30
44
|
.command('rollback', 'Rollback to a previous APK version', {}, async () => {
|
|
31
|
-
console.log('\x1b[36m%s\x1b[0m', '\n
|
|
45
|
+
console.log('\x1b[36m%s\x1b[0m', '\n--- Rollback to Previous Version ---');
|
|
32
46
|
if (!isConfigured()) {
|
|
33
47
|
await runConfigureFlow();
|
|
34
48
|
}
|
|
35
49
|
await runRollbackFlow();
|
|
36
50
|
})
|
|
37
|
-
.command('
|
|
51
|
+
.command('config <action>', 'Manage Firebase credentials (set, remove, edit, show)', (yargs) => {
|
|
52
|
+
return yargs.positional('action', {
|
|
53
|
+
describe: 'Action to perform',
|
|
54
|
+
choices: ['set', 'remove', 'edit', 'show']
|
|
55
|
+
});
|
|
56
|
+
}, async (argv) => {
|
|
57
|
+
switch (argv.action) {
|
|
58
|
+
case 'set':
|
|
59
|
+
case 'edit':
|
|
60
|
+
console.log('\x1b[36m%s\x1b[0m', `\n--- ${argv.action === 'edit' ? 'Edit' : 'Configure'} Firebase Credentials ---`);
|
|
61
|
+
await runConfigureFlow();
|
|
62
|
+
runPromptFlow();
|
|
63
|
+
break;
|
|
64
|
+
case 'remove':
|
|
65
|
+
await runRemoveConfig();
|
|
66
|
+
break;
|
|
67
|
+
case 'show':
|
|
68
|
+
runShowConfig();
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.command('configure', 'Alias for config set', {}, async () => {
|
|
38
73
|
console.log('\x1b[36m%s\x1b[0m', '\n--- Configure Firebase Credentials ---');
|
|
39
74
|
await runConfigureFlow();
|
|
75
|
+
runPromptFlow();
|
|
76
|
+
})
|
|
77
|
+
.command('manual', 'Open the detailed user manual', {}, async () => {
|
|
78
|
+
const manualPath = path.resolve(__dirname, 'MANUAL.md');
|
|
79
|
+
if (fs.existsSync(manualPath)) {
|
|
80
|
+
console.log('\n' + fs.readFileSync(manualPath, 'utf8'));
|
|
81
|
+
} else {
|
|
82
|
+
console.log('\x1b[31mManual file not found. Please check GitHub.\x1b[0m');
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.command('prompt', 'Show the integration guide and next steps', {}, async () => {
|
|
86
|
+
runPromptFlow();
|
|
40
87
|
})
|
|
41
|
-
.demandCommand(1, 'Please specify a command
|
|
88
|
+
.demandCommand(1, 'Please specify a valid command.')
|
|
89
|
+
.recommendCommands()
|
|
90
|
+
.strict()
|
|
91
|
+
.showHelpOnFail(true, 'Type --help for further assistance.')
|
|
92
|
+
.epilogue(`
|
|
93
|
+
📊 DASHBOARD SUMMARY:
|
|
94
|
+
--------------------------------------------------
|
|
95
|
+
1. CONFIG: 'releaseflow config set' (Setup credentials)
|
|
96
|
+
2. RELEASE: 'releaseflow release' (Build + Upload)
|
|
97
|
+
3. ROLLBACK: 'releaseflow rollback' (Quick Revert)
|
|
98
|
+
4. HELP: 'releaseflow prompt' (Integration checklist)
|
|
99
|
+
|
|
100
|
+
Full Documentation: 'releaseflow manual'
|
|
101
|
+
--------------------------------------------------
|
|
102
|
+
`)
|
|
42
103
|
.help()
|
|
43
104
|
.argv;
|
|
44
105
|
}
|
|
45
106
|
|
|
46
|
-
|
|
107
|
+
// Global path detection for pubspec.yaml
|
|
108
|
+
function getProjectPaths() {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
|
|
111
|
+
// Option 1: Current directory (user is inside flutter_app)
|
|
112
|
+
const p1 = path.join(cwd, 'pubspec.yaml');
|
|
113
|
+
// Option 2: flutter_app subfolder (user is in project root)
|
|
114
|
+
const p2 = path.join(cwd, 'flutter_app', 'pubspec.yaml');
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(p1)) return { root: cwd, pubspec: p1 };
|
|
117
|
+
if (fs.existsSync(p2)) return { root: path.join(cwd, 'flutter_app'), pubspec: p2 };
|
|
118
|
+
|
|
119
|
+
throw new Error('Could not find pubspec.yaml. Please run this command inside your Flutter project folder.');
|
|
120
|
+
}
|
|
47
121
|
|
|
48
122
|
function getPubspecVersion() {
|
|
49
123
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
124
|
+
const { pubspec } = getProjectPaths();
|
|
125
|
+
const doc = yaml.load(fs.readFileSync(pubspec, 'utf8'));
|
|
126
|
+
return doc.version.split('+')[0];
|
|
54
127
|
} catch (e) {
|
|
55
|
-
console.error('
|
|
128
|
+
console.error('\x1b[31mError: ' + e.message + '\x1b[0m');
|
|
129
|
+
process.exit(1);
|
|
56
130
|
}
|
|
57
|
-
return null;
|
|
58
131
|
}
|
|
59
132
|
|
|
60
133
|
function updatePubspecVersion(newVersion) {
|
|
61
134
|
try {
|
|
62
|
-
const
|
|
135
|
+
const { pubspec } = getProjectPaths();
|
|
136
|
+
const rawPubspec = fs.readFileSync(pubspec, 'utf8');
|
|
63
137
|
const updatedPubspec = rawPubspec.replace(/^version: ([\d\.]+)(.*)$/m, (match, v, build) => {
|
|
64
138
|
return `version: ${newVersion}${build || '+1'}`;
|
|
65
139
|
});
|
|
66
|
-
fs.writeFileSync(
|
|
140
|
+
fs.writeFileSync(pubspec, updatedPubspec);
|
|
67
141
|
return true;
|
|
68
142
|
} catch (e) {
|
|
69
|
-
console.error('
|
|
143
|
+
console.error('\x1b[31mError updating pubspec.yaml: ' + e.message + '\x1b[0m');
|
|
70
144
|
return false;
|
|
71
145
|
}
|
|
72
146
|
}
|
|
73
147
|
|
|
74
148
|
async function runConfigureFlow() {
|
|
149
|
+
const { choice } = await inquirer.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'list',
|
|
152
|
+
name: 'choice',
|
|
153
|
+
message: 'Where would you like to save the configuration?',
|
|
154
|
+
choices: [
|
|
155
|
+
{ name: `Current Project (${path.basename(process.cwd())})`, value: 'this' },
|
|
156
|
+
{ name: 'Select a Subfolder/Project...', value: 'select' },
|
|
157
|
+
{ name: 'Enter a Custom Path...', value: 'manual' }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
let configLocation = '';
|
|
163
|
+
|
|
164
|
+
switch (choice) {
|
|
165
|
+
case 'this':
|
|
166
|
+
configLocation = path.join(process.cwd(), 'releaseflow.config.json');
|
|
167
|
+
console.log(`\x1b[36mTargeting Current Project: ${path.basename(process.cwd())}\x1b[0m`);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'select':
|
|
171
|
+
const subDirs = fs.readdirSync(process.cwd())
|
|
172
|
+
.filter(f => fs.statSync(path.join(process.cwd(), f)).isDirectory() && !f.startsWith('.'))
|
|
173
|
+
.map(f => ({ name: `${f}/`, value: path.join(process.cwd(), f) }));
|
|
174
|
+
|
|
175
|
+
if (subDirs.length === 0) {
|
|
176
|
+
console.log('\x1b[33mNo subfolders found. Reverting to Current Project.\x1b[0m');
|
|
177
|
+
configLocation = path.join(process.cwd(), 'releaseflow.config.json');
|
|
178
|
+
} else {
|
|
179
|
+
const { selectedDir } = await inquirer.prompt([{
|
|
180
|
+
type: 'list',
|
|
181
|
+
name: 'selectedDir',
|
|
182
|
+
message: 'Pick a project folder:',
|
|
183
|
+
choices: subDirs
|
|
184
|
+
}]);
|
|
185
|
+
configLocation = path.join(selectedDir, 'releaseflow.config.json');
|
|
186
|
+
console.log(`\x1b[36mTargeting Folder: ${path.basename(selectedDir)}\x1b[0m`);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'manual':
|
|
191
|
+
const { manualPath } = await inquirer.prompt([{
|
|
192
|
+
type: 'input',
|
|
193
|
+
name: 'manualPath',
|
|
194
|
+
message: 'Enter the exact directory path:',
|
|
195
|
+
validate: (input) => {
|
|
196
|
+
const fullPath = path.resolve(process.cwd(), input);
|
|
197
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory() ? true : 'Please provide a valid directory path.';
|
|
198
|
+
}
|
|
199
|
+
}]);
|
|
200
|
+
configLocation = path.join(path.resolve(process.cwd(), manualPath), 'releaseflow.config.json');
|
|
201
|
+
console.log('\x1b[36mTargeting Custom Path\x1b[0m');
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let defaultConfig = {
|
|
206
|
+
serviceAccountPath: '',
|
|
207
|
+
storageBucket: 'your-project-id.firebasestorage.app'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Try to find ANY existing config to use as a helpful default
|
|
211
|
+
const bestAvailableConfig = fs.existsSync(configLocation) ? configLocation : getConfigPath();
|
|
212
|
+
|
|
213
|
+
if (fs.existsSync(bestAvailableConfig)) {
|
|
214
|
+
try {
|
|
215
|
+
const existing = JSON.parse(fs.readFileSync(bestAvailableConfig, 'utf8'));
|
|
216
|
+
defaultConfig = { ...defaultConfig, ...existing };
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
}
|
|
219
|
+
|
|
75
220
|
const answers = await inquirer.prompt([
|
|
76
221
|
{
|
|
77
222
|
type: 'input',
|
|
78
223
|
name: 'serviceAccountPath',
|
|
79
|
-
message: 'Enter the path to your Firebase Service Account JSON file:',
|
|
224
|
+
message: 'Enter the path to your Firebase Service Account JSON file:\n (Settings > Service Accounts > Select Node.js > Generate new private key > Download and enter its file path)\n Path:',
|
|
225
|
+
default: defaultConfig.serviceAccountPath,
|
|
80
226
|
validate: (input) => {
|
|
81
227
|
const fullPath = path.resolve(process.cwd(), input);
|
|
82
228
|
if (fs.existsSync(fullPath) && (input.endsWith('.json') || fs.statSync(fullPath).isFile())) {
|
|
@@ -88,8 +234,8 @@ async function runConfigureFlow() {
|
|
|
88
234
|
{
|
|
89
235
|
type: 'input',
|
|
90
236
|
name: 'storageBucket',
|
|
91
|
-
message: 'Enter your Firebase Storage Bucket name:',
|
|
92
|
-
default:
|
|
237
|
+
message: 'Enter your Firebase Storage Bucket name:\n (Go to Firebase Console > Storage > Files > Copy the bucket name without gs://)\n Name:',
|
|
238
|
+
default: defaultConfig.storageBucket
|
|
93
239
|
}
|
|
94
240
|
]);
|
|
95
241
|
|
|
@@ -97,8 +243,61 @@ async function runConfigureFlow() {
|
|
|
97
243
|
serviceAccountPath: path.resolve(process.cwd(), answers.serviceAccountPath),
|
|
98
244
|
storageBucket: answers.storageBucket
|
|
99
245
|
};
|
|
100
|
-
|
|
101
|
-
|
|
246
|
+
|
|
247
|
+
fs.writeFileSync(configLocation, JSON.stringify(config, null, 2));
|
|
248
|
+
console.log('\x1b[32mConfiguration saved successfully!\x1b[0m\n');
|
|
249
|
+
console.log(`\x1b[34mSaved to: ${configLocation}\x1b[0m`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function runPromptFlow() {
|
|
253
|
+
console.log('\n\x1b[36m--- NEXT STEPS FOR YOUR FLUTTER APP ---\x1b[0m');
|
|
254
|
+
console.log('1. Manual UI: You must build the Force Update screen in your Flutter code.');
|
|
255
|
+
console.log('2. Firestore: The CLI will manage "app_config/version" automatically.');
|
|
256
|
+
console.log('3. Security: Allow your users to READ "app_config/version" in Firebase Rules.');
|
|
257
|
+
|
|
258
|
+
console.log('\n\x1b[33m--- VERIFICATION CHECKLIST (TEST YOUR UI) ---\x1b[0m');
|
|
259
|
+
console.log('If you have built your UI, here is how to verify it works:');
|
|
260
|
+
console.log(' a. Run "releaseflow release" and set "Force Update" to TRUE.');
|
|
261
|
+
console.log(' b. Open your old app version: It should now show your Force Update screen.');
|
|
262
|
+
console.log(' c. Run "releaseflow rollback": Your app should return to normal.');
|
|
263
|
+
|
|
264
|
+
console.log('\n\x1b[35m--- READY TO RELEASE? ---\x1b[0m');
|
|
265
|
+
console.log('Run these commands to start:');
|
|
266
|
+
console.log(' releaseflow release : Build and Upload your first APK.');
|
|
267
|
+
console.log(' releaseflow config show : Verify your current project settings.');
|
|
268
|
+
console.log(' releaseflow manual : View full Flutter integration guide.\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function runRemoveConfig() {
|
|
272
|
+
const CONFIG_PATH = getConfigPath();
|
|
273
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
274
|
+
const { confirm } = await inquirer.prompt([{
|
|
275
|
+
type: 'confirm',
|
|
276
|
+
name: 'confirm',
|
|
277
|
+
message: 'Are you sure you want to remove your Firebase configuration?',
|
|
278
|
+
default: false
|
|
279
|
+
}]);
|
|
280
|
+
|
|
281
|
+
if (confirm) {
|
|
282
|
+
fs.unlinkSync(CONFIG_PATH);
|
|
283
|
+
console.log('\x1b[32mConfiguration removed successfully!\x1b[0m\n');
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.log('\x1b[33mNo configuration found to remove.\x1b[0m\n');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function runShowConfig() {
|
|
291
|
+
const CONFIG_PATH = getConfigPath();
|
|
292
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
293
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
294
|
+
console.log('\n\x1b[36m--- Current Configuration ---\x1b[0m');
|
|
295
|
+
console.log(`Config File: \x1b[34m${CONFIG_PATH}\x1b[0m`);
|
|
296
|
+
console.log(`Service Account: \x1b[33m${config.serviceAccountPath}\x1b[0m`);
|
|
297
|
+
console.log(`Storage Bucket: \x1b[33m${config.storageBucket}\x1b[0m\n`);
|
|
298
|
+
} else {
|
|
299
|
+
console.log('\x1b[33mNo configuration found.\x1b[0m\n');
|
|
300
|
+
}
|
|
102
301
|
}
|
|
103
302
|
|
|
104
303
|
async function runFullReleaseFlow() {
|
|
@@ -133,29 +332,30 @@ async function runFullReleaseFlow() {
|
|
|
133
332
|
if (choice.action === 'auto') {
|
|
134
333
|
localVersion = suggestedVersion;
|
|
135
334
|
updatePubspecVersion(localVersion);
|
|
136
|
-
console.log(
|
|
335
|
+
console.log(`Updated pubspec.yaml to ${localVersion}`);
|
|
137
336
|
}
|
|
138
337
|
} else {
|
|
139
|
-
console.log('
|
|
338
|
+
console.log('New version detected! Proceeding...\n');
|
|
140
339
|
}
|
|
141
340
|
|
|
142
|
-
|
|
143
|
-
|
|
341
|
+
|
|
342
|
+
const { root } = getProjectPaths();
|
|
343
|
+
console.log('\x1b[35mBuilding Optimized APK (arm64)... Please wait...\x1b[0m');
|
|
144
344
|
|
|
145
345
|
const build = spawn('flutter', ['build', 'apk', '--release', '--split-per-abi', '--target-platform', 'android-arm64'], {
|
|
146
|
-
cwd:
|
|
346
|
+
cwd: root,
|
|
147
347
|
stdio: 'inherit'
|
|
148
348
|
});
|
|
149
349
|
|
|
150
350
|
build.on('close', async (code) => {
|
|
151
351
|
if (code !== 0) {
|
|
152
|
-
console.error('\x1b[
|
|
352
|
+
console.error('\x1b[31mBuild failed!\x1b[0m');
|
|
153
353
|
return;
|
|
154
354
|
}
|
|
155
355
|
|
|
156
|
-
console.log('\x1b[
|
|
157
|
-
const apkPath = '
|
|
158
|
-
|
|
356
|
+
console.log('\x1b[32mBuild Successful!\x1b[0m');
|
|
357
|
+
const apkPath = path.join(root, 'build/app/outputs/flutter-apk/app-arm64-v8a-release.apk');
|
|
358
|
+
|
|
159
359
|
|
|
160
360
|
const answers = await inquirer.prompt([{
|
|
161
361
|
type: 'confirm',
|
|
@@ -165,16 +365,16 @@ async function runFullReleaseFlow() {
|
|
|
165
365
|
}]);
|
|
166
366
|
|
|
167
367
|
try {
|
|
168
|
-
console.log('\n\x1b[
|
|
169
|
-
const downloadUrl = await uploadApk(
|
|
368
|
+
console.log('\n\x1b[33mStarting Cloud Release Flow...\x1b[0m');
|
|
369
|
+
const downloadUrl = await uploadApk(apkPath, localVersion);
|
|
170
370
|
await updateFirestore(localVersion, downloadUrl, answers.forceUpdate);
|
|
171
371
|
|
|
172
372
|
console.log('\n\x1b[35mSummary:\x1b[0m');
|
|
173
373
|
console.log(`Version: ${localVersion}`);
|
|
174
374
|
console.log(`Force Update: ${answers.forceUpdate ? 'Yes' : 'No'}`);
|
|
175
|
-
console.log('\n\x1b[
|
|
375
|
+
console.log('\n\x1b[32mRelease Complete! Your app is now LIVE.\x1b[0m');
|
|
176
376
|
} catch (error) {
|
|
177
|
-
console.error('\x1b[31m\
|
|
377
|
+
console.error('\x1b[31m\nError: ' + error.message + '\x1b[0m');
|
|
178
378
|
}
|
|
179
379
|
});
|
|
180
380
|
}
|
|
@@ -184,7 +384,7 @@ async function runRollbackFlow() {
|
|
|
184
384
|
const versions = await listAvailableVersions();
|
|
185
385
|
|
|
186
386
|
if (versions.length === 0) {
|
|
187
|
-
console.log('\x1b[
|
|
387
|
+
console.log('\x1b[31mNo previous versions found.\x1b[0m');
|
|
188
388
|
return;
|
|
189
389
|
}
|
|
190
390
|
|
|
@@ -210,11 +410,11 @@ async function runRollbackFlow() {
|
|
|
210
410
|
]);
|
|
211
411
|
|
|
212
412
|
try {
|
|
213
|
-
console.log(`\n\x1b[
|
|
413
|
+
console.log(`\n\x1b[33mRolling back to version ${answers.version}...\x1b[0m`);
|
|
214
414
|
await rollbackToVersion(answers.version, answers.forceUpdate);
|
|
215
|
-
console.log('\n\x1b[
|
|
415
|
+
console.log('\n\x1b[32mSuccess! The database has been rolled back.\x1b[0m');
|
|
216
416
|
} catch (error) {
|
|
217
|
-
console.error('\n\x1b[
|
|
417
|
+
console.error('\n\x1b[31mRollback failed: ' + error.message + '\x1b[0m');
|
|
218
418
|
}
|
|
219
419
|
}
|
|
220
420
|
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|