fire-pilot-rn 1.0.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/GoogleService-Info.plist +30 -0
- package/LICENSE +21 -0
- package/README.md +391 -0
- package/build/commands/enable.d.ts +2 -0
- package/build/commands/enable.d.ts.map +1 -0
- package/build/commands/enable.js +73 -0
- package/build/commands/enable.js.map +1 -0
- package/build/commands/login.d.ts +2 -0
- package/build/commands/login.d.ts.map +1 -0
- package/build/commands/login.js +117 -0
- package/build/commands/login.js.map +1 -0
- package/build/commands/setup.d.ts +2 -0
- package/build/commands/setup.d.ts.map +1 -0
- package/build/commands/setup.js +297 -0
- package/build/commands/setup.js.map +1 -0
- package/build/commands/sha.d.ts +7 -0
- package/build/commands/sha.d.ts.map +1 -0
- package/build/commands/sha.js +165 -0
- package/build/commands/sha.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +59 -0
- package/build/index.js.map +1 -0
- package/build/utils/checker.d.ts +2 -0
- package/build/utils/checker.d.ts.map +1 -0
- package/build/utils/checker.js +100 -0
- package/build/utils/checker.js.map +1 -0
- package/build/utils/logger.d.ts +11 -0
- package/build/utils/logger.d.ts.map +1 -0
- package/build/utils/logger.js +18 -0
- package/build/utils/logger.js.map +1 -0
- package/build/utils/runner.d.ts +7 -0
- package/build/utils/runner.d.ts.map +1 -0
- package/build/utils/runner.js +125 -0
- package/build/utils/runner.js.map +1 -0
- package/dist/commands/enable.d.ts +2 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/enable.js +65 -0
- package/dist/commands/enable.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +86 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +133 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/sha.d.ts +2 -0
- package/dist/commands/sha.d.ts.map +1 -0
- package/dist/commands/sha.js +118 -0
- package/dist/commands/sha.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/checker.d.ts +2 -0
- package/dist/utils/checker.d.ts.map +1 -0
- package/dist/utils/checker.js +100 -0
- package/dist/utils/checker.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/runner.d.ts +4 -0
- package/dist/utils/runner.d.ts.map +1 -0
- package/dist/utils/runner.js +36 -0
- package/dist/utils/runner.js.map +1 -0
- package/google-services.json +48 -0
- package/package.json +51 -0
- package/src/commands/enable.ts +80 -0
- package/src/commands/login.ts +119 -0
- package/src/commands/setup.ts +313 -0
- package/src/commands/sha.ts +177 -0
- package/src/index.ts +65 -0
- package/src/utils/checker.ts +70 -0
- package/src/utils/logger.ts +13 -0
- package/src/utils/runner.ts +98 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { runLive, runSilent } from '../utils/runner';
|
|
3
|
+
import { logger } from '../utils/logger';
|
|
4
|
+
|
|
5
|
+
export async function loginCommand(): Promise<void> {
|
|
6
|
+
logger.title('Firebase Account Manager');
|
|
7
|
+
|
|
8
|
+
// ── Get current logged-in accounts ──────────────────────────
|
|
9
|
+
logger.step('Fetching logged-in Firebase accounts...');
|
|
10
|
+
const accountsRaw = runSilent('firebase', ['login:list']);
|
|
11
|
+
|
|
12
|
+
// Parse emails from output
|
|
13
|
+
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
14
|
+
const accounts: string[] = accountsRaw.match(emailRegex) ?? [];
|
|
15
|
+
|
|
16
|
+
if (accounts.length > 0) {
|
|
17
|
+
logger.info('Currently logged-in accounts:');
|
|
18
|
+
accounts.forEach((acc, i) => console.log(` ${i + 1}. ${acc}`));
|
|
19
|
+
} else {
|
|
20
|
+
logger.warn('No accounts logged in yet.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// ── Ask what the user wants to do (loop until cancel) ──────
|
|
26
|
+
let continueMenu = true;
|
|
27
|
+
let currentAccounts = accounts;
|
|
28
|
+
|
|
29
|
+
while (continueMenu) {
|
|
30
|
+
const { action } = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'action',
|
|
34
|
+
message: 'What do you want to do?',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: '🔑 Login with a new Google account', value: 'login' },
|
|
37
|
+
{ name: '🔄 Switch to a different account', value: 'switch' },
|
|
38
|
+
{ name: '📋 List all logged-in accounts', value: 'list' },
|
|
39
|
+
{ name: '🚪 Logout from current account', value: 'logout' },
|
|
40
|
+
{ name: '⬅️ Continue to next step', value: 'cancel' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (action === 'cancel') {
|
|
46
|
+
continueMenu = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Login ────────────────────────────────────────────────────
|
|
50
|
+
else if (action === 'login') {
|
|
51
|
+
logger.info('Opening browser to add a new Google account...');
|
|
52
|
+
runLive('firebase', ['login:add']);
|
|
53
|
+
logger.success('Account added!');
|
|
54
|
+
// Refresh account list after login
|
|
55
|
+
const refreshed = runSilent('firebase', ['login:list']);
|
|
56
|
+
currentAccounts = refreshed.match(emailRegex) ?? [];
|
|
57
|
+
if (currentAccounts.length > 0) {
|
|
58
|
+
logger.info('Logged-in accounts:');
|
|
59
|
+
currentAccounts.forEach((acc, i) => console.log(` ${i + 1}. ${acc}`));
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Switch account ───────────────────────────────────────────
|
|
65
|
+
else if (action === 'switch') {
|
|
66
|
+
if (currentAccounts.length === 0) {
|
|
67
|
+
logger.error('No accounts found. Use "Login with a new Google account" to add one.');
|
|
68
|
+
} else if (currentAccounts.length === 1) {
|
|
69
|
+
logger.warn(`Only one account is registered: ${currentAccounts[0]}`);
|
|
70
|
+
logger.info('Use "🔑 Login with a new Google account" to add more accounts first.');
|
|
71
|
+
console.log('');
|
|
72
|
+
} else {
|
|
73
|
+
const { selectedAccount } = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'selectedAccount',
|
|
77
|
+
message: 'Select the account to switch to:',
|
|
78
|
+
choices: currentAccounts,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
runLive('firebase', ['login:use', selectedAccount]);
|
|
82
|
+
logger.success(`Switched to: ${selectedAccount}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── List accounts ─────────────────────────────────────────────
|
|
87
|
+
else if (action === 'list') {
|
|
88
|
+
const refreshed = runSilent('firebase', ['login:list']);
|
|
89
|
+
currentAccounts = refreshed.match(emailRegex) ?? [];
|
|
90
|
+
if (currentAccounts.length > 0) {
|
|
91
|
+
logger.info('All logged-in accounts:');
|
|
92
|
+
currentAccounts.forEach((acc, i) => console.log(` ${i + 1}. ${acc}`));
|
|
93
|
+
} else {
|
|
94
|
+
logger.warn('No accounts are currently logged in.');
|
|
95
|
+
}
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Logout ───────────────────────────────────────────────────
|
|
100
|
+
else if (action === 'logout') {
|
|
101
|
+
const { confirmLogout } = await inquirer.prompt([
|
|
102
|
+
{
|
|
103
|
+
type: 'confirm',
|
|
104
|
+
name: 'confirmLogout',
|
|
105
|
+
message: 'Are you sure you want to logout?',
|
|
106
|
+
default: false,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
if (confirmLogout) {
|
|
111
|
+
runLive('firebase', ['logout']);
|
|
112
|
+
logger.success('Logged out successfully!');
|
|
113
|
+
// Refresh after logout
|
|
114
|
+
const refreshed = runSilent('firebase', ['login:list']);
|
|
115
|
+
currentAccounts = refreshed.match(emailRegex) ?? [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { checkAllTools } from '../utils/checker';
|
|
6
|
+
import { loginCommand } from './login';
|
|
7
|
+
import { shaCommand } from './sha';
|
|
8
|
+
import { enableCommand } from './enable';
|
|
9
|
+
import { runCheckedOutput, runLive } from '../utils/runner';
|
|
10
|
+
import { logger } from '../utils/logger';
|
|
11
|
+
|
|
12
|
+
type FirebaseProject = {
|
|
13
|
+
projectId: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type FirebaseApp = {
|
|
18
|
+
appId: string;
|
|
19
|
+
displayName?: string;
|
|
20
|
+
packageName?: string;
|
|
21
|
+
bundleId?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AppSetupAnswers = {
|
|
25
|
+
platforms: Array<'ANDROID' | 'IOS'>;
|
|
26
|
+
appDisplayName: string;
|
|
27
|
+
androidPackageName?: string;
|
|
28
|
+
iosBundleId?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function parseFirebaseResult<T>(output: string, label: string): T {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(output) as { result?: T };
|
|
34
|
+
|
|
35
|
+
if (parsed.result === undefined) {
|
|
36
|
+
throw new Error('Missing result field');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parsed.result;
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`Could not parse Firebase ${label} output.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAndroidConfigPath(cwd: string): string {
|
|
46
|
+
const androidAppDir = path.join(cwd, 'android', 'app');
|
|
47
|
+
return existsSync(androidAppDir)
|
|
48
|
+
? path.join(androidAppDir, 'google-services.json')
|
|
49
|
+
: path.join(cwd, 'google-services.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getIosConfigPath(cwd: string): string {
|
|
53
|
+
const iosDir = path.join(cwd, 'ios');
|
|
54
|
+
|
|
55
|
+
if (!existsSync(iosDir)) {
|
|
56
|
+
return path.join(cwd, 'GoogleService-Info.plist');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const xcodeProject = readdirSync(iosDir).find((entry) => entry.endsWith('.xcodeproj'));
|
|
60
|
+
|
|
61
|
+
if (xcodeProject) {
|
|
62
|
+
const appDir = path.join(iosDir, path.basename(xcodeProject, '.xcodeproj'));
|
|
63
|
+
|
|
64
|
+
if (existsSync(appDir)) {
|
|
65
|
+
return path.join(appDir, 'GoogleService-Info.plist');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return path.join(iosDir, 'GoogleService-Info.plist');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateMobileIdentifier(value: string): true | string {
|
|
73
|
+
return /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/.test(value.trim())
|
|
74
|
+
? true
|
|
75
|
+
: 'Enter a valid identifier like com.example.app';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listFirebaseProjects(): FirebaseProject[] {
|
|
79
|
+
const output = runCheckedOutput('firebase', ['projects:list', '--json']);
|
|
80
|
+
return parseFirebaseResult<FirebaseProject[]>(output, 'project list');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listFirebaseApps(platform: 'ANDROID' | 'IOS', projectId: string): FirebaseApp[] {
|
|
84
|
+
const output = runCheckedOutput('firebase', ['apps:list', platform, '--project', projectId, '--json']);
|
|
85
|
+
return parseFirebaseResult<FirebaseApp[]>(output, `${platform} app list`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureFirebaseApp(
|
|
89
|
+
platform: 'ANDROID' | 'IOS',
|
|
90
|
+
projectId: string,
|
|
91
|
+
displayName: string,
|
|
92
|
+
identifier: string,
|
|
93
|
+
): FirebaseApp {
|
|
94
|
+
const existingApps = listFirebaseApps(platform, projectId);
|
|
95
|
+
const match = existingApps.find((app) =>
|
|
96
|
+
platform === 'ANDROID' ? app.packageName === identifier : app.bundleId === identifier,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (match) {
|
|
100
|
+
logger.success(`${platform} app already exists: ${match.appId}`);
|
|
101
|
+
return match;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
logger.step(`Creating Firebase ${platform} app...`);
|
|
105
|
+
const identifierFlag = platform === 'ANDROID' ? '--package-name' : '--bundle-id';
|
|
106
|
+
const output = runCheckedOutput('firebase', [
|
|
107
|
+
'apps:create',
|
|
108
|
+
platform,
|
|
109
|
+
displayName,
|
|
110
|
+
identifierFlag,
|
|
111
|
+
identifier,
|
|
112
|
+
'--project',
|
|
113
|
+
projectId,
|
|
114
|
+
'--json',
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
return parseFirebaseResult<FirebaseApp>(output, `${platform} app creation`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exportFirebaseConfig(platform: 'ANDROID' | 'IOS', appId: string, outputPath: string): void {
|
|
121
|
+
mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
122
|
+
logger.step(`Writing ${path.basename(outputPath)}...`);
|
|
123
|
+
runLive('firebase', ['apps:sdkconfig', platform, appId, '-o', outputPath]);
|
|
124
|
+
logger.success(`Saved ${platform} config to ${outputPath}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function setupCommand(full: boolean): Promise<void> {
|
|
128
|
+
logger.title('FirePilot RN — Firebase Setup Wizard');
|
|
129
|
+
console.log('This wizard will set up Firebase for your React Native project.\n');
|
|
130
|
+
|
|
131
|
+
// ── STEP 1: Check & install all tools ───────────────────────
|
|
132
|
+
await checkAllTools();
|
|
133
|
+
|
|
134
|
+
// ── STEP 2: Firebase login ───────────────────────────────────
|
|
135
|
+
logger.step('STEP 2 — Firebase Login');
|
|
136
|
+
const { doLogin } = await inquirer.prompt([{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'doLogin',
|
|
139
|
+
message: 'Do you want to manage your Firebase login now?',
|
|
140
|
+
default: true,
|
|
141
|
+
}]);
|
|
142
|
+
if (doLogin) await loginCommand();
|
|
143
|
+
|
|
144
|
+
// ── STEP 3: Create or select Firebase project ────────────────
|
|
145
|
+
logger.step('STEP 3 — Firebase Project');
|
|
146
|
+
logger.info('Fetching your existing Firebase projects...');
|
|
147
|
+
const existingProjects = listFirebaseProjects();
|
|
148
|
+
|
|
149
|
+
const { projectAction } = await inquirer.prompt([{
|
|
150
|
+
type: 'list',
|
|
151
|
+
name: 'projectAction',
|
|
152
|
+
message: 'What do you want to do?',
|
|
153
|
+
choices: [
|
|
154
|
+
{ name: '➕ Create a new Firebase project', value: 'create' },
|
|
155
|
+
{ name: '✅ Use an existing project', value: 'existing' },
|
|
156
|
+
],
|
|
157
|
+
}]);
|
|
158
|
+
|
|
159
|
+
let projectId = '';
|
|
160
|
+
|
|
161
|
+
if (projectAction === 'create') {
|
|
162
|
+
const { newProjectId } = await inquirer.prompt([{
|
|
163
|
+
type: 'input',
|
|
164
|
+
name: 'newProjectId',
|
|
165
|
+
message: 'Enter new Firebase project ID:',
|
|
166
|
+
validate: (val: string) =>
|
|
167
|
+
val.trim().length > 0 ? true : 'Project ID cannot be empty',
|
|
168
|
+
}]);
|
|
169
|
+
projectId = newProjectId.trim();
|
|
170
|
+
|
|
171
|
+
const spinner = ora(`Creating Firebase project: ${projectId}...`).start();
|
|
172
|
+
try {
|
|
173
|
+
runLive('firebase', ['projects:create', projectId]);
|
|
174
|
+
spinner.succeed(`Project "${projectId}" created!`);
|
|
175
|
+
} catch (e: any) {
|
|
176
|
+
spinner.fail('Project creation failed!');
|
|
177
|
+
// ── Real-Time Diagnostics (like fire_pilot) ──────────────
|
|
178
|
+
if (e.message.includes('quota')) {
|
|
179
|
+
logger.error('REASON: Firebase project quota exceeded.');
|
|
180
|
+
logger.info('Fix: Delete old projects at https://console.firebase.google.com');
|
|
181
|
+
} else if (e.message.includes('already exists')) {
|
|
182
|
+
logger.error('REASON: A project with this ID already exists.');
|
|
183
|
+
logger.info('Fix: Choose a different project ID.');
|
|
184
|
+
} else {
|
|
185
|
+
logger.error('REASON: ' + e.message);
|
|
186
|
+
}
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
if (existingProjects.length === 0) {
|
|
191
|
+
logger.error('No Firebase projects found for the current account.');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { existingId } = await inquirer.prompt([{
|
|
196
|
+
type: 'list',
|
|
197
|
+
name: 'existingId',
|
|
198
|
+
message: 'Select the Firebase project to use:',
|
|
199
|
+
choices: existingProjects.map((project) => ({
|
|
200
|
+
name: `${project.displayName} (${project.projectId})`,
|
|
201
|
+
value: project.projectId,
|
|
202
|
+
})),
|
|
203
|
+
}]);
|
|
204
|
+
projectId = existingId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── STEP 4: Install @react-native-firebase/app ───────────────
|
|
208
|
+
logger.step('STEP 4 — Install React Native Firebase Core');
|
|
209
|
+
const spinner2 = ora('Installing @react-native-firebase/app...').start();
|
|
210
|
+
try {
|
|
211
|
+
runLive('npm', ['install', '@react-native-firebase/app']);
|
|
212
|
+
spinner2.succeed('@react-native-firebase/app installed!');
|
|
213
|
+
} catch {
|
|
214
|
+
spinner2.fail('Installation failed. Run manually: npm install @react-native-firebase/app');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── STEP 5: Create apps and download config files ───────────
|
|
218
|
+
logger.step('STEP 5 — Firebase App Setup');
|
|
219
|
+
const defaultDisplayName = path.basename(process.cwd());
|
|
220
|
+
const { platforms, appDisplayName, androidPackageName, iosBundleId } = await inquirer.prompt<AppSetupAnswers>([
|
|
221
|
+
{
|
|
222
|
+
type: 'checkbox',
|
|
223
|
+
name: 'platforms',
|
|
224
|
+
message: 'Create or configure Firebase apps for:',
|
|
225
|
+
choices: [
|
|
226
|
+
{ name: 'Android', value: 'ANDROID', checked: true },
|
|
227
|
+
{ name: 'iOS', value: 'IOS', checked: true },
|
|
228
|
+
],
|
|
229
|
+
validate: (selected: string[]) =>
|
|
230
|
+
selected.length > 0 ? true : 'Select at least one platform',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: 'input',
|
|
234
|
+
name: 'appDisplayName',
|
|
235
|
+
message: 'Enter the Firebase app display name:',
|
|
236
|
+
default: defaultDisplayName,
|
|
237
|
+
validate: (value: string) => value.trim().length > 0 ? true : 'Display name cannot be empty',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'input',
|
|
241
|
+
name: 'androidPackageName',
|
|
242
|
+
message: 'Enter the Android package name:',
|
|
243
|
+
when: (answers: { platforms: string[] }) => answers.platforms.includes('ANDROID'),
|
|
244
|
+
validate: validateMobileIdentifier,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: 'input',
|
|
248
|
+
name: 'iosBundleId',
|
|
249
|
+
message: 'Enter the iOS bundle identifier:',
|
|
250
|
+
when: (answers: { platforms: string[] }) => answers.platforms.includes('IOS'),
|
|
251
|
+
validate: validateMobileIdentifier,
|
|
252
|
+
},
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
let androidAppId: string | undefined;
|
|
256
|
+
|
|
257
|
+
if (platforms.includes('ANDROID')) {
|
|
258
|
+
if (!androidPackageName) {
|
|
259
|
+
logger.error('Android package name is required to create or configure the Android app.');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const androidApp = ensureFirebaseApp('ANDROID', projectId, appDisplayName.trim(), androidPackageName.trim());
|
|
264
|
+
androidAppId = androidApp.appId;
|
|
265
|
+
exportFirebaseConfig('ANDROID', androidAppId, getAndroidConfigPath(process.cwd()));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (platforms.includes('IOS')) {
|
|
269
|
+
if (!iosBundleId) {
|
|
270
|
+
logger.error('iOS bundle identifier is required to create or configure the iOS app.');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const iosApp = ensureFirebaseApp('IOS', projectId, appDisplayName.trim(), iosBundleId.trim());
|
|
275
|
+
exportFirebaseConfig('IOS', iosApp.appId, getIosConfigPath(process.cwd()));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── STEP 6: SHA keys (only if --full) ───────────────────────
|
|
279
|
+
if (full) {
|
|
280
|
+
logger.step('STEP 6 — SHA Key Registration');
|
|
281
|
+
const { doSha } = await inquirer.prompt([{
|
|
282
|
+
type: 'confirm',
|
|
283
|
+
name: 'doSha',
|
|
284
|
+
message: 'Do you want to register Android SHA keys now?',
|
|
285
|
+
default: true,
|
|
286
|
+
}]);
|
|
287
|
+
if (doSha && androidAppId) {
|
|
288
|
+
await shaCommand({ projectId, appId: androidAppId });
|
|
289
|
+
} else if (doSha) {
|
|
290
|
+
logger.warn('Skipping SHA registration because no Android app was configured in this run.');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── STEP 7: Enable Firebase features ────────────────────────
|
|
295
|
+
logger.step('STEP 7 — Enable Firebase Features');
|
|
296
|
+
const { doFeatures } = await inquirer.prompt([{
|
|
297
|
+
type: 'confirm',
|
|
298
|
+
name: 'doFeatures',
|
|
299
|
+
message: 'Do you want to install additional Firebase features (Auth, Firestore, etc)?',
|
|
300
|
+
default: true,
|
|
301
|
+
}]);
|
|
302
|
+
if (doFeatures) await enableCommand();
|
|
303
|
+
|
|
304
|
+
// ── DONE ─────────────────────────────────────────────────────
|
|
305
|
+
console.log('\n' + '─'.repeat(60));
|
|
306
|
+
logger.success('🎉 Firebase setup complete for your React Native project!');
|
|
307
|
+
console.log('─'.repeat(60));
|
|
308
|
+
logger.info('Next steps:');
|
|
309
|
+
console.log(' 1. Verify google-services.json and GoogleService-Info.plist were written to your project');
|
|
310
|
+
console.log(' 2. Follow platform setup at https://rnfirebase.io');
|
|
311
|
+
console.log(' 3. Run your app: npx react-native run-android');
|
|
312
|
+
console.log('─'.repeat(60) + '\n');
|
|
313
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { runLive, runSilent, resolveJavaTool } from '../utils/runner';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
type FirebaseProject = {
|
|
8
|
+
projectId: string;
|
|
9
|
+
projectNumber: string;
|
|
10
|
+
displayName: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type FirebaseAndroidApp = {
|
|
14
|
+
appId: string;
|
|
15
|
+
displayName?: string;
|
|
16
|
+
packageName?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseFirebaseJson<T>(output: string, label: string): T[] {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(output) as { result?: T[] };
|
|
22
|
+
return parsed.result ?? [];
|
|
23
|
+
} catch {
|
|
24
|
+
logger.error(`Could not parse Firebase ${label} output.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ShaCommandOptions = {
|
|
30
|
+
projectId?: string;
|
|
31
|
+
appId?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ProjectPromptAnswer = {
|
|
35
|
+
projectId: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AndroidAppPromptAnswer = {
|
|
39
|
+
appId: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function shaCommand(options: ShaCommandOptions = {}): Promise<void> {
|
|
43
|
+
logger.title('SHA Key Registration');
|
|
44
|
+
|
|
45
|
+
// ── Check keytool exists ─────────────────────────────────────
|
|
46
|
+
const keytoolPath = resolveJavaTool('keytool');
|
|
47
|
+
|
|
48
|
+
if (!keytoolPath) {
|
|
49
|
+
logger.error('Java keytool not found!');
|
|
50
|
+
logger.info('Install Java from: https://adoptium.net');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Find the Android debug keystore ─────────────────────────
|
|
55
|
+
const keystorePath = path.join(os.homedir(), '.android', 'debug.keystore');
|
|
56
|
+
logger.info(`Looking for keystore at: ${keystorePath}`);
|
|
57
|
+
|
|
58
|
+
// ── Extract SHA keys ─────────────────────────────────────────
|
|
59
|
+
logger.step('Extracting SHA-1 and SHA-256 from debug keystore...');
|
|
60
|
+
const output = runSilent(keytoolPath, [
|
|
61
|
+
'-list', '-v',
|
|
62
|
+
'-keystore', keystorePath,
|
|
63
|
+
'-alias', 'androiddebugkey',
|
|
64
|
+
'-storepass', 'android',
|
|
65
|
+
'-keypass', 'android',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
if (!output) {
|
|
69
|
+
logger.error('Could not read keystore. Make sure you have run your React Native Android app at least once.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Parse SHA-1 and SHA-256 ──────────────────────────────────
|
|
74
|
+
const sha1Match = output.match(/SHA1:\s+([A-F0-9:]+)/i);
|
|
75
|
+
const sha256Match = output.match(/SHA256:\s+([A-F0-9:]+)/i);
|
|
76
|
+
|
|
77
|
+
if (!sha1Match || !sha256Match) {
|
|
78
|
+
logger.error('SHA keys not found in keystore output.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sha1 = sha1Match[1].trim();
|
|
83
|
+
const sha256 = sha256Match[1].trim();
|
|
84
|
+
|
|
85
|
+
logger.success(`SHA-1: ${sha1}`);
|
|
86
|
+
logger.success(`SHA-256: ${sha256}`);
|
|
87
|
+
|
|
88
|
+
let projectId = options.projectId;
|
|
89
|
+
|
|
90
|
+
if (!projectId) {
|
|
91
|
+
logger.step('Fetching your Firebase projects...');
|
|
92
|
+
const projectOutput = runSilent('firebase', ['projects:list', '--json']);
|
|
93
|
+
const projects = parseFirebaseJson<FirebaseProject>(projectOutput, 'project list');
|
|
94
|
+
|
|
95
|
+
if (projects.length === 0) {
|
|
96
|
+
logger.error('No Firebase projects found for the current account.');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const projectAnswer = await inquirer.prompt<ProjectPromptAnswer>([
|
|
101
|
+
{
|
|
102
|
+
type: 'list',
|
|
103
|
+
name: 'projectId',
|
|
104
|
+
message: 'Select your Firebase project:',
|
|
105
|
+
choices: projects.map((project) => ({
|
|
106
|
+
name: `${project.displayName} (${project.projectId})`,
|
|
107
|
+
value: project.projectId,
|
|
108
|
+
})),
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
projectId = projectAnswer.projectId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!projectId) {
|
|
116
|
+
logger.error('Firebase project selection failed.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const selectedProjectId = projectId;
|
|
121
|
+
|
|
122
|
+
// ── Show Android apps for the chosen project ─────────────────
|
|
123
|
+
let appId = options.appId;
|
|
124
|
+
|
|
125
|
+
if (!appId) {
|
|
126
|
+
logger.step('Fetching Android apps for the selected Firebase project...');
|
|
127
|
+
const appsOutput = runSilent('firebase', ['apps:list', 'ANDROID', '--project', selectedProjectId, '--json']);
|
|
128
|
+
const androidApps = parseFirebaseJson<FirebaseAndroidApp>(appsOutput, 'Android app list');
|
|
129
|
+
|
|
130
|
+
if (androidApps.length === 0) {
|
|
131
|
+
logger.error(`No Android apps found in Firebase project ${selectedProjectId}.`);
|
|
132
|
+
logger.info('Create an Android app in Firebase Console first, then rerun this command.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const appAnswer = await inquirer.prompt<AndroidAppPromptAnswer>([
|
|
137
|
+
{
|
|
138
|
+
type: 'list',
|
|
139
|
+
name: 'appId',
|
|
140
|
+
message: 'Select your Firebase Android app:',
|
|
141
|
+
choices: androidApps.map((app) => ({
|
|
142
|
+
name: `${app.displayName || 'Unnamed Android App'}${app.packageName ? ` (${app.packageName})` : ''} [${app.appId}]`,
|
|
143
|
+
value: app.appId,
|
|
144
|
+
})),
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
appId = appAnswer.appId;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!appId) {
|
|
152
|
+
logger.error('Firebase Android app selection failed.');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const selectedAppId = appId;
|
|
157
|
+
|
|
158
|
+
// ── Register SHA keys ────────────────────────────────────────
|
|
159
|
+
logger.step('Registering SHA-1 key with Firebase...');
|
|
160
|
+
runLive('firebase', [
|
|
161
|
+
'apps:android:sha:create',
|
|
162
|
+
'--project', selectedProjectId,
|
|
163
|
+
selectedAppId,
|
|
164
|
+
sha1,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
logger.step('Registering SHA-256 key with Firebase...');
|
|
168
|
+
runLive('firebase', [
|
|
169
|
+
'apps:android:sha:create',
|
|
170
|
+
'--project', selectedProjectId,
|
|
171
|
+
selectedAppId,
|
|
172
|
+
sha256,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
logger.success('Both SHA keys registered successfully!');
|
|
176
|
+
logger.info('Go to Firebase Console → Project Settings → Your Android App to verify.');
|
|
177
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { setupCommand } from './commands/setup';
|
|
5
|
+
import { loginCommand } from './commands/login';
|
|
6
|
+
import { shaCommand } from './commands/sha';
|
|
7
|
+
import { enableCommand } from './commands/enable';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('fire-pilot-rn')
|
|
13
|
+
.description('🔥 Elite Firebase setup orchestrator for React Native')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// ── Parent command: firebase ─────────────────────────────────
|
|
17
|
+
const firebase = program.command('firebase');
|
|
18
|
+
|
|
19
|
+
// firebase setup --full
|
|
20
|
+
firebase
|
|
21
|
+
.command('setup')
|
|
22
|
+
.description('The complete Pilot experience: Tools → Login → Project → Deps → SHA')
|
|
23
|
+
.option('--full', 'Run the complete setup including SHA key registration')
|
|
24
|
+
.action((options) => {
|
|
25
|
+
setupCommand(options.full ?? false).catch((err) => {
|
|
26
|
+
console.error('Error:', err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// firebase login
|
|
32
|
+
firebase
|
|
33
|
+
.command('login')
|
|
34
|
+
.description('Manage and switch between multiple Google accounts')
|
|
35
|
+
.action(() => {
|
|
36
|
+
loginCommand().catch((err) => {
|
|
37
|
+
console.error('Error:', err.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// firebase sha add
|
|
43
|
+
firebase
|
|
44
|
+
.command('sha')
|
|
45
|
+
.command('add')
|
|
46
|
+
.description('Automatically extract and add SHA keys to your Firebase project')
|
|
47
|
+
.action(() => {
|
|
48
|
+
shaCommand().catch((err) => {
|
|
49
|
+
console.error('Error:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// firebase enable <feature>
|
|
55
|
+
firebase
|
|
56
|
+
.command('enable [feature]')
|
|
57
|
+
.description('Quickly enable Firebase features (auth, firestore, storage, etc.)')
|
|
58
|
+
.action((feature?: string) => {
|
|
59
|
+
enableCommand(feature).catch((err) => {
|
|
60
|
+
console.error('Error:', err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program.parse(process.argv);
|