forge-remote 0.1.3 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-remote",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Desktop relay for Forge Remote — monitor and control Claude Code sessions from your phone",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -80,6 +80,47 @@ export async function getAccessToken() {
80
80
  return data.access_token;
81
81
  }
82
82
 
83
+ // ─── Web App SDK Config ─────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Fetch the Firebase web app SDK config via REST API.
87
+ * This bypasses the `firebase apps:sdkconfig` CLI command, which sometimes
88
+ * returns 403 on newly created projects.
89
+ *
90
+ * Returns an object with apiKey, authDomain, projectId, storageBucket,
91
+ * messagingSenderId, and appId — or throws on failure.
92
+ */
93
+ export async function getWebAppConfig(projectId, appId) {
94
+ const token = await getAccessToken();
95
+
96
+ const response = await fetch(
97
+ `https://firebase.googleapis.com/v1beta1/projects/${projectId}/webApps/${appId}/config`,
98
+ {
99
+ headers: {
100
+ Authorization: `Bearer ${token}`,
101
+ "X-Goog-User-Project": projectId,
102
+ },
103
+ },
104
+ );
105
+
106
+ if (!response.ok) {
107
+ const err = await response.json().catch(() => ({}));
108
+ throw new Error(
109
+ `Failed to fetch web app config: ${err.error?.message || response.statusText}`,
110
+ );
111
+ }
112
+
113
+ const data = await response.json();
114
+ return {
115
+ apiKey: data.apiKey || null,
116
+ authDomain: data.authDomain || null,
117
+ projectId: data.projectId || null,
118
+ storageBucket: data.storageBucket || null,
119
+ messagingSenderId: data.messagingSenderId || null,
120
+ appId: data.appId || null,
121
+ };
122
+ }
123
+
83
124
  // ─── Anonymous Authentication ───────────────────────────────────────────────
84
125
 
85
126
  /**
package/src/init.js CHANGED
@@ -15,6 +15,7 @@ import chalk from "chalk";
15
15
 
16
16
  import {
17
17
  readRefreshToken,
18
+ getWebAppConfig,
18
19
  enableAnonymousAuth,
19
20
  enableApi,
20
21
  findFirebaseAdminSdkAccount,
@@ -426,50 +427,58 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
426
427
 
427
428
  console.log(chalk.green(` ✓ App ID: ${appId}`));
428
429
 
429
- // Get SDK config.
430
+ // Get SDK config via REST API (bypasses flaky `firebase apps:sdkconfig` CLI).
430
431
  let sdkConfig = {};
431
432
 
432
433
  try {
433
- const sdkOutput = runOrFail(
434
- ["firebase", "apps:sdkconfig", "web", appId, "--project", projectId],
435
- "Failed to get SDK config.",
436
- );
434
+ // Primary method: REST API with retry for newly created apps.
435
+ const maxSdkRetries = 4;
436
+ const retryDelayMs = 5000; // 5 seconds between retries.
437
437
 
438
- // Parse the config output may be JSON or JS object.
439
- const jsonMatch = sdkOutput.match(/\{[\s\S]*\}/);
440
- if (jsonMatch) {
438
+ for (let attempt = 1; attempt <= maxSdkRetries; attempt++) {
441
439
  try {
442
- const parsed = JSON.parse(jsonMatch[0]);
443
- sdkConfig = {
444
- apiKey: parsed.apiKey || null,
445
- authDomain: parsed.authDomain || null,
446
- projectId: parsed.projectId || null,
447
- storageBucket: parsed.storageBucket || null,
448
- messagingSenderId: parsed.messagingSenderId || null,
449
- appId: parsed.appId || null,
450
- };
451
- } catch {
452
- // Not valid JSON fall back to regex.
453
- const parseField = (field) => {
454
- const regex = new RegExp(`"?${field}"?:\\s*"([^"]+)"`);
455
- const match = sdkOutput.match(regex);
456
- return match ? match[1] : null;
457
- };
458
- sdkConfig = {
459
- apiKey: parseField("apiKey"),
460
- authDomain: parseField("authDomain"),
461
- projectId: parseField("projectId"),
462
- storageBucket: parseField("storageBucket"),
463
- messagingSenderId: parseField("messagingSenderId"),
464
- appId: parseField("appId"),
465
- };
440
+ sdkConfig = await getWebAppConfig(projectId, appId);
441
+ break; // Success — exit retry loop.
442
+ } catch (e) {
443
+ if (attempt < maxSdkRetries) {
444
+ console.log(
445
+ chalk.yellow(
446
+ ` ⚠ SDK config not available yet (attempt ${attempt}/${maxSdkRetries}). ` +
447
+ `Retrying in ${retryDelayMs / 1000}s...`,
448
+ ),
449
+ );
450
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
451
+ } else {
452
+ throw new Error(
453
+ `Could not retrieve SDK config after ${maxSdkRetries} attempts.\n` +
454
+ ` Last error: ${e.message}\n\n` +
455
+ chalk.bold(" Try these steps:\n\n") +
456
+ chalk.cyan(
457
+ ` 1. Wait 30 seconds, then re-run: forge-remote init\n`,
458
+ ) +
459
+ chalk.cyan(
460
+ ` 2. Or get the config from the Firebase Console:\n`,
461
+ ) +
462
+ chalk.dim(
463
+ ` https://console.firebase.google.com/project/${projectId}/settings/general\n`,
464
+ ) +
465
+ chalk.dim(
466
+ ` Scroll to "Your apps" → Web app → Config snippet\n`,
467
+ ) +
468
+ chalk.dim(` Save it to: ~/.forge-remote/sdk-config.json\n`) +
469
+ chalk.dim(
470
+ ` Format: { "apiKey": "...", "authDomain": "...", "projectId": "...",\n`,
471
+ ) +
472
+ chalk.dim(
473
+ ` "storageBucket": "...", "messagingSenderId": "...", "appId": "..." }\n`,
474
+ ),
475
+ );
476
+ }
466
477
  }
467
478
  }
468
479
 
469
480
  if (!sdkConfig.apiKey || !sdkConfig.projectId) {
470
- throw new Error(
471
- "Could not parse apiKey or projectId from SDK config output.",
472
- );
481
+ throw new Error("SDK config is missing apiKey or projectId.");
473
482
  }
474
483
 
475
484
  console.log(chalk.green(" ✓ SDK config retrieved"));
@@ -279,9 +279,14 @@ async function handleDesktopCommand(desktopId, commandDoc) {
279
279
 
280
280
  try {
281
281
  switch (data.type) {
282
- case "start_session":
283
- await startNewSession(desktopId, data.payload);
284
- break;
282
+ case "start_session": {
283
+ const sessionId = await startNewSession(desktopId, data.payload);
284
+ await cmdRef.update({
285
+ status: "completed",
286
+ result: { sessionId },
287
+ });
288
+ return; // Skip generic completed update below
289
+ }
285
290
  case "rescan_projects": {
286
291
  const { scanProjects } = await import("./project-scanner.js");
287
292
  const { updateProjects } = await import("./desktop.js");