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/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 and return stdout (trimmed).
20
- * Returns null if the command fails.
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(cmd, { silent = false } = {}) {
37
+ function run(args, { silent = false } = {}) {
23
38
  try {
24
- return execSync(cmd, {
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(cmd, errorMsg) {
53
+ function runOrFail(args, errorMsg) {
37
54
  try {
38
- return execSync(cmd, {
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() : e.message;
44
- throw new Error(`${errorMsg}\n Command: ${cmd}\n ${stderr}`);
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
- * Walk up from a starting directory to find a file.
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 findFileUpwards(filename, startDir) {
87
- let dir = startDir;
88
- while (true) {
89
- const candidate = join(dir, filename);
90
- if (existsSync(candidate)) return candidate;
91
- const parent = dirname(dir);
92
- if (parent === dir) return null; // reached filesystem root
93
- dir = parent;
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. Check Firebase login
103
- * 3. Generate / confirm project name
104
- * 4. Create Firebase project
105
- * 5. Create web app
106
- * 6. Get SDK config
107
- * 7. Enable Firestore
108
- * 8. Deploy security rules (if found)
109
- * 9. Set up service account credentials
110
- * 10. Register desktop in Firestore
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(" This command will set up a Firebase project for Forge Remote"),
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(" and generate a QR code to pair with the mobile app.\n"),
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/13 — Checking Firebase CLI...\n"));
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.yellow("Install it with:\n\n npm install -g firebase-tools\n"),
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: Check Firebase login ────────────────────────────────────────
209
+ // ── Step 2: Firebase login ──────────────────────────────────────────────
144
210
 
145
- console.log(chalk.bold("\n📋 Step 2/13 — Checking Firebase login...\n"));
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
- execSync("firebase login", { stdio: "inherit" });
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
- // ── Step 3: Generate project name ───────────────────────────────────────
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/13Generating project name...\n"));
253
+ console.log(chalk.bold("\n📋 Step 3/11Creating 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 = overrideProjectId || defaultProjectId;
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
- console.log(chalk.green(` ✓ Project ID: ${projectId}`));
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
- execSync(`firebase apps:list --project ${projectId}`, {
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
- // Command failed — project doesn't exist or isn't accessible.
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
- const createResult = runOrFail(
227
- `firebase projects:create ${projectId} --display-name "Forge Remote"`,
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 a different name.\n",
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
- // ── Step 5: Create web app ──────────────────────────────────────────────
339
+ console.log(chalk.green(` ✓ Project ID: ${projectId}`));
257
340
 
258
- console.log(chalk.bold("\n📋 Step 5/13 Creating web app...\n"));
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 createAppOutput = runOrFail(
264
- `firebase apps:create web "Forge Remote Mobile" --project ${projectId}`,
265
- "Failed to create web app.",
351
+ const appsList = runOrFail(
352
+ ["firebase", "apps:list", "--project", projectId],
353
+ "Failed to list apps.",
266
354
  );
267
355
 
268
- // Parse the app ID from the output.
269
- // The output typically contains "App ID: 1:xxxx:web:xxxx" or similar.
270
- const appIdMatch = createAppOutput.match(/(?:App ID|appId)[:\s]+(\S+)/i);
271
- if (appIdMatch) {
272
- appId = appIdMatch[1];
273
- }
274
- console.log(chalk.green(` ✓ Web app created`));
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
- // If we didn't parse the app ID from creation output, list apps to find it.
288
- if (!appId) {
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 appsList = runOrFail(
291
- `firebase apps:list --project ${projectId}`,
292
- "Failed to list apps.",
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
- // Look for a WEB app ID in the table output.
296
- const lines = appsList.split("\n");
297
- for (const appLine of lines) {
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
- if (!appId) {
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
- // ── Step 6: Get SDK config ──────────────────────────────────────────────
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
- `firebase apps:sdkconfig web ${appId} --project ${projectId}`,
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 (quoted keys) or JS object (unquoted keys).
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
- } catch (e) {
402
- console.error(chalk.red(` ✗ ${e.message}`));
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
- try {
411
- runOrFail(
412
- `firebase firestore:databases:create default --project ${projectId} --location=nam5`,
413
- "Failed to enable Firestore.",
414
- );
415
- console.log(chalk.green(" ✓ Firestore enabled (nam5 — US multi-region)"));
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
- } else {
440
- console.warn(chalk.yellow(` ${e.message}`));
441
- console.warn(
442
- chalk.dim("\n You may need to enable Firestore manually at:"),
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.warn(
506
+ console.log(
445
507
  chalk.dim(
446
- ` https://console.firebase.google.com/project/${projectId}/firestore\n`,
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 8: Enable Anonymous Auth ──────────────────────────────────────
517
+ // ── Step 5: Enable Firestore ────────────────────────────────────────────
453
518
 
454
- console.log(
455
- chalk.bold("\n📋 Step 8/13 — Enabling Anonymous Authentication...\n"),
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
- try {
459
- // Use the Identity Toolkit REST API to enable anonymous sign-in.
460
- // This requires a gcloud access token.
461
- const gcToken = run("gcloud auth print-access-token", { silent: true });
462
- if (!gcToken) {
463
- throw new Error("gcloud CLI not available or not authenticated.");
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
- 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}}}'`;
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
- 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"));
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.warn(
478
- chalk.yellow(" ⚠ Could not enable Anonymous Auth automatically."),
479
- );
480
- console.warn(chalk.dim(` ${e.message}`));
481
- console.warn(chalk.bold("\n Enable it manually:"));
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.warn(chalk.dim(" → Click Anonymous → Enable → Save\n"));
624
+ console.error(chalk.dim(" → Click Anonymous → Enable → Save\n"));
488
625
  }
489
626
 
490
- // ── Step 9: Deploy security rules ───────────────────────────────────────
627
+ // ── Step 7: Install cloudflared tunnel binary ────────────────────────────
491
628
 
492
629
  console.log(
493
- chalk.bold("\n📋 Step 9/13Deploying Firestore security rules...\n"),
630
+ chalk.bold("\n📋 Step 7/11Installing cloudflared tunnel binary...\n"),
494
631
  );
495
632
 
496
- // Walk up from the relay directory to find firestore.rules in the project root.
497
- const __dirname = dirname(fileURLToPath(import.meta.url));
498
- const rulesPath = findFileUpwards("firestore.rules", join(__dirname, ".."));
499
-
500
- if (rulesPath) {
501
- console.log(chalk.dim(` Found rules at: ${rulesPath}`));
502
- const rulesDir = dirname(rulesPath);
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(chalk.green(" ✓ Firestore security rules deployed"));
511
- } catch (e) {
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
- } else {
522
- console.log(chalk.yellow(" ⚠ No firestore.rules file found — skipping."));
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
- " You can add rules later and deploy with: firebase deploy --only firestore:rules --project " +
526
- projectId,
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 10: Set up service account credentials ─────────────────────────
662
+ // ── Step 8: Service account key ─────────────────────────────────────────
532
663
 
533
664
  console.log(
534
- chalk.bold("\n📋 Step 10/13 — Setting up service account credentials...\n"),
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
- console.log(
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 saListOutput = run(
557
- `gcloud iam service-accounts list --project ${projectId} --format="value(email)" --filter="email:firebase-adminsdk"`,
558
- { silent: true },
559
- );
560
- if (saListOutput) {
561
- saEmail = saListOutput.split("\n")[0].trim();
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
- // gcloud not available fall through to manual instructions.
690
+ // Can't parseregenerate.
691
+ console.log(chalk.yellow(" ⚠ Existing key is corrupt. Regenerating..."));
565
692
  }
693
+ }
566
694
 
567
- if (saEmail) {
568
- // gcloud is available — try to generate the key automatically.
569
- console.log(chalk.dim(` Found service account: ${saEmail}`));
570
-
571
- try {
572
- runOrFail(
573
- `gcloud iam service-accounts keys create "${saKeyPath}" --iam-account="${saEmail}" --project="${projectId}"`,
574
- "Failed to create service account key.",
575
- );
576
- console.log(
577
- chalk.green(` ✓ Service account key saved to ${saKeyPath}`),
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
- if (!saReady) {
587
- // Fallback: guide the user to download manually.
588
- const consoleUrl = `https://console.firebase.google.com/project/${projectId}/settings/serviceaccounts/adminsdk`;
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.log(
591
- chalk.yellow(" gcloud CLI not available or key generation failed."),
592
- );
593
- console.log(
594
- chalk.yellow(" Please download the service account key manually:\n"),
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
- console.log(
603
- chalk.yellow(
604
- ` Save the key to ${chalk.cyan(saKeyPath)} and re-run, or run ${chalk.cyan("forge-remote start")} once the key is in place.\n`,
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 11: Register desktop in Firestore ──────────────────────────────
764
+ // ── Step 9: Register desktop + verify connection ────────────────────────
611
765
 
612
766
  console.log(
613
- chalk.bold("\n📋 Step 11/13 — Registering desktop in Firestore...\n"),
767
+ chalk.bold(
768
+ "\n📋 Step 10/11 — Registering desktop & verifying connection...\n",
769
+ ),
614
770
  );
615
771
 
616
- const desktopId = getDesktopId();
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 (saReady) {
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: "", // Will be set during pairing.
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.warn(
656
- chalk.yellow(` Could not register desktop: ${e.message}`),
821
+ console.error(
822
+ chalk.red(` Connection verification failed: ${e.message}`),
657
823
  );
658
- console.warn(
659
- chalk.dim(" The relay will register on first `forge-remote start`.\n"),
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 desktop registration (no service account key).",
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 12: Build QR payload ───────────────────────────────────────────
841
+ // ── Step 10: QR code or failure summary ─────────────────────────────────
674
842
 
675
- console.log(chalk.bold("\n📋 Step 12/13Building pairing QR code...\n"));
843
+ console.log(chalk.bold("\n📋 Step 11/11Finalizing...\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
- // ── Success! ────────────────────────────────────────────────────────────
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(` 1. Scan the QR code above with the Forge Remote app`);
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
- ` 3. Open a Claude Code session and watch it appear in the app!\n`,
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/AirForgeApps\n",
965
+ " Love Forge Remote? Support us: https://github.com/sponsors/IronForgeApps\n",
731
966
  ),
732
967
  );
733
968
  }