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.
- package/package.json +1 -1
- package/src/cli.js +33 -14
- package/src/init.js +147 -59
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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/
|
|
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.
|
|
390
|
-
console.
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
531
|
+
// ── Step 10: Set up service account credentials ─────────────────────────
|
|
444
532
|
|
|
445
533
|
console.log(
|
|
446
|
-
chalk.bold("\n📋 Step
|
|
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
|
|
610
|
+
// ── Step 11: Register desktop in Firestore ──────────────────────────────
|
|
523
611
|
|
|
524
612
|
console.log(
|
|
525
|
-
chalk.bold("\n📋 Step
|
|
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
|
|
673
|
+
// ── Step 12: Build QR payload ───────────────────────────────────────────
|
|
586
674
|
|
|
587
|
-
console.log(chalk.bold("\n📋 Step
|
|
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
|
|
695
|
+
// ── Step 13: Display QR code ────────────────────────────────────────────
|
|
608
696
|
|
|
609
|
-
console.log(chalk.bold("\n📋 Step
|
|
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"),
|