forge-remote 0.1.1 → 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/cloudflared-installer.js +164 -0
- package/src/desktop.js +17 -4
- package/src/google-auth.js +334 -0
- package/src/init.js +452 -273
- 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"));
|
|
240
|
+
|
|
241
|
+
// ── Step 3: Create Firebase project ─────────────────────────────────────
|
|
174
242
|
|
|
175
|
-
console.log(chalk.bold("\n📋 Step 3/
|
|
243
|
+
console.log(chalk.bold("\n📋 Step 3/11 — Creating Firebase project...\n"));
|
|
176
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,14 +272,7 @@ 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/13 — Creating Firebase project...\n"));
|
|
206
|
-
|
|
207
|
-
// Check if the project already exists by querying it directly.
|
|
208
|
-
// `firebase apps:list` succeeds (exit 0) if the project exists, fails otherwise.
|
|
275
|
+
// Check if the project already exists.
|
|
209
276
|
let projectExists = false;
|
|
210
277
|
try {
|
|
211
278
|
execSync(`firebase apps:list --project ${projectId}`, {
|
|
@@ -214,7 +281,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
214
281
|
});
|
|
215
282
|
projectExists = true;
|
|
216
283
|
} catch {
|
|
217
|
-
//
|
|
284
|
+
// Project doesn't exist or isn't accessible.
|
|
218
285
|
}
|
|
219
286
|
|
|
220
287
|
if (projectExists) {
|
|
@@ -223,12 +290,11 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
223
290
|
);
|
|
224
291
|
} else {
|
|
225
292
|
try {
|
|
226
|
-
|
|
293
|
+
runOrFail(
|
|
227
294
|
`firebase projects:create ${projectId} --display-name "Forge Remote"`,
|
|
228
295
|
"Failed to create Firebase project.",
|
|
229
296
|
);
|
|
230
297
|
console.log(chalk.green(` ✓ Firebase project created: ${projectId}`));
|
|
231
|
-
console.log(chalk.dim(` ${createResult.split("\n").pop()}`));
|
|
232
298
|
} catch (e) {
|
|
233
299
|
const msg = e.message || "";
|
|
234
300
|
if (
|
|
@@ -245,7 +311,8 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
245
311
|
console.error(chalk.red(` ✗ ${e.message}`));
|
|
246
312
|
console.error(
|
|
247
313
|
chalk.dim(
|
|
248
|
-
"\n If the project ID is taken by another account, re-run with
|
|
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",
|
|
249
316
|
),
|
|
250
317
|
);
|
|
251
318
|
process.exit(1);
|
|
@@ -253,59 +320,64 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
253
320
|
}
|
|
254
321
|
}
|
|
255
322
|
|
|
256
|
-
|
|
323
|
+
console.log(chalk.green(` ✓ Project ID: ${projectId}`));
|
|
324
|
+
|
|
325
|
+
// ── Step 4: Create web app + get SDK config ─────────────────────────────
|
|
257
326
|
|
|
258
|
-
console.log(
|
|
327
|
+
console.log(
|
|
328
|
+
chalk.bold("\n📋 Step 4/11 — Creating web app & fetching SDK config...\n"),
|
|
329
|
+
);
|
|
259
330
|
|
|
260
331
|
let appId = null;
|
|
261
332
|
|
|
333
|
+
// First, check if a web app already exists for this project.
|
|
262
334
|
try {
|
|
263
|
-
const
|
|
264
|
-
`firebase apps:
|
|
265
|
-
"Failed to
|
|
335
|
+
const appsList = runOrFail(
|
|
336
|
+
`firebase apps:list --project ${projectId}`,
|
|
337
|
+
"Failed to list apps.",
|
|
266
338
|
);
|
|
267
339
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} catch (e) {
|
|
276
|
-
const msg = e.message || "";
|
|
277
|
-
if (msg.includes("already exists") || msg.includes("ALREADY_EXISTS")) {
|
|
278
|
-
console.log(
|
|
279
|
-
chalk.yellow(" ⚠ Web app already exists — fetching existing app ID."),
|
|
280
|
-
);
|
|
281
|
-
} else {
|
|
282
|
-
console.error(chalk.red(` ✗ ${e.message}`));
|
|
283
|
-
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
|
+
}
|
|
284
347
|
}
|
|
348
|
+
} catch {
|
|
349
|
+
// Could not list apps — will try to create one.
|
|
285
350
|
}
|
|
286
351
|
|
|
287
|
-
|
|
288
|
-
|
|
352
|
+
if (appId) {
|
|
353
|
+
console.log(chalk.green(` ✓ Existing web app found: ${appId}`));
|
|
354
|
+
} else {
|
|
355
|
+
// No existing web app — create one.
|
|
289
356
|
try {
|
|
290
|
-
const
|
|
291
|
-
`firebase apps:
|
|
292
|
-
"Failed to
|
|
357
|
+
const createAppOutput = runOrFail(
|
|
358
|
+
`firebase apps:create web "Forge Remote Mobile" --project ${projectId}`,
|
|
359
|
+
"Failed to create web app.",
|
|
293
360
|
);
|
|
294
361
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (appLine.includes("WEB") || appLine.includes("web")) {
|
|
299
|
-
const idMatch = appLine.match(/1:\d+:web:[a-f0-9]+/);
|
|
300
|
-
if (idMatch) {
|
|
301
|
-
appId = idMatch[0];
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
362
|
+
const appIdMatch = createAppOutput.match(/(?:App ID|appId)[:\s]+(\S+)/i);
|
|
363
|
+
if (appIdMatch) {
|
|
364
|
+
appId = appIdMatch[1];
|
|
305
365
|
}
|
|
366
|
+
console.log(chalk.green(" ✓ Web app created"));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
306
371
|
|
|
307
|
-
|
|
308
|
-
|
|
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");
|
|
309
381
|
for (const appLine of lines) {
|
|
310
382
|
const idMatch = appLine.match(/1:\d+:web:[a-f0-9]+/);
|
|
311
383
|
if (idMatch) {
|
|
@@ -313,30 +385,25 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
313
385
|
break;
|
|
314
386
|
}
|
|
315
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);
|
|
316
392
|
}
|
|
317
|
-
} catch (e) {
|
|
318
|
-
console.error(chalk.red(` ✗ Could not determine web app ID.`));
|
|
319
|
-
console.error(chalk.dim(` ${e.message}\n`));
|
|
320
|
-
process.exit(1);
|
|
321
393
|
}
|
|
322
394
|
}
|
|
323
395
|
|
|
324
396
|
if (!appId) {
|
|
325
397
|
console.error(chalk.red(" ✗ Could not determine web app ID."));
|
|
326
398
|
console.error(
|
|
327
|
-
chalk.dim(
|
|
328
|
-
" Run `firebase apps:list --project " + projectId + "` to find it.\n",
|
|
329
|
-
),
|
|
399
|
+
chalk.dim(` Run: firebase apps:list --project ${projectId}\n`),
|
|
330
400
|
);
|
|
331
401
|
process.exit(1);
|
|
332
402
|
}
|
|
333
403
|
|
|
334
404
|
console.log(chalk.green(` ✓ App ID: ${appId}`));
|
|
335
405
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
console.log(chalk.bold("\n📋 Step 6/13 — Fetching SDK config...\n"));
|
|
339
|
-
|
|
406
|
+
// Get SDK config.
|
|
340
407
|
let sdkConfig = {};
|
|
341
408
|
|
|
342
409
|
try {
|
|
@@ -345,8 +412,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
345
412
|
"Failed to get SDK config.",
|
|
346
413
|
);
|
|
347
414
|
|
|
348
|
-
// Parse the config — output may be JSON
|
|
349
|
-
// Try JSON.parse first, fall back to regex.
|
|
415
|
+
// Parse the config — output may be JSON or JS object.
|
|
350
416
|
const jsonMatch = sdkOutput.match(/\{[\s\S]*\}/);
|
|
351
417
|
if (jsonMatch) {
|
|
352
418
|
try {
|
|
@@ -377,7 +443,6 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
377
443
|
}
|
|
378
444
|
}
|
|
379
445
|
|
|
380
|
-
// Validate we got the critical fields.
|
|
381
446
|
if (!sdkConfig.apiKey || !sdkConfig.projectId) {
|
|
382
447
|
throw new Error(
|
|
383
448
|
"Could not parse apiKey or projectId from SDK config output.",
|
|
@@ -403,229 +468,275 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
403
468
|
process.exit(1);
|
|
404
469
|
}
|
|
405
470
|
|
|
406
|
-
// ── Step
|
|
471
|
+
// ── Step 5: Enable Firestore ────────────────────────────────────────────
|
|
407
472
|
|
|
408
|
-
console.log(chalk.bold("\n📋 Step
|
|
473
|
+
console.log(chalk.bold("\n📋 Step 5/11 — Enabling Firestore...\n"));
|
|
409
474
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}
|
|
449
548
|
}
|
|
450
549
|
}
|
|
451
550
|
|
|
452
|
-
// ── Step
|
|
551
|
+
// ── Step 6: Enable Anonymous Authentication ─────────────────────────────
|
|
453
552
|
|
|
454
553
|
console.log(
|
|
455
|
-
chalk.bold("\n📋 Step
|
|
554
|
+
chalk.bold("\n📋 Step 6/11 — Enabling Anonymous Authentication...\n"),
|
|
456
555
|
);
|
|
457
556
|
|
|
458
557
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!gcToken) {
|
|
463
|
-
throw new Error("gcloud CLI not available or not authenticated.");
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const curlCmd = `curl -s -X PATCH "https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config?updateMask=signIn.anonymous.enabled" -H "Authorization: Bearer ${gcToken}" -H "Content-Type: application/json" -H "X-Goog-User-Project: ${projectId}" -d '{"signIn":{"anonymous":{"enabled":true}}}'`;
|
|
467
|
-
|
|
468
|
-
const authResult = run(curlCmd, { silent: true });
|
|
469
|
-
if (authResult && authResult.includes('"enabled": true')) {
|
|
470
|
-
console.log(chalk.green(" ✓ Anonymous Authentication enabled"));
|
|
471
|
-
} else if (authResult && authResult.includes("error")) {
|
|
472
|
-
throw new Error(authResult);
|
|
473
|
-
} else {
|
|
474
|
-
console.log(chalk.green(" ✓ Anonymous Authentication enabled"));
|
|
475
|
-
}
|
|
558
|
+
await enableAnonymousAuth(projectId);
|
|
559
|
+
console.log(chalk.green(" ✓ Anonymous Authentication enabled"));
|
|
560
|
+
results.anonymousAuthEnabled = true;
|
|
476
561
|
} catch (e) {
|
|
477
|
-
console.
|
|
478
|
-
|
|
479
|
-
);
|
|
480
|
-
console.
|
|
481
|
-
|
|
482
|
-
console.warn(
|
|
483
|
-
chalk.dim(
|
|
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(
|
|
484
567
|
` https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
485
568
|
),
|
|
486
569
|
);
|
|
487
|
-
console.
|
|
570
|
+
console.error(chalk.dim(" → Click Anonymous → Enable → Save\n"));
|
|
488
571
|
}
|
|
489
572
|
|
|
490
|
-
// ── Step
|
|
573
|
+
// ── Step 7: Install cloudflared tunnel binary ────────────────────────────
|
|
491
574
|
|
|
492
575
|
console.log(
|
|
493
|
-
chalk.bold("\n📋 Step
|
|
576
|
+
chalk.bold("\n📋 Step 7/11 — Installing cloudflared tunnel binary...\n"),
|
|
494
577
|
);
|
|
495
578
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
// Deploy rules using the directory containing firestore.rules.
|
|
506
|
-
runOrFail(
|
|
507
|
-
`firebase deploy --only firestore:rules --project ${projectId}`,
|
|
508
|
-
"Failed to deploy Firestore security rules.",
|
|
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"),
|
|
509
587
|
);
|
|
510
|
-
console.log(
|
|
511
|
-
|
|
512
|
-
console.warn(chalk.yellow(` ⚠ Could not deploy rules automatically.`));
|
|
513
|
-
console.warn(chalk.dim(` ${e.message}`));
|
|
514
|
-
console.warn(
|
|
515
|
-
chalk.dim(
|
|
516
|
-
" You can deploy them manually: firebase deploy --only firestore:rules --project " +
|
|
517
|
-
projectId,
|
|
518
|
-
),
|
|
588
|
+
console.log(
|
|
589
|
+
chalk.dim(" Live preview tunnels may fall back to localtunnel"),
|
|
519
590
|
);
|
|
520
591
|
}
|
|
521
|
-
}
|
|
522
|
-
console.log(
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.log(
|
|
594
|
+
chalk.yellow(` ⚠ Could not install cloudflared: ${e.message}`),
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.dim(
|
|
598
|
+
" Live preview will use localtunnel as fallback (may show splash page)",
|
|
599
|
+
),
|
|
600
|
+
);
|
|
523
601
|
console.log(
|
|
524
602
|
chalk.dim(
|
|
525
|
-
"
|
|
526
|
-
projectId,
|
|
603
|
+
" Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n",
|
|
527
604
|
),
|
|
528
605
|
);
|
|
529
606
|
}
|
|
530
607
|
|
|
531
|
-
// ── Step
|
|
608
|
+
// ── Step 8: Service account key ─────────────────────────────────────────
|
|
532
609
|
|
|
533
610
|
console.log(
|
|
534
|
-
chalk.bold("\n📋 Step
|
|
611
|
+
chalk.bold("\n📋 Step 8/11 — Setting up service account credentials...\n"),
|
|
535
612
|
);
|
|
536
613
|
|
|
537
614
|
const saKeyPath = join(forgeDir, "service-account.json");
|
|
538
615
|
|
|
539
|
-
// Ensure ~/.forge-remote directory exists.
|
|
540
|
-
if (!existsSync(forgeDir)) {
|
|
541
|
-
mkdirSync(forgeDir, { recursive: true });
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
let saReady = false;
|
|
545
|
-
|
|
546
616
|
if (existsSync(saKeyPath)) {
|
|
547
|
-
|
|
548
|
-
chalk.green(` ✓ Service account key already exists at ${saKeyPath}`),
|
|
549
|
-
);
|
|
550
|
-
saReady = true;
|
|
551
|
-
} else {
|
|
552
|
-
// Try to find the service account email for this project.
|
|
553
|
-
let saEmail = null;
|
|
554
|
-
|
|
617
|
+
// Verify the existing key is for the correct project.
|
|
555
618
|
try {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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;
|
|
562
634
|
}
|
|
563
635
|
} catch {
|
|
564
|
-
//
|
|
636
|
+
// Can't parse — regenerate.
|
|
637
|
+
console.log(chalk.yellow(" ⚠ Existing key is corrupt. Regenerating..."));
|
|
565
638
|
}
|
|
639
|
+
}
|
|
566
640
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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.",
|
|
578
653
|
);
|
|
579
|
-
saReady = true;
|
|
580
|
-
} catch (e) {
|
|
581
|
-
console.warn(chalk.yellow(" ⚠ Could not generate key automatically."));
|
|
582
|
-
console.warn(chalk.dim(` ${e.message}`));
|
|
583
654
|
}
|
|
584
|
-
|
|
655
|
+
console.log(chalk.dim(` Found: ${sa.email}`));
|
|
585
656
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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}`));
|
|
589
666
|
|
|
590
|
-
console.
|
|
591
|
-
|
|
592
|
-
);
|
|
593
|
-
console.
|
|
594
|
-
|
|
595
|
-
);
|
|
596
|
-
console.log(chalk.bold(` 1. Open: ${chalk.cyan(consoleUrl)}`));
|
|
597
|
-
console.log(chalk.bold(' 2. Click "Generate new private key"'));
|
|
598
|
-
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(
|
|
599
672
|
chalk.bold(` 3. Save the file to: ${chalk.cyan(saKeyPath)}\n`),
|
|
600
673
|
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
601
676
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.`,
|
|
606
692
|
);
|
|
607
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
|
+
);
|
|
608
708
|
}
|
|
609
709
|
|
|
610
|
-
// ── Step
|
|
710
|
+
// ── Step 9: Register desktop + verify connection ────────────────────────
|
|
611
711
|
|
|
612
712
|
console.log(
|
|
613
|
-
chalk.bold(
|
|
713
|
+
chalk.bold(
|
|
714
|
+
"\n📋 Step 10/11 — Registering desktop & verifying connection...\n",
|
|
715
|
+
),
|
|
614
716
|
);
|
|
615
717
|
|
|
616
|
-
|
|
718
|
+
// Use cached desktop ID if available (stable across network changes).
|
|
719
|
+
const desktopId = cachedConfig.desktopId || getDesktopId();
|
|
617
720
|
const platformName = getPlatformName();
|
|
618
721
|
const host = hostname();
|
|
619
722
|
|
|
620
|
-
if (
|
|
723
|
+
if (results.serviceAccountKeyReady && results.firestoreEnabled) {
|
|
621
724
|
try {
|
|
622
725
|
const serviceAccount = JSON.parse(readFileSync(saKeyPath, "utf-8"));
|
|
623
726
|
|
|
624
|
-
initializeApp({
|
|
625
|
-
credential: cert(serviceAccount),
|
|
626
|
-
});
|
|
627
|
-
|
|
727
|
+
initializeApp({ credential: cert(serviceAccount) });
|
|
628
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.
|
|
629
740
|
const desktopRef = db.collection("desktops").doc(desktopId);
|
|
630
741
|
const doc = await desktopRef.get();
|
|
631
742
|
|
|
@@ -638,7 +749,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
638
749
|
});
|
|
639
750
|
} else {
|
|
640
751
|
await desktopRef.set({
|
|
641
|
-
ownerUid:
|
|
752
|
+
ownerUid: null, // Will be set when the mobile app scans the QR code.
|
|
642
753
|
hostname: host,
|
|
643
754
|
platform: platformName,
|
|
644
755
|
status: "online",
|
|
@@ -651,18 +762,21 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
651
762
|
console.log(
|
|
652
763
|
chalk.green(` ✓ Desktop registered: ${desktopId} (${host})`),
|
|
653
764
|
);
|
|
765
|
+
results.desktopRegistered = true;
|
|
654
766
|
} catch (e) {
|
|
655
|
-
console.
|
|
656
|
-
chalk.
|
|
767
|
+
console.error(
|
|
768
|
+
chalk.red(` ✗ Connection verification failed: ${e.message}`),
|
|
657
769
|
);
|
|
658
|
-
console.
|
|
659
|
-
chalk.dim(
|
|
770
|
+
console.error(
|
|
771
|
+
chalk.dim(
|
|
772
|
+
" The relay will attempt to register on first `forge-remote start`.\n",
|
|
773
|
+
),
|
|
660
774
|
);
|
|
661
775
|
}
|
|
662
776
|
} else {
|
|
663
777
|
console.log(
|
|
664
778
|
chalk.yellow(
|
|
665
|
-
" ⚠ Skipping
|
|
779
|
+
" ⚠ Skipping (missing service account key or Firestore not ready).",
|
|
666
780
|
),
|
|
667
781
|
);
|
|
668
782
|
console.log(
|
|
@@ -670,9 +784,79 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
670
784
|
);
|
|
671
785
|
}
|
|
672
786
|
|
|
673
|
-
// ── Step
|
|
787
|
+
// ── Step 10: QR code or failure summary ─────────────────────────────────
|
|
788
|
+
|
|
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 ─────────────────
|
|
674
853
|
|
|
675
|
-
|
|
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
|
+
);
|
|
676
860
|
|
|
677
861
|
const qrPayload = {
|
|
678
862
|
v: 1,
|
|
@@ -692,10 +876,6 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
692
876
|
|
|
693
877
|
const qrString = JSON.stringify(qrPayload);
|
|
694
878
|
|
|
695
|
-
// ── Step 13: Display QR code ────────────────────────────────────────────
|
|
696
|
-
|
|
697
|
-
console.log(chalk.bold("\n📋 Step 13/13 — Displaying QR code...\n"));
|
|
698
|
-
|
|
699
879
|
console.log(
|
|
700
880
|
chalk.cyan(" Scan this QR code with the Forge Remote mobile app:\n"),
|
|
701
881
|
);
|
|
@@ -705,8 +885,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
705
885
|
console.log(chalk.dim("\n Raw pairing data (for manual entry):\n"));
|
|
706
886
|
console.log(chalk.dim(` ${qrString}\n`));
|
|
707
887
|
|
|
708
|
-
//
|
|
709
|
-
|
|
888
|
+
// Success banner.
|
|
710
889
|
const successLine = "═".repeat(50);
|
|
711
890
|
|
|
712
891
|
console.log(`\n${chalk.bold.green(successLine)}`);
|
|
@@ -714,10 +893,10 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
714
893
|
console.log(`${chalk.bold.green(successLine)}\n`);
|
|
715
894
|
|
|
716
895
|
console.log(chalk.bold(" What's next:\n"));
|
|
717
|
-
console.log(
|
|
896
|
+
console.log(" 1. Scan the QR code above with the Forge Remote app");
|
|
718
897
|
console.log(` 2. Start the relay: ${chalk.cyan("forge-remote start")}`);
|
|
719
898
|
console.log(
|
|
720
|
-
|
|
899
|
+
" 3. Open a Claude Code session and watch it appear in the app!\n",
|
|
721
900
|
);
|
|
722
901
|
|
|
723
902
|
console.log(chalk.dim(" Your Firebase project:"));
|
|
@@ -727,7 +906,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
727
906
|
|
|
728
907
|
console.log(
|
|
729
908
|
chalk.dim(
|
|
730
|
-
" Love Forge Remote? Support us: https://github.com/sponsors/
|
|
909
|
+
" Love Forge Remote? Support us: https://github.com/sponsors/IronForgeApps\n",
|
|
731
910
|
),
|
|
732
911
|
);
|
|
733
912
|
}
|