forge-remote 0.1.0 → 0.1.2
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/firestore.rules +100 -0
- package/package.json +4 -2
- package/src/cli.js +33 -14
- package/src/cloudflared-installer.js +164 -0
- package/src/desktop.js +17 -4
- package/src/google-auth.js +334 -0
- package/src/init.js +528 -261
- package/src/session-manager.js +116 -14
- package/src/tunnel-manager.js +226 -33
package/src/init.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// AGPL-3.0 License — See LICENSE
|
|
5
5
|
|
|
6
6
|
import { execSync } from "child_process";
|
|
7
|
+
import { createInterface } from "readline";
|
|
7
8
|
import { hostname, platform, homedir } from "os";
|
|
8
9
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
9
10
|
import { join, dirname } from "path";
|
|
@@ -13,6 +14,19 @@ import { getFirestore, FieldValue } from "firebase-admin/firestore";
|
|
|
13
14
|
import qrcode from "qrcode-terminal";
|
|
14
15
|
import chalk from "chalk";
|
|
15
16
|
|
|
17
|
+
import {
|
|
18
|
+
readRefreshToken,
|
|
19
|
+
enableAnonymousAuth,
|
|
20
|
+
enableApi,
|
|
21
|
+
findFirebaseAdminSdkAccount,
|
|
22
|
+
createServiceAccountKey,
|
|
23
|
+
deployFirestoreRules,
|
|
24
|
+
} from "./google-auth.js";
|
|
25
|
+
import {
|
|
26
|
+
installCloudflared,
|
|
27
|
+
isCloudflaredWorking,
|
|
28
|
+
} from "./cloudflared-installer.js";
|
|
29
|
+
|
|
16
30
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
17
31
|
|
|
18
32
|
/**
|
|
@@ -81,16 +95,29 @@ function getDesktopId() {
|
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
/**
|
|
84
|
-
*
|
|
98
|
+
* Prompt the user and wait for Enter.
|
|
99
|
+
*/
|
|
100
|
+
function waitForEnter(message) {
|
|
101
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
rl.question(message, () => {
|
|
104
|
+
rl.close();
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Open a URL in the default browser (best-effort, no throw).
|
|
85
112
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
function openBrowser(url) {
|
|
114
|
+
const p = platform();
|
|
115
|
+
try {
|
|
116
|
+
if (p === "darwin") execSync(`open "${url}"`, { stdio: "pipe" });
|
|
117
|
+
else if (p === "win32") execSync(`start "" "${url}"`, { stdio: "pipe" });
|
|
118
|
+
else execSync(`xdg-open "${url}"`, { stdio: "pipe" });
|
|
119
|
+
} catch {
|
|
120
|
+
// Silently fail — URL is always printed for manual opening.
|
|
94
121
|
}
|
|
95
122
|
}
|
|
96
123
|
|
|
@@ -98,51 +125,80 @@ function findFileUpwards(filename, startDir) {
|
|
|
98
125
|
|
|
99
126
|
/**
|
|
100
127
|
* Run the full Forge Remote initialization:
|
|
128
|
+
*
|
|
101
129
|
* 1. Check Firebase CLI
|
|
102
|
-
* 2.
|
|
103
|
-
* 3.
|
|
104
|
-
* 4. Create
|
|
105
|
-
* 5.
|
|
106
|
-
* 6.
|
|
107
|
-
* 7.
|
|
108
|
-
* 8. Deploy security rules (
|
|
109
|
-
* 9.
|
|
110
|
-
* 10.
|
|
111
|
-
* 11. Build QR payload
|
|
112
|
-
* 12. Display QR code + success message
|
|
130
|
+
* 2. Firebase login (+ verify refresh token)
|
|
131
|
+
* 3. Create Firebase project
|
|
132
|
+
* 4. Create web app + get SDK config
|
|
133
|
+
* 5. Enable Firestore (with Blaze plan guidance)
|
|
134
|
+
* 6. Enable Anonymous Authentication (via REST API, no gcloud)
|
|
135
|
+
* 7. Service account key (via REST API, no gcloud)
|
|
136
|
+
* 8. Deploy Firestore security rules (via REST API, bundled rules file)
|
|
137
|
+
* 9. Register desktop + verify connection
|
|
138
|
+
* 10. Show QR code (only if all critical steps passed)
|
|
113
139
|
*/
|
|
114
140
|
export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
115
141
|
const forgeDir = join(homedir(), ".forge-remote");
|
|
142
|
+
const configPath = join(forgeDir, "config.json");
|
|
116
143
|
const line = "═".repeat(50);
|
|
117
144
|
|
|
145
|
+
// Ensure ~/.forge-remote directory exists.
|
|
146
|
+
if (!existsSync(forgeDir)) {
|
|
147
|
+
mkdirSync(forgeDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Load cached config from a previous init (project ID, desktop ID).
|
|
151
|
+
// This prevents hostname changes (e.g. different Wi-Fi networks on macOS)
|
|
152
|
+
// from creating a brand-new Firebase project each time.
|
|
153
|
+
let cachedConfig = {};
|
|
154
|
+
if (existsSync(configPath)) {
|
|
155
|
+
try {
|
|
156
|
+
cachedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
157
|
+
} catch {
|
|
158
|
+
// Corrupt file — will be overwritten at the end.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Track critical step results — QR code is gated on ALL passing.
|
|
163
|
+
const results = {
|
|
164
|
+
firestoreEnabled: false,
|
|
165
|
+
anonymousAuthEnabled: false,
|
|
166
|
+
serviceAccountKeyReady: false,
|
|
167
|
+
rulesDeployed: false,
|
|
168
|
+
desktopRegistered: false,
|
|
169
|
+
};
|
|
170
|
+
|
|
118
171
|
console.log(`\n${chalk.bold.cyan(line)}`);
|
|
119
172
|
console.log(chalk.bold.cyan(" ⚡ FORGE REMOTE INIT"));
|
|
120
173
|
console.log(`${chalk.bold.cyan(line)}\n`);
|
|
121
174
|
console.log(
|
|
122
|
-
chalk.dim(
|
|
175
|
+
chalk.dim(
|
|
176
|
+
" This will set up a Firebase project for Forge Remote and generate",
|
|
177
|
+
),
|
|
123
178
|
);
|
|
124
179
|
console.log(
|
|
125
|
-
chalk.dim("
|
|
180
|
+
chalk.dim(" a QR code to pair with the mobile app. Fully automated.\n"),
|
|
126
181
|
);
|
|
127
182
|
|
|
128
183
|
// ── Step 1: Check Firebase CLI ──────────────────────────────────────────
|
|
129
184
|
|
|
130
|
-
console.log(chalk.bold("\n📋 Step 1/
|
|
185
|
+
console.log(chalk.bold("\n📋 Step 1/11 — Checking Firebase CLI...\n"));
|
|
131
186
|
|
|
132
187
|
const firebaseVersion = run("firebase --version", { silent: true });
|
|
133
188
|
if (!firebaseVersion) {
|
|
134
|
-
console.error(chalk.red("Firebase CLI not found
|
|
189
|
+
console.error(chalk.red(" ✗ Firebase CLI not found.\n"));
|
|
190
|
+
console.error(chalk.bold(" Install it with:\n"));
|
|
191
|
+
console.error(chalk.cyan(" npm install -g firebase-tools\n"));
|
|
135
192
|
console.error(
|
|
136
|
-
chalk.
|
|
193
|
+
chalk.dim(" Then run this command again: forge-remote init\n"),
|
|
137
194
|
);
|
|
138
|
-
console.error(chalk.dim("Then run this command again: forge-remote init"));
|
|
139
195
|
process.exit(1);
|
|
140
196
|
}
|
|
141
197
|
console.log(chalk.green(` ✓ Firebase CLI ${firebaseVersion}`));
|
|
142
198
|
|
|
143
|
-
// ── Step 2:
|
|
199
|
+
// ── Step 2: Firebase login ──────────────────────────────────────────────
|
|
144
200
|
|
|
145
|
-
console.log(chalk.bold("\n📋 Step 2/
|
|
201
|
+
console.log(chalk.bold("\n📋 Step 2/11 — Checking Firebase login...\n"));
|
|
146
202
|
|
|
147
203
|
const loginList = run("firebase login:list", { silent: true });
|
|
148
204
|
const isLoggedIn = loginList && !loginList.includes("No authorized accounts");
|
|
@@ -170,14 +226,32 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
170
226
|
}
|
|
171
227
|
console.log(chalk.green(" ✓ Logged into Firebase"));
|
|
172
228
|
|
|
173
|
-
//
|
|
229
|
+
// Verify the refresh token is available (needed for REST API calls later).
|
|
230
|
+
const refreshToken = readRefreshToken();
|
|
231
|
+
if (!refreshToken) {
|
|
232
|
+
console.error(
|
|
233
|
+
chalk.red(" ✗ Could not find Firebase CLI refresh token after login.\n"),
|
|
234
|
+
);
|
|
235
|
+
console.error(chalk.dim(" Try logging out and back in:"));
|
|
236
|
+
console.error(chalk.dim(" firebase logout && firebase login\n"));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
console.log(chalk.green(" ✓ Auth token verified"));
|
|
174
240
|
|
|
175
|
-
|
|
241
|
+
// ── Step 3: Create Firebase project ─────────────────────────────────────
|
|
176
242
|
|
|
243
|
+
console.log(chalk.bold("\n📋 Step 3/11 — Creating Firebase project...\n"));
|
|
244
|
+
|
|
245
|
+
// Priority: 1) CLI override, 2) cached from previous init, 3) hostname-based.
|
|
177
246
|
const defaultProjectId = `forge-remote-${sanitizedHostname()}`;
|
|
178
|
-
const projectId =
|
|
247
|
+
const projectId =
|
|
248
|
+
overrideProjectId || cachedConfig.projectId || defaultProjectId;
|
|
179
249
|
console.log(chalk.dim(` Using project ID: ${projectId}`));
|
|
180
|
-
if (!overrideProjectId) {
|
|
250
|
+
if (cachedConfig.projectId && !overrideProjectId) {
|
|
251
|
+
console.log(
|
|
252
|
+
chalk.dim(` (Cached from previous init — stable across networks)`),
|
|
253
|
+
);
|
|
254
|
+
} else if (!overrideProjectId) {
|
|
181
255
|
console.log(
|
|
182
256
|
chalk.dim(` (Override with: forge-remote init --project-id <id>)`),
|
|
183
257
|
);
|
|
@@ -198,93 +272,112 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
198
272
|
process.exit(1);
|
|
199
273
|
}
|
|
200
274
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// ── Step 4: Create Firebase project ─────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
console.log(chalk.bold("\n📋 Step 4/12 — Creating Firebase project...\n"));
|
|
206
|
-
|
|
275
|
+
// Check if the project already exists.
|
|
276
|
+
let projectExists = false;
|
|
207
277
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
execSync(`firebase apps:list --project ${projectId}`, {
|
|
279
|
+
encoding: "utf-8",
|
|
280
|
+
stdio: "pipe",
|
|
281
|
+
});
|
|
282
|
+
projectExists = true;
|
|
283
|
+
} catch {
|
|
284
|
+
// Project doesn't exist or isn't accessible.
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (projectExists) {
|
|
288
|
+
console.log(
|
|
289
|
+
chalk.yellow(` ⚠ Project "${projectId}" already exists — reusing it.`),
|
|
211
290
|
);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
msg.includes("already exists") ||
|
|
218
|
-
msg.includes("ALREADY_EXISTS") ||
|
|
219
|
-
msg.includes("already being used")
|
|
220
|
-
) {
|
|
221
|
-
console.log(
|
|
222
|
-
chalk.yellow(` ⚠ Project "${projectId}" already exists — reusing it.`),
|
|
223
|
-
);
|
|
224
|
-
} else {
|
|
225
|
-
console.error(chalk.red(` ✗ ${e.message}`));
|
|
226
|
-
console.error(
|
|
227
|
-
chalk.dim(
|
|
228
|
-
"\n If the project ID is taken, re-run with a different name.\n",
|
|
229
|
-
),
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
runOrFail(
|
|
294
|
+
`firebase projects:create ${projectId} --display-name "Forge Remote"`,
|
|
295
|
+
"Failed to create Firebase project.",
|
|
230
296
|
);
|
|
231
|
-
|
|
297
|
+
console.log(chalk.green(` ✓ Firebase project created: ${projectId}`));
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const msg = e.message || "";
|
|
300
|
+
if (
|
|
301
|
+
msg.includes("already exists") ||
|
|
302
|
+
msg.includes("ALREADY_EXISTS") ||
|
|
303
|
+
msg.includes("already being used")
|
|
304
|
+
) {
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.yellow(
|
|
307
|
+
` ⚠ Project "${projectId}" already exists — reusing it.`,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
312
|
+
console.error(
|
|
313
|
+
chalk.dim(
|
|
314
|
+
"\n If the project ID is taken by another account, re-run with:\n" +
|
|
315
|
+
" forge-remote init --project-id <your-custom-id>\n",
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
232
320
|
}
|
|
233
321
|
}
|
|
234
322
|
|
|
235
|
-
|
|
323
|
+
console.log(chalk.green(` ✓ Project ID: ${projectId}`));
|
|
236
324
|
|
|
237
|
-
|
|
325
|
+
// ── Step 4: Create web app + get SDK config ─────────────────────────────
|
|
326
|
+
|
|
327
|
+
console.log(
|
|
328
|
+
chalk.bold("\n📋 Step 4/11 — Creating web app & fetching SDK config...\n"),
|
|
329
|
+
);
|
|
238
330
|
|
|
239
331
|
let appId = null;
|
|
240
332
|
|
|
333
|
+
// First, check if a web app already exists for this project.
|
|
241
334
|
try {
|
|
242
|
-
const
|
|
243
|
-
`firebase apps:
|
|
244
|
-
"Failed to
|
|
335
|
+
const appsList = runOrFail(
|
|
336
|
+
`firebase apps:list --project ${projectId}`,
|
|
337
|
+
"Failed to list apps.",
|
|
245
338
|
);
|
|
246
339
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
} catch (e) {
|
|
255
|
-
const msg = e.message || "";
|
|
256
|
-
if (msg.includes("already exists") || msg.includes("ALREADY_EXISTS")) {
|
|
257
|
-
console.log(
|
|
258
|
-
chalk.yellow(" ⚠ Web app already exists — fetching existing app ID."),
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
261
|
-
console.error(chalk.red(` ✗ ${e.message}`));
|
|
262
|
-
process.exit(1);
|
|
340
|
+
const lines = appsList.split("\n");
|
|
341
|
+
for (const appLine of lines) {
|
|
342
|
+
const idMatch = appLine.match(/1:\d+:web:[a-f0-9]+/);
|
|
343
|
+
if (idMatch) {
|
|
344
|
+
appId = idMatch[0];
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
263
347
|
}
|
|
348
|
+
} catch {
|
|
349
|
+
// Could not list apps — will try to create one.
|
|
264
350
|
}
|
|
265
351
|
|
|
266
|
-
|
|
267
|
-
|
|
352
|
+
if (appId) {
|
|
353
|
+
console.log(chalk.green(` ✓ Existing web app found: ${appId}`));
|
|
354
|
+
} else {
|
|
355
|
+
// No existing web app — create one.
|
|
268
356
|
try {
|
|
269
|
-
const
|
|
270
|
-
`firebase apps:
|
|
271
|
-
"Failed to
|
|
357
|
+
const createAppOutput = runOrFail(
|
|
358
|
+
`firebase apps:create web "Forge Remote Mobile" --project ${projectId}`,
|
|
359
|
+
"Failed to create web app.",
|
|
272
360
|
);
|
|
273
361
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (appLine.includes("WEB") || appLine.includes("web")) {
|
|
278
|
-
const idMatch = appLine.match(/1:\d+:web:[a-f0-9]+/);
|
|
279
|
-
if (idMatch) {
|
|
280
|
-
appId = idMatch[0];
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
362
|
+
const appIdMatch = createAppOutput.match(/(?:App ID|appId)[:\s]+(\S+)/i);
|
|
363
|
+
if (appIdMatch) {
|
|
364
|
+
appId = appIdMatch[1];
|
|
284
365
|
}
|
|
366
|
+
console.log(chalk.green(" ✓ Web app created"));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
285
371
|
|
|
286
|
-
|
|
287
|
-
|
|
372
|
+
// If we still don't have the app ID, list apps to find it.
|
|
373
|
+
if (!appId) {
|
|
374
|
+
try {
|
|
375
|
+
const appsList = runOrFail(
|
|
376
|
+
`firebase apps:list --project ${projectId}`,
|
|
377
|
+
"Failed to list apps.",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const lines = appsList.split("\n");
|
|
288
381
|
for (const appLine of lines) {
|
|
289
382
|
const idMatch = appLine.match(/1:\d+:web:[a-f0-9]+/);
|
|
290
383
|
if (idMatch) {
|
|
@@ -292,30 +385,25 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
292
385
|
break;
|
|
293
386
|
}
|
|
294
387
|
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.error(chalk.red(" ✗ Could not determine web app ID."));
|
|
390
|
+
console.error(chalk.dim(` ${e.message}\n`));
|
|
391
|
+
process.exit(1);
|
|
295
392
|
}
|
|
296
|
-
} catch (e) {
|
|
297
|
-
console.error(chalk.red(` ✗ Could not determine web app ID.`));
|
|
298
|
-
console.error(chalk.dim(` ${e.message}\n`));
|
|
299
|
-
process.exit(1);
|
|
300
393
|
}
|
|
301
394
|
}
|
|
302
395
|
|
|
303
396
|
if (!appId) {
|
|
304
397
|
console.error(chalk.red(" ✗ Could not determine web app ID."));
|
|
305
398
|
console.error(
|
|
306
|
-
chalk.dim(
|
|
307
|
-
" Run `firebase apps:list --project " + projectId + "` to find it.\n",
|
|
308
|
-
),
|
|
399
|
+
chalk.dim(` Run: firebase apps:list --project ${projectId}\n`),
|
|
309
400
|
);
|
|
310
401
|
process.exit(1);
|
|
311
402
|
}
|
|
312
403
|
|
|
313
404
|
console.log(chalk.green(` ✓ App ID: ${appId}`));
|
|
314
405
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
console.log(chalk.bold("\n📋 Step 6/12 — Fetching SDK config...\n"));
|
|
318
|
-
|
|
406
|
+
// Get SDK config.
|
|
319
407
|
let sdkConfig = {};
|
|
320
408
|
|
|
321
409
|
try {
|
|
@@ -324,24 +412,37 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
324
412
|
"Failed to get SDK config.",
|
|
325
413
|
);
|
|
326
414
|
|
|
327
|
-
// Parse the config
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
415
|
+
// Parse the config — output may be JSON or JS object.
|
|
416
|
+
const jsonMatch = sdkOutput.match(/\{[\s\S]*\}/);
|
|
417
|
+
if (jsonMatch) {
|
|
418
|
+
try {
|
|
419
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
420
|
+
sdkConfig = {
|
|
421
|
+
apiKey: parsed.apiKey || null,
|
|
422
|
+
authDomain: parsed.authDomain || null,
|
|
423
|
+
projectId: parsed.projectId || null,
|
|
424
|
+
storageBucket: parsed.storageBucket || null,
|
|
425
|
+
messagingSenderId: parsed.messagingSenderId || null,
|
|
426
|
+
appId: parsed.appId || null,
|
|
427
|
+
};
|
|
428
|
+
} catch {
|
|
429
|
+
// Not valid JSON — fall back to regex.
|
|
430
|
+
const parseField = (field) => {
|
|
431
|
+
const regex = new RegExp(`"?${field}"?:\\s*"([^"]+)"`);
|
|
432
|
+
const match = sdkOutput.match(regex);
|
|
433
|
+
return match ? match[1] : null;
|
|
434
|
+
};
|
|
435
|
+
sdkConfig = {
|
|
436
|
+
apiKey: parseField("apiKey"),
|
|
437
|
+
authDomain: parseField("authDomain"),
|
|
438
|
+
projectId: parseField("projectId"),
|
|
439
|
+
storageBucket: parseField("storageBucket"),
|
|
440
|
+
messagingSenderId: parseField("messagingSenderId"),
|
|
441
|
+
appId: parseField("appId"),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
345
446
|
if (!sdkConfig.apiKey || !sdkConfig.projectId) {
|
|
346
447
|
throw new Error(
|
|
347
448
|
"Could not parse apiKey or projectId from SDK config output.",
|
|
@@ -367,177 +468,275 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
367
468
|
process.exit(1);
|
|
368
469
|
}
|
|
369
470
|
|
|
370
|
-
// ── Step
|
|
471
|
+
// ── Step 5: Enable Firestore ────────────────────────────────────────────
|
|
371
472
|
|
|
372
|
-
console.log(chalk.bold("\n📋 Step
|
|
473
|
+
console.log(chalk.bold("\n📋 Step 5/11 — Enabling Firestore...\n"));
|
|
373
474
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
)
|
|
398
|
-
|
|
475
|
+
// First check if Firestore already exists by listing databases.
|
|
476
|
+
const existingDbs = run(
|
|
477
|
+
`firebase firestore:databases:list --project ${projectId}`,
|
|
478
|
+
{ silent: true },
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (existingDbs && existingDbs.includes("(default)")) {
|
|
482
|
+
console.log(chalk.yellow(" ⚠ Firestore already enabled — continuing."));
|
|
483
|
+
results.firestoreEnabled = true;
|
|
484
|
+
} else {
|
|
485
|
+
const maxFirestoreRetries = 2;
|
|
486
|
+
|
|
487
|
+
for (let attempt = 0; attempt < maxFirestoreRetries; attempt++) {
|
|
488
|
+
try {
|
|
489
|
+
runOrFail(
|
|
490
|
+
`firebase firestore:databases:create default --project ${projectId} --location=nam5`,
|
|
491
|
+
"Failed to enable Firestore.",
|
|
492
|
+
);
|
|
493
|
+
console.log(
|
|
494
|
+
chalk.green(" ✓ Firestore enabled (nam5 — US multi-region)"),
|
|
495
|
+
);
|
|
496
|
+
results.firestoreEnabled = true;
|
|
497
|
+
break;
|
|
498
|
+
} catch (e) {
|
|
499
|
+
const msg = e.message || "";
|
|
500
|
+
// Also capture stdout/stderr from the original exec error for detection.
|
|
501
|
+
const fullOutput = [msg, e.stdout || "", e.stderr || ""].join("\n");
|
|
502
|
+
|
|
503
|
+
if (
|
|
504
|
+
fullOutput.includes("already exists") ||
|
|
505
|
+
fullOutput.includes("ALREADY_EXISTS") ||
|
|
506
|
+
fullOutput.includes("database already exists")
|
|
507
|
+
) {
|
|
508
|
+
console.log(
|
|
509
|
+
chalk.yellow(" ⚠ Firestore already enabled — continuing."),
|
|
510
|
+
);
|
|
511
|
+
results.firestoreEnabled = true;
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (
|
|
516
|
+
(fullOutput.includes("billing") ||
|
|
517
|
+
fullOutput.includes("FAILED_PRECONDITION") ||
|
|
518
|
+
fullOutput.includes("403")) &&
|
|
519
|
+
attempt === 0
|
|
520
|
+
) {
|
|
521
|
+
// First attempt failed due to billing — guide the user through upgrading.
|
|
522
|
+
console.log(
|
|
523
|
+
chalk.yellow(
|
|
524
|
+
"\n Firestore requires the Blaze (pay-as-you-go) plan.",
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
console.log(
|
|
528
|
+
chalk.yellow(
|
|
529
|
+
" The Blaze plan has a generous free tier — you likely won't be charged.\n",
|
|
530
|
+
),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const billingUrl = `https://console.firebase.google.com/project/${projectId}/usage/details`;
|
|
534
|
+
console.log(chalk.bold(` Opening: ${chalk.cyan(billingUrl)}\n`));
|
|
535
|
+
openBrowser(billingUrl);
|
|
536
|
+
|
|
537
|
+
await waitForEnter(
|
|
538
|
+
chalk.bold(" Press Enter after upgrading to Blaze plan... "),
|
|
539
|
+
);
|
|
540
|
+
console.log(chalk.dim("\n Retrying Firestore creation...\n"));
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Final attempt failed.
|
|
545
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
399
548
|
}
|
|
400
549
|
}
|
|
401
550
|
|
|
402
|
-
// ── Step
|
|
551
|
+
// ── Step 6: Enable Anonymous Authentication ─────────────────────────────
|
|
403
552
|
|
|
404
553
|
console.log(
|
|
405
|
-
chalk.bold("\n📋 Step
|
|
554
|
+
chalk.bold("\n📋 Step 6/11 — Enabling Anonymous Authentication...\n"),
|
|
406
555
|
);
|
|
407
556
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
557
|
+
try {
|
|
558
|
+
await enableAnonymousAuth(projectId);
|
|
559
|
+
console.log(chalk.green(" ✓ Anonymous Authentication enabled"));
|
|
560
|
+
results.anonymousAuthEnabled = true;
|
|
561
|
+
} catch (e) {
|
|
562
|
+
console.error(chalk.red(` ✗ Could not enable Anonymous Auth.`));
|
|
563
|
+
console.error(chalk.dim(` ${e.message}\n`));
|
|
564
|
+
console.error(chalk.bold(" Enable it manually:"));
|
|
565
|
+
console.error(
|
|
566
|
+
chalk.cyan(
|
|
567
|
+
` https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
568
|
+
),
|
|
569
|
+
);
|
|
570
|
+
console.error(chalk.dim(" → Click Anonymous → Enable → Save\n"));
|
|
571
|
+
}
|
|
411
572
|
|
|
412
|
-
|
|
413
|
-
console.log(chalk.dim(` Found rules at: ${rulesPath}`));
|
|
414
|
-
const rulesDir = dirname(rulesPath);
|
|
573
|
+
// ── Step 7: Install cloudflared tunnel binary ────────────────────────────
|
|
415
574
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
575
|
+
console.log(
|
|
576
|
+
chalk.bold("\n📋 Step 7/11 — Installing cloudflared tunnel binary...\n"),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const cfPath = await installCloudflared();
|
|
581
|
+
console.log(chalk.green(` ✓ cloudflared installed at ${cfPath}`));
|
|
582
|
+
if (isCloudflaredWorking(cfPath)) {
|
|
583
|
+
console.log(chalk.green(" ✓ cloudflared verified and functional"));
|
|
584
|
+
} else {
|
|
585
|
+
console.log(
|
|
586
|
+
chalk.yellow(" ⚠ cloudflared installed but verification failed"),
|
|
421
587
|
);
|
|
422
|
-
console.log(
|
|
423
|
-
|
|
424
|
-
console.warn(chalk.yellow(` ⚠ Could not deploy rules automatically.`));
|
|
425
|
-
console.warn(chalk.dim(` ${e.message}`));
|
|
426
|
-
console.warn(
|
|
427
|
-
chalk.dim(
|
|
428
|
-
" You can deploy them manually: firebase deploy --only firestore:rules --project " +
|
|
429
|
-
projectId,
|
|
430
|
-
),
|
|
588
|
+
console.log(
|
|
589
|
+
chalk.dim(" Live preview tunnels may fall back to localtunnel"),
|
|
431
590
|
);
|
|
432
591
|
}
|
|
433
|
-
}
|
|
434
|
-
console.log(
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.log(
|
|
594
|
+
chalk.yellow(` ⚠ Could not install cloudflared: ${e.message}`),
|
|
595
|
+
);
|
|
435
596
|
console.log(
|
|
436
597
|
chalk.dim(
|
|
437
|
-
"
|
|
438
|
-
|
|
598
|
+
" Live preview will use localtunnel as fallback (may show splash page)",
|
|
599
|
+
),
|
|
600
|
+
);
|
|
601
|
+
console.log(
|
|
602
|
+
chalk.dim(
|
|
603
|
+
" Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n",
|
|
439
604
|
),
|
|
440
605
|
);
|
|
441
606
|
}
|
|
442
607
|
|
|
443
|
-
// ── Step
|
|
608
|
+
// ── Step 8: Service account key ─────────────────────────────────────────
|
|
444
609
|
|
|
445
610
|
console.log(
|
|
446
|
-
chalk.bold("\n📋 Step
|
|
611
|
+
chalk.bold("\n📋 Step 8/11 — Setting up service account credentials...\n"),
|
|
447
612
|
);
|
|
448
613
|
|
|
449
614
|
const saKeyPath = join(forgeDir, "service-account.json");
|
|
450
615
|
|
|
451
|
-
// Ensure ~/.forge-remote directory exists.
|
|
452
|
-
if (!existsSync(forgeDir)) {
|
|
453
|
-
mkdirSync(forgeDir, { recursive: true });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
let saReady = false;
|
|
457
|
-
|
|
458
616
|
if (existsSync(saKeyPath)) {
|
|
459
|
-
|
|
460
|
-
chalk.green(` ✓ Service account key already exists at ${saKeyPath}`),
|
|
461
|
-
);
|
|
462
|
-
saReady = true;
|
|
463
|
-
} else {
|
|
464
|
-
// Try to find the service account email for this project.
|
|
465
|
-
let saEmail = null;
|
|
466
|
-
|
|
617
|
+
// Verify the existing key is for the correct project.
|
|
467
618
|
try {
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
619
|
+
const existingKey = JSON.parse(readFileSync(saKeyPath, "utf-8"));
|
|
620
|
+
if (existingKey.project_id && existingKey.project_id !== projectId) {
|
|
621
|
+
console.log(
|
|
622
|
+
chalk.yellow(
|
|
623
|
+
` ⚠ Existing key is for project "${existingKey.project_id}", not "${projectId}".`,
|
|
624
|
+
),
|
|
625
|
+
);
|
|
626
|
+
console.log(
|
|
627
|
+
chalk.yellow(" Generating a new key for the correct project..."),
|
|
628
|
+
);
|
|
629
|
+
} else {
|
|
630
|
+
console.log(
|
|
631
|
+
chalk.green(` ✓ Service account key already exists at ${saKeyPath}`),
|
|
632
|
+
);
|
|
633
|
+
results.serviceAccountKeyReady = true;
|
|
474
634
|
}
|
|
475
635
|
} catch {
|
|
476
|
-
//
|
|
636
|
+
// Can't parse — regenerate.
|
|
637
|
+
console.log(chalk.yellow(" ⚠ Existing key is corrupt. Regenerating..."));
|
|
477
638
|
}
|
|
639
|
+
}
|
|
478
640
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
641
|
+
if (!results.serviceAccountKeyReady) {
|
|
642
|
+
try {
|
|
643
|
+
// Enable the IAM API (required to list/create service account keys).
|
|
644
|
+
console.log(chalk.dim(" Enabling IAM API (if not already enabled)..."));
|
|
645
|
+
await enableApi(projectId, "iam.googleapis.com");
|
|
646
|
+
console.log(chalk.green(" ✓ IAM API enabled"));
|
|
647
|
+
|
|
648
|
+
console.log(chalk.dim(" Finding Firebase Admin SDK service account..."));
|
|
649
|
+
const sa = await findFirebaseAdminSdkAccount(projectId);
|
|
650
|
+
if (!sa) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
"No firebase-adminsdk service account found for this project.",
|
|
490
653
|
);
|
|
491
|
-
saReady = true;
|
|
492
|
-
} catch (e) {
|
|
493
|
-
console.warn(chalk.yellow(" ⚠ Could not generate key automatically."));
|
|
494
|
-
console.warn(chalk.dim(` ${e.message}`));
|
|
495
654
|
}
|
|
496
|
-
|
|
655
|
+
console.log(chalk.dim(` Found: ${sa.email}`));
|
|
497
656
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
657
|
+
console.log(chalk.dim(" Generating private key..."));
|
|
658
|
+
const keyJson = await createServiceAccountKey(projectId, sa.email);
|
|
659
|
+
writeFileSync(saKeyPath, JSON.stringify(keyJson, null, 2), {
|
|
660
|
+
mode: 0o600,
|
|
661
|
+
});
|
|
662
|
+
console.log(chalk.green(` ✓ Service account key saved to ${saKeyPath}`));
|
|
663
|
+
results.serviceAccountKeyReady = true;
|
|
664
|
+
} catch (e) {
|
|
665
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
501
666
|
|
|
502
|
-
console.
|
|
503
|
-
|
|
504
|
-
);
|
|
505
|
-
console.
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
console.log(chalk.bold(` 1. Open: ${chalk.cyan(consoleUrl)}`));
|
|
509
|
-
console.log(chalk.bold(' 2. Click "Generate new private key"'));
|
|
510
|
-
console.log(
|
|
667
|
+
const consoleUrl = `https://console.firebase.google.com/project/${projectId}/settings/serviceaccounts/adminsdk`;
|
|
668
|
+
console.error(chalk.bold("\n Download the key manually:"));
|
|
669
|
+
console.error(chalk.bold(` 1. Open: ${chalk.cyan(consoleUrl)}`));
|
|
670
|
+
console.error(chalk.bold(' 2. Click "Generate new private key"'));
|
|
671
|
+
console.error(
|
|
511
672
|
chalk.bold(` 3. Save the file to: ${chalk.cyan(saKeyPath)}\n`),
|
|
512
673
|
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
513
676
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
677
|
+
// ── Step 8: Deploy Firestore security rules ─────────────────────────────
|
|
678
|
+
|
|
679
|
+
console.log(
|
|
680
|
+
chalk.bold("\n📋 Step 9/11 — Deploying Firestore security rules...\n"),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
// Read the bundled rules file from the package directory.
|
|
685
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
686
|
+
const packageRoot = join(__dirname, "..");
|
|
687
|
+
const rulesPath = join(packageRoot, "firestore.rules");
|
|
688
|
+
|
|
689
|
+
if (!existsSync(rulesPath)) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`Rules file not found at ${rulesPath}. Package may be corrupted.`,
|
|
518
692
|
);
|
|
519
693
|
}
|
|
694
|
+
|
|
695
|
+
const rulesContent = readFileSync(rulesPath, "utf-8");
|
|
696
|
+
console.log(chalk.dim(" Deploying rules via Firebase Rules API..."));
|
|
697
|
+
|
|
698
|
+
await deployFirestoreRules(projectId, rulesContent);
|
|
699
|
+
console.log(chalk.green(" ✓ Firestore security rules deployed"));
|
|
700
|
+
results.rulesDeployed = true;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
console.error(chalk.red(` ✗ Could not deploy rules: ${e.message}`));
|
|
703
|
+
console.error(
|
|
704
|
+
chalk.dim(
|
|
705
|
+
` You can deploy manually: firebase deploy --only firestore:rules --project ${projectId}\n`,
|
|
706
|
+
),
|
|
707
|
+
);
|
|
520
708
|
}
|
|
521
709
|
|
|
522
|
-
// ── Step
|
|
710
|
+
// ── Step 9: Register desktop + verify connection ────────────────────────
|
|
523
711
|
|
|
524
712
|
console.log(
|
|
525
|
-
chalk.bold(
|
|
713
|
+
chalk.bold(
|
|
714
|
+
"\n📋 Step 10/11 — Registering desktop & verifying connection...\n",
|
|
715
|
+
),
|
|
526
716
|
);
|
|
527
717
|
|
|
528
|
-
|
|
718
|
+
// Use cached desktop ID if available (stable across network changes).
|
|
719
|
+
const desktopId = cachedConfig.desktopId || getDesktopId();
|
|
529
720
|
const platformName = getPlatformName();
|
|
530
721
|
const host = hostname();
|
|
531
722
|
|
|
532
|
-
if (
|
|
723
|
+
if (results.serviceAccountKeyReady && results.firestoreEnabled) {
|
|
533
724
|
try {
|
|
534
725
|
const serviceAccount = JSON.parse(readFileSync(saKeyPath, "utf-8"));
|
|
535
726
|
|
|
536
|
-
initializeApp({
|
|
537
|
-
credential: cert(serviceAccount),
|
|
538
|
-
});
|
|
539
|
-
|
|
727
|
+
initializeApp({ credential: cert(serviceAccount) });
|
|
540
728
|
const db = getFirestore();
|
|
729
|
+
|
|
730
|
+
// Verification: write, read, delete a test document.
|
|
731
|
+
console.log(chalk.dim(" Verifying Firestore connection..."));
|
|
732
|
+
const testDocRef = db.collection("_forge_remote_init_test").doc();
|
|
733
|
+
await testDocRef.set({ test: true, ts: FieldValue.serverTimestamp() });
|
|
734
|
+
const snap = await testDocRef.get();
|
|
735
|
+
if (!snap.exists) throw new Error("Verification read failed.");
|
|
736
|
+
await testDocRef.delete();
|
|
737
|
+
console.log(chalk.green(" ✓ Firestore connection verified"));
|
|
738
|
+
|
|
739
|
+
// Register the desktop.
|
|
541
740
|
const desktopRef = db.collection("desktops").doc(desktopId);
|
|
542
741
|
const doc = await desktopRef.get();
|
|
543
742
|
|
|
@@ -550,7 +749,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
550
749
|
});
|
|
551
750
|
} else {
|
|
552
751
|
await desktopRef.set({
|
|
553
|
-
ownerUid:
|
|
752
|
+
ownerUid: null, // Will be set when the mobile app scans the QR code.
|
|
554
753
|
hostname: host,
|
|
555
754
|
platform: platformName,
|
|
556
755
|
status: "online",
|
|
@@ -563,18 +762,21 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
563
762
|
console.log(
|
|
564
763
|
chalk.green(` ✓ Desktop registered: ${desktopId} (${host})`),
|
|
565
764
|
);
|
|
765
|
+
results.desktopRegistered = true;
|
|
566
766
|
} catch (e) {
|
|
567
|
-
console.
|
|
568
|
-
chalk.
|
|
767
|
+
console.error(
|
|
768
|
+
chalk.red(` ✗ Connection verification failed: ${e.message}`),
|
|
569
769
|
);
|
|
570
|
-
console.
|
|
571
|
-
chalk.dim(
|
|
770
|
+
console.error(
|
|
771
|
+
chalk.dim(
|
|
772
|
+
" The relay will attempt to register on first `forge-remote start`.\n",
|
|
773
|
+
),
|
|
572
774
|
);
|
|
573
775
|
}
|
|
574
776
|
} else {
|
|
575
777
|
console.log(
|
|
576
778
|
chalk.yellow(
|
|
577
|
-
" ⚠ Skipping
|
|
779
|
+
" ⚠ Skipping (missing service account key or Firestore not ready).",
|
|
578
780
|
),
|
|
579
781
|
);
|
|
580
782
|
console.log(
|
|
@@ -582,9 +784,79 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
582
784
|
);
|
|
583
785
|
}
|
|
584
786
|
|
|
585
|
-
// ── Step
|
|
787
|
+
// ── Step 10: QR code or failure summary ─────────────────────────────────
|
|
586
788
|
|
|
587
|
-
console.log(chalk.bold("\n📋 Step 11/
|
|
789
|
+
console.log(chalk.bold("\n📋 Step 11/11 — Finalizing...\n"));
|
|
790
|
+
|
|
791
|
+
const CRITICAL_STEPS = [
|
|
792
|
+
"firestoreEnabled",
|
|
793
|
+
"anonymousAuthEnabled",
|
|
794
|
+
"serviceAccountKeyReady",
|
|
795
|
+
"rulesDeployed",
|
|
796
|
+
"desktopRegistered",
|
|
797
|
+
];
|
|
798
|
+
|
|
799
|
+
const stepLabels = {
|
|
800
|
+
firestoreEnabled: {
|
|
801
|
+
label: "Firestore database",
|
|
802
|
+
fix: `https://console.firebase.google.com/project/${projectId}/firestore`,
|
|
803
|
+
},
|
|
804
|
+
anonymousAuthEnabled: {
|
|
805
|
+
label: "Anonymous Authentication",
|
|
806
|
+
fix: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
807
|
+
},
|
|
808
|
+
serviceAccountKeyReady: {
|
|
809
|
+
label: "Service account key",
|
|
810
|
+
fix: `https://console.firebase.google.com/project/${projectId}/settings/serviceaccounts/adminsdk`,
|
|
811
|
+
},
|
|
812
|
+
rulesDeployed: {
|
|
813
|
+
label: "Firestore security rules",
|
|
814
|
+
fix: `firebase deploy --only firestore:rules --project ${projectId}`,
|
|
815
|
+
},
|
|
816
|
+
desktopRegistered: {
|
|
817
|
+
label: "Desktop registration",
|
|
818
|
+
fix: "Will auto-register on first `forge-remote start`",
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const failures = CRITICAL_STEPS.filter((step) => !results[step]);
|
|
823
|
+
|
|
824
|
+
if (failures.length > 0) {
|
|
825
|
+
// ── FAILURE — show summary and do NOT show QR code ──────────────────
|
|
826
|
+
|
|
827
|
+
const failLine = "═".repeat(50);
|
|
828
|
+
console.log(`\n${chalk.bold.red(failLine)}`);
|
|
829
|
+
console.log(
|
|
830
|
+
chalk.bold.red(" SETUP INCOMPLETE — Some steps need attention"),
|
|
831
|
+
);
|
|
832
|
+
console.log(`${chalk.bold.red(failLine)}\n`);
|
|
833
|
+
|
|
834
|
+
for (const step of CRITICAL_STEPS) {
|
|
835
|
+
const info = stepLabels[step];
|
|
836
|
+
if (results[step]) {
|
|
837
|
+
console.log(chalk.green(` ✓ ${info.label}`));
|
|
838
|
+
} else {
|
|
839
|
+
console.log(chalk.red(` ✗ ${info.label}`));
|
|
840
|
+
console.log(chalk.dim(` → ${info.fix}`));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
console.log(
|
|
845
|
+
chalk.bold(
|
|
846
|
+
`\n Fix the issues above, then re-run: ${chalk.cyan("forge-remote init")}\n`,
|
|
847
|
+
),
|
|
848
|
+
);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ── SUCCESS — all critical steps passed, show QR code ─────────────────
|
|
853
|
+
|
|
854
|
+
// Persist project ID and desktop ID so re-runs (even on different networks)
|
|
855
|
+
// always target the same Firebase project and desktop document.
|
|
856
|
+
writeFileSync(configPath, JSON.stringify({ projectId, desktopId }, null, 2));
|
|
857
|
+
console.log(
|
|
858
|
+
chalk.dim(` Config saved to ${configPath} (stable across networks)`),
|
|
859
|
+
);
|
|
588
860
|
|
|
589
861
|
const qrPayload = {
|
|
590
862
|
v: 1,
|
|
@@ -604,10 +876,6 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
604
876
|
|
|
605
877
|
const qrString = JSON.stringify(qrPayload);
|
|
606
878
|
|
|
607
|
-
// ── Step 12: Display QR code ────────────────────────────────────────────
|
|
608
|
-
|
|
609
|
-
console.log(chalk.bold("\n📋 Step 12/12 — Displaying QR code...\n"));
|
|
610
|
-
|
|
611
879
|
console.log(
|
|
612
880
|
chalk.cyan(" Scan this QR code with the Forge Remote mobile app:\n"),
|
|
613
881
|
);
|
|
@@ -617,8 +885,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
617
885
|
console.log(chalk.dim("\n Raw pairing data (for manual entry):\n"));
|
|
618
886
|
console.log(chalk.dim(` ${qrString}\n`));
|
|
619
887
|
|
|
620
|
-
//
|
|
621
|
-
|
|
888
|
+
// Success banner.
|
|
622
889
|
const successLine = "═".repeat(50);
|
|
623
890
|
|
|
624
891
|
console.log(`\n${chalk.bold.green(successLine)}`);
|
|
@@ -626,10 +893,10 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
626
893
|
console.log(`${chalk.bold.green(successLine)}\n`);
|
|
627
894
|
|
|
628
895
|
console.log(chalk.bold(" What's next:\n"));
|
|
629
|
-
console.log(
|
|
896
|
+
console.log(" 1. Scan the QR code above with the Forge Remote app");
|
|
630
897
|
console.log(` 2. Start the relay: ${chalk.cyan("forge-remote start")}`);
|
|
631
898
|
console.log(
|
|
632
|
-
|
|
899
|
+
" 3. Open a Claude Code session and watch it appear in the app!\n",
|
|
633
900
|
);
|
|
634
901
|
|
|
635
902
|
console.log(chalk.dim(" Your Firebase project:"));
|
|
@@ -639,7 +906,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
639
906
|
|
|
640
907
|
console.log(
|
|
641
908
|
chalk.dim(
|
|
642
|
-
" Love Forge Remote? Support us: https://github.com/sponsors/
|
|
909
|
+
" Love Forge Remote? Support us: https://github.com/sponsors/IronForgeApps\n",
|
|
643
910
|
),
|
|
644
911
|
);
|
|
645
912
|
}
|