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