forge-remote 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +33 -14
  3. package/src/init.js +147 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-remote",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Desktop relay for Forge Remote — monitor and control Claude Code sessions from your phone",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
package/src/cli.js CHANGED
@@ -36,7 +36,7 @@ import * as log from "./logger.js";
36
36
  program
37
37
  .name("forge-remote")
38
38
  .description("Desktop relay for Forge Remote")
39
- .version("0.1.0");
39
+ .version("0.1.1");
40
40
 
41
41
  /**
42
42
  * Load Firebase web SDK config.
@@ -74,19 +74,38 @@ function loadSdkConfig() {
74
74
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
75
75
  );
76
76
 
77
- const parse = (field) => {
78
- const m = output.match(new RegExp(`${field}:\\s*"([^"]+)"`));
79
- return m ? m[1] : null;
80
- };
81
-
82
- const config = {
83
- apiKey: parse("apiKey"),
84
- authDomain: parse("authDomain"),
85
- projectId: parse("projectId"),
86
- storageBucket: parse("storageBucket"),
87
- messagingSenderId: parse("messagingSenderId"),
88
- appId: parse("appId"),
89
- };
77
+ // Output may be JSON (quoted keys) or JS object (unquoted keys).
78
+ let config = null;
79
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
80
+ if (jsonMatch) {
81
+ try {
82
+ const parsed = JSON.parse(jsonMatch[0]);
83
+ config = {
84
+ apiKey: parsed.apiKey,
85
+ authDomain: parsed.authDomain,
86
+ projectId: parsed.projectId,
87
+ storageBucket: parsed.storageBucket,
88
+ messagingSenderId: parsed.messagingSenderId,
89
+ appId: parsed.appId,
90
+ };
91
+ } catch {
92
+ // Not valid JSON — fall back to regex.
93
+ }
94
+ }
95
+ if (!config) {
96
+ const parse = (field) => {
97
+ const m = output.match(new RegExp(`"?${field}"?:\\s*"([^"]+)"`));
98
+ return m ? m[1] : null;
99
+ };
100
+ config = {
101
+ apiKey: parse("apiKey"),
102
+ authDomain: parse("authDomain"),
103
+ projectId: parse("projectId"),
104
+ storageBucket: parse("storageBucket"),
105
+ messagingSenderId: parse("messagingSenderId"),
106
+ appId: parse("appId"),
107
+ };
108
+ }
90
109
 
91
110
  if (!config.apiKey || !config.projectId) return null;
92
111
 
package/src/init.js CHANGED
@@ -127,7 +127,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
127
127
 
128
128
  // ── Step 1: Check Firebase CLI ──────────────────────────────────────────
129
129
 
130
- console.log(chalk.bold("\n📋 Step 1/12 — Checking Firebase CLI...\n"));
130
+ console.log(chalk.bold("\n📋 Step 1/13 — Checking Firebase CLI...\n"));
131
131
 
132
132
  const firebaseVersion = run("firebase --version", { silent: true });
133
133
  if (!firebaseVersion) {
@@ -142,7 +142,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
142
142
 
143
143
  // ── Step 2: Check Firebase login ────────────────────────────────────────
144
144
 
145
- console.log(chalk.bold("\n📋 Step 2/12 — Checking Firebase login...\n"));
145
+ console.log(chalk.bold("\n📋 Step 2/13 — Checking Firebase login...\n"));
146
146
 
147
147
  const loginList = run("firebase login:list", { silent: true });
148
148
  const isLoggedIn = loginList && !loginList.includes("No authorized accounts");
@@ -172,7 +172,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
172
172
 
173
173
  // ── Step 3: Generate project name ───────────────────────────────────────
174
174
 
175
- console.log(chalk.bold("\n📋 Step 3/12 — Generating project name...\n"));
175
+ console.log(chalk.bold("\n📋 Step 3/13 — Generating project name...\n"));
176
176
 
177
177
  const defaultProjectId = `forge-remote-${sanitizedHostname()}`;
178
178
  const projectId = overrideProjectId || defaultProjectId;
@@ -202,39 +202,60 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
202
202
 
203
203
  // ── Step 4: Create Firebase project ─────────────────────────────────────
204
204
 
205
- console.log(chalk.bold("\n📋 Step 4/12 — Creating Firebase project...\n"));
205
+ console.log(chalk.bold("\n📋 Step 4/13 — Creating Firebase project...\n"));
206
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.
209
+ let projectExists = false;
207
210
  try {
208
- const createResult = runOrFail(
209
- `firebase projects:create ${projectId} --display-name "Forge Remote"`,
210
- "Failed to create Firebase project.",
211
+ execSync(`firebase apps:list --project ${projectId}`, {
212
+ encoding: "utf-8",
213
+ stdio: "pipe",
214
+ });
215
+ projectExists = true;
216
+ } catch {
217
+ // Command failed — project doesn't exist or isn't accessible.
218
+ }
219
+
220
+ if (projectExists) {
221
+ console.log(
222
+ chalk.yellow(` ⚠ Project "${projectId}" already exists — reusing it.`),
211
223
  );
212
- console.log(chalk.green(` Firebase project created: ${projectId}`));
213
- console.log(chalk.dim(` ${createResult.split("\n").pop()}`));
214
- } catch (e) {
215
- const msg = e.message || "";
216
- if (
217
- msg.includes("already exists") ||
218
- msg.includes("ALREADY_EXISTS") ||
219
- msg.includes("already being used")
220
- ) {
221
- console.log(
222
- chalk.yellow(` ⚠ Project "${projectId}" already exists — reusing it.`),
223
- );
224
- } else {
225
- console.error(chalk.red(` ✗ ${e.message}`));
226
- console.error(
227
- chalk.dim(
228
- "\n If the project ID is taken, re-run with a different name.\n",
229
- ),
224
+ } else {
225
+ try {
226
+ const createResult = runOrFail(
227
+ `firebase projects:create ${projectId} --display-name "Forge Remote"`,
228
+ "Failed to create Firebase project.",
230
229
  );
231
- process.exit(1);
230
+ console.log(chalk.green(` ✓ Firebase project created: ${projectId}`));
231
+ console.log(chalk.dim(` ${createResult.split("\n").pop()}`));
232
+ } catch (e) {
233
+ const msg = e.message || "";
234
+ if (
235
+ msg.includes("already exists") ||
236
+ msg.includes("ALREADY_EXISTS") ||
237
+ msg.includes("already being used")
238
+ ) {
239
+ console.log(
240
+ chalk.yellow(
241
+ ` ⚠ Project "${projectId}" already exists — reusing it.`,
242
+ ),
243
+ );
244
+ } else {
245
+ console.error(chalk.red(` ✗ ${e.message}`));
246
+ console.error(
247
+ chalk.dim(
248
+ "\n If the project ID is taken by another account, re-run with a different name.\n",
249
+ ),
250
+ );
251
+ process.exit(1);
252
+ }
232
253
  }
233
254
  }
234
255
 
235
256
  // ── Step 5: Create web app ──────────────────────────────────────────────
236
257
 
237
- console.log(chalk.bold("\n📋 Step 5/12 — Creating web app...\n"));
258
+ console.log(chalk.bold("\n📋 Step 5/13 — Creating web app...\n"));
238
259
 
239
260
  let appId = null;
240
261
 
@@ -314,7 +335,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
314
335
 
315
336
  // ── Step 6: Get SDK config ──────────────────────────────────────────────
316
337
 
317
- console.log(chalk.bold("\n📋 Step 6/12 — Fetching SDK config...\n"));
338
+ console.log(chalk.bold("\n📋 Step 6/13 — Fetching SDK config...\n"));
318
339
 
319
340
  let sdkConfig = {};
320
341
 
@@ -324,22 +345,37 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
324
345
  "Failed to get SDK config.",
325
346
  );
326
347
 
327
- // Parse the config object from the output.
328
- // The output contains a JS object like: { apiKey: "...", authDomain: "...", ... }
329
- const parseField = (field) => {
330
- const regex = new RegExp(`${field}:\\s*"([^"]+)"`);
331
- const match = sdkOutput.match(regex);
332
- return match ? match[1] : null;
333
- };
334
-
335
- sdkConfig = {
336
- apiKey: parseField("apiKey"),
337
- authDomain: parseField("authDomain"),
338
- projectId: parseField("projectId"),
339
- storageBucket: parseField("storageBucket"),
340
- messagingSenderId: parseField("messagingSenderId"),
341
- appId: parseField("appId"),
342
- };
348
+ // Parse the config — output may be JSON (quoted keys) or JS object (unquoted keys).
349
+ // Try JSON.parse first, fall back to regex.
350
+ const jsonMatch = sdkOutput.match(/\{[\s\S]*\}/);
351
+ if (jsonMatch) {
352
+ try {
353
+ const parsed = JSON.parse(jsonMatch[0]);
354
+ sdkConfig = {
355
+ apiKey: parsed.apiKey || null,
356
+ authDomain: parsed.authDomain || null,
357
+ projectId: parsed.projectId || null,
358
+ storageBucket: parsed.storageBucket || null,
359
+ messagingSenderId: parsed.messagingSenderId || null,
360
+ appId: parsed.appId || null,
361
+ };
362
+ } catch {
363
+ // Not valid JSON — fall back to regex.
364
+ const parseField = (field) => {
365
+ const regex = new RegExp(`"?${field}"?:\\s*"([^"]+)"`);
366
+ const match = sdkOutput.match(regex);
367
+ return match ? match[1] : null;
368
+ };
369
+ sdkConfig = {
370
+ apiKey: parseField("apiKey"),
371
+ authDomain: parseField("authDomain"),
372
+ projectId: parseField("projectId"),
373
+ storageBucket: parseField("storageBucket"),
374
+ messagingSenderId: parseField("messagingSenderId"),
375
+ appId: parseField("appId"),
376
+ };
377
+ }
378
+ }
343
379
 
344
380
  // Validate we got the critical fields.
345
381
  if (!sdkConfig.apiKey || !sdkConfig.projectId) {
@@ -369,7 +405,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
369
405
 
370
406
  // ── Step 7: Enable Firestore ────────────────────────────────────────────
371
407
 
372
- console.log(chalk.bold("\n📋 Step 7/12 — Enabling Firestore...\n"));
408
+ console.log(chalk.bold("\n📋 Step 7/13 — Enabling Firestore...\n"));
373
409
 
374
410
  try {
375
411
  runOrFail(
@@ -385,24 +421,76 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
385
421
  msg.includes("database already exists")
386
422
  ) {
387
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
+ ),
438
+ );
388
439
  } else {
389
- console.error(chalk.red(` ${e.message}`));
390
- console.error(
440
+ console.warn(chalk.yellow(` ${e.message}`));
441
+ console.warn(
391
442
  chalk.dim("\n You may need to enable Firestore manually at:"),
392
443
  );
393
- console.error(
444
+ console.warn(
394
445
  chalk.dim(
395
446
  ` https://console.firebase.google.com/project/${projectId}/firestore\n`,
396
447
  ),
397
448
  );
398
- process.exit(1);
399
449
  }
400
450
  }
401
451
 
402
- // ── Step 8: Deploy security rules ───────────────────────────────────────
452
+ // ── Step 8: Enable Anonymous Auth ──────────────────────────────────────
453
+
454
+ console.log(
455
+ chalk.bold("\n📋 Step 8/13 — Enabling Anonymous Authentication...\n"),
456
+ );
457
+
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
+ }
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
+ }
476
+ } 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(
484
+ ` https://console.firebase.google.com/project/${projectId}/authentication/providers`,
485
+ ),
486
+ );
487
+ console.warn(chalk.dim(" → Click Anonymous → Enable → Save\n"));
488
+ }
489
+
490
+ // ── Step 9: Deploy security rules ───────────────────────────────────────
403
491
 
404
492
  console.log(
405
- chalk.bold("\n📋 Step 8/12 — Deploying Firestore security rules...\n"),
493
+ chalk.bold("\n📋 Step 9/13 — Deploying Firestore security rules...\n"),
406
494
  );
407
495
 
408
496
  // Walk up from the relay directory to find firestore.rules in the project root.
@@ -440,10 +528,10 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
440
528
  );
441
529
  }
442
530
 
443
- // ── Step 9: Set up service account credentials ──────────────────────────
531
+ // ── Step 10: Set up service account credentials ─────────────────────────
444
532
 
445
533
  console.log(
446
- chalk.bold("\n📋 Step 9/12 — Setting up service account credentials...\n"),
534
+ chalk.bold("\n📋 Step 10/13 — Setting up service account credentials...\n"),
447
535
  );
448
536
 
449
537
  const saKeyPath = join(forgeDir, "service-account.json");
@@ -519,10 +607,10 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
519
607
  }
520
608
  }
521
609
 
522
- // ── Step 10: Register desktop in Firestore ──────────────────────────────
610
+ // ── Step 11: Register desktop in Firestore ──────────────────────────────
523
611
 
524
612
  console.log(
525
- chalk.bold("\n📋 Step 10/12 — Registering desktop in Firestore...\n"),
613
+ chalk.bold("\n📋 Step 11/13 — Registering desktop in Firestore...\n"),
526
614
  );
527
615
 
528
616
  const desktopId = getDesktopId();
@@ -582,9 +670,9 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
582
670
  );
583
671
  }
584
672
 
585
- // ── Step 11: Build QR payload ───────────────────────────────────────────
673
+ // ── Step 12: Build QR payload ───────────────────────────────────────────
586
674
 
587
- console.log(chalk.bold("\n📋 Step 11/12 — Building pairing QR code...\n"));
675
+ console.log(chalk.bold("\n📋 Step 12/13 — Building pairing QR code...\n"));
588
676
 
589
677
  const qrPayload = {
590
678
  v: 1,
@@ -604,9 +692,9 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
604
692
 
605
693
  const qrString = JSON.stringify(qrPayload);
606
694
 
607
- // ── Step 12: Display QR code ────────────────────────────────────────────
695
+ // ── Step 13: Display QR code ────────────────────────────────────────────
608
696
 
609
- console.log(chalk.bold("\n📋 Step 12/12 — Displaying QR code...\n"));
697
+ console.log(chalk.bold("\n📋 Step 13/13 — Displaying QR code...\n"));
610
698
 
611
699
  console.log(
612
700
  chalk.cyan(" Scan this QR code with the Forge Remote mobile app:\n"),