@doubling/compound-sync 1.12.4 → 1.12.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/sync.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  /**
4
3
  * Bidirectional sync between Firestore files and local markdown files.
5
4
  * Uses Firebase client SDK with browser-based auth — no gcloud needed.
@@ -15,7 +14,6 @@
15
14
  * node sync.js --version
16
15
  * node sync.js --help
17
16
  */
18
-
19
17
  // DOU-208: Node 22+ ships its own CA bundle and does not consult the
20
18
  // macOS system trust store. Without NODE_EXTRA_CA_CERTS pointing at
21
19
  // /etc/ssl/cert.pem, fetch() to securetoken.googleapis.com fails
@@ -29,39 +27,30 @@
29
27
  // or a wrapper explicitly chose a different bundle).
30
28
  import { spawnSync as __dou208_spawnSync } from 'node:child_process';
31
29
  import { existsSync as __dou208_existsSync } from 'node:fs';
32
- if (!process.env.NODE_EXTRA_CA_CERTS) {
33
- const __dou208_candidates = process.platform === 'darwin'
34
- ? ['/etc/ssl/cert.pem']
35
- : process.platform === 'linux'
36
- ? ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt']
37
- : [];
38
- for (const __dou208_candidate of __dou208_candidates) {
39
- if (__dou208_existsSync(__dou208_candidate)) {
40
- const __dou208_result = __dou208_spawnSync(
41
- process.execPath,
42
- process.argv.slice(1),
43
- {
44
- stdio: 'inherit',
45
- env: { ...process.env, NODE_EXTRA_CA_CERTS: __dou208_candidate },
46
- },
47
- );
48
- process.exit(__dou208_result.status ?? 1);
30
+ if (!process.env['NODE_EXTRA_CA_CERTS']) {
31
+ const __dou208_candidates = process.platform === 'darwin'
32
+ ? ['/etc/ssl/cert.pem']
33
+ : process.platform === 'linux'
34
+ ? ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt']
35
+ : [];
36
+ for (const __dou208_candidate of __dou208_candidates) {
37
+ if (__dou208_existsSync(__dou208_candidate)) {
38
+ const __dou208_result = __dou208_spawnSync(process.execPath, process.argv.slice(1), {
39
+ stdio: 'inherit',
40
+ env: { ...process.env, NODE_EXTRA_CA_CERTS: __dou208_candidate },
41
+ });
42
+ process.exit(__dou208_result.status ?? 1);
43
+ }
49
44
  }
50
- }
51
- // No candidate found (Windows, unusual Linux distro): fall through
52
- // and let Node's bundled CA bundle handle TLS. The user can still
53
- // set NODE_EXTRA_CA_CERTS manually for corporate cert chains.
45
+ // No candidate found (Windows, unusual Linux distro): fall through
46
+ // and let Node's bundled CA bundle handle TLS. The user can still
47
+ // set NODE_EXTRA_CA_CERTS manually for corporate cert chains.
54
48
  }
55
-
56
49
  import { initializeApp } from 'firebase/app';
57
50
  import { getAuth, initializeAuth, signInWithEmailAndPassword, signInWithCredential, GoogleAuthProvider } from 'firebase/auth';
58
- import {
59
- getFirestore, collection, doc, getDocs, getDoc, setDoc, addDoc, updateDoc, deleteDoc,
60
- query, where, onSnapshot
61
- } from 'firebase/firestore';
51
+ import { getFirestore, collection, doc, getDocs, getDoc, query, where } from 'firebase/firestore';
62
52
  import { getStorage } from 'firebase/storage';
63
53
  import { initializeAppCheck, CustomProvider } from 'firebase/app-check';
64
- import chokidar from 'chokidar';
65
54
  import fs from 'fs';
66
55
  import path from 'path';
67
56
  import readline from 'readline';
@@ -69,99 +58,69 @@ import os from 'os';
69
58
  import http from 'http';
70
59
  import { exec } from 'child_process';
71
60
  import { fileURLToPath } from 'url';
72
- import {
73
- pushFileToCloud,
74
- deleteFileFromCloud,
75
- pushFolderToCloud,
76
- deleteFolderFromCloud,
77
- readBlob,
78
- readBlobAsText,
79
- extFromPath,
80
- isTextMimeType,
81
- mimeTypeFromExt,
82
- } from './files.js';
83
-
84
61
  // Max file size enforced before upload. Mirrored in storage.rules.
85
62
  const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
86
63
  import { resolveConfigPath, loadConfig, saveConfig } from './config.js';
87
64
  import { makeFilePersistence, resolveAuthFilePath } from './auth-persistence.js';
88
65
  import { readPersistedRefreshToken, exchangeRefreshTokenForIdToken } from './pre-mint-app-check.js';
89
66
  import { setupAuthWithPreMint } from './setup-auth-with-pre-mint.js';
90
- import {
91
- scopeFromLocalPath,
92
- teamFolderName,
93
- PRIVATE_FOLDER,
94
- SHARED_WITH_ME_FOLDER,
95
- SHARED_BY_ME_FOLDER,
96
- } from './paths.js';
97
- import { TeamRegistry } from './team-registry.js';
98
67
  import { startOrgSync } from './org-sync.js';
99
-
100
68
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
101
-
102
69
  // --- Firebase configs (same as the web app) ---
103
-
104
70
  const firebaseConfigs = {
105
- 'doubling-compound-sandbox': {
106
- apiKey: "AIzaSyBnskQNh--bRaRN7tsVFbb01oll8OSdVH8",
107
- authDomain: "doubling-compound-sandbox.firebaseapp.com",
108
- projectId: "doubling-compound-sandbox",
109
- storageBucket: "doubling-compound-sandbox.firebasestorage.app",
110
- messagingSenderId: "83118259611",
111
- appId: "1:83118259611:web:cf9cda61c9ecdc161b353e"
112
- },
113
- 'doubling-compound-dev': {
114
- apiKey: "AIzaSyBPKJqfy77_qmP_x9zOVzoHKuY8PHFCUm0",
115
- authDomain: "doubling-compound-dev.firebaseapp.com",
116
- projectId: "doubling-compound-dev",
117
- storageBucket: "doubling-compound-dev.firebasestorage.app",
118
- messagingSenderId: "967509355891",
119
- appId: "1:967509355891:web:c253ebdb8205d3ef012906"
120
- },
121
- 'doubling-compound-prod': {
122
- apiKey: "AIzaSyDvnPV5dLxzubPE8pwq_S80gMsF8E97LaI",
123
- authDomain: "compound.doubling.io",
124
- projectId: "doubling-compound-prod",
125
- storageBucket: "doubling-compound-prod.firebasestorage.app",
126
- messagingSenderId: "831648925388",
127
- appId: "1:831648925388:web:87550a55f807f2a2a1a469"
128
- }
71
+ 'doubling-compound-sandbox': {
72
+ apiKey: "AIzaSyBnskQNh--bRaRN7tsVFbb01oll8OSdVH8",
73
+ authDomain: "doubling-compound-sandbox.firebaseapp.com",
74
+ projectId: "doubling-compound-sandbox",
75
+ storageBucket: "doubling-compound-sandbox.firebasestorage.app",
76
+ messagingSenderId: "83118259611",
77
+ appId: "1:83118259611:web:cf9cda61c9ecdc161b353e"
78
+ },
79
+ 'doubling-compound-dev': {
80
+ apiKey: "AIzaSyBPKJqfy77_qmP_x9zOVzoHKuY8PHFCUm0",
81
+ authDomain: "doubling-compound-dev.firebaseapp.com",
82
+ projectId: "doubling-compound-dev",
83
+ storageBucket: "doubling-compound-dev.firebasestorage.app",
84
+ messagingSenderId: "967509355891",
85
+ appId: "1:967509355891:web:c253ebdb8205d3ef012906"
86
+ },
87
+ 'doubling-compound-prod': {
88
+ apiKey: "AIzaSyDvnPV5dLxzubPE8pwq_S80gMsF8E97LaI",
89
+ authDomain: "compound.doubling.io",
90
+ projectId: "doubling-compound-prod",
91
+ storageBucket: "doubling-compound-prod.firebasestorage.app",
92
+ messagingSenderId: "831648925388",
93
+ appId: "1:831648925388:web:87550a55f807f2a2a1a469"
94
+ }
129
95
  };
130
-
131
96
  // App Check token endpoints per environment
132
97
  const appCheckEndpoints = {
133
- 'doubling-compound-prod': 'https://mintappchecktoken-oaimzbv67a-uc.a.run.app',
134
- 'doubling-compound-dev': 'https://mintappchecktoken-molg2wq42a-uc.a.run.app',
135
- 'doubling-compound-sandbox': 'https://mintappchecktoken-pbpkn3igxa-uc.a.run.app',
98
+ 'doubling-compound-prod': 'https://mintappchecktoken-oaimzbv67a-uc.a.run.app',
99
+ 'doubling-compound-dev': 'https://mintappchecktoken-molg2wq42a-uc.a.run.app',
100
+ 'doubling-compound-sandbox': 'https://mintappchecktoken-pbpkn3igxa-uc.a.run.app',
136
101
  };
137
-
138
102
  // --- Helpers ---
139
-
140
103
  function ask(question) {
141
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
142
- return new Promise(resolve => {
143
- rl.question(question, answer => {
144
- rl.close();
145
- resolve(answer.trim());
104
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
105
+ return new Promise((resolve) => {
106
+ rl.question(question, (answer) => {
107
+ rl.close();
108
+ resolve(answer.trim());
109
+ });
146
110
  });
147
- });
148
111
  }
149
-
150
112
  function expandHome(p) {
151
- if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
152
- return p;
113
+ if (p.startsWith('~/'))
114
+ return path.join(os.homedir(), p.slice(2));
115
+ return p;
153
116
  }
154
-
155
117
  function openBrowser(url) {
156
- const cmd = process.platform === 'darwin' ? 'open' :
157
- process.platform === 'win32' ? 'start' : 'xdg-open';
158
- exec(`${cmd} "${url}"`);
118
+ const cmd = process.platform === 'darwin' ? 'open' :
119
+ process.platform === 'win32' ? 'start' : 'xdg-open';
120
+ exec(`${cmd} "${url}"`);
159
121
  }
160
-
161
- // --- Browser-based auth ---
162
-
163
122
  function buildLoginPage(fbConfig) {
164
- return `<!DOCTYPE html>
123
+ return `<!DOCTYPE html>
165
124
  <html><head><meta charset="utf-8"><title>Compound Sync - Sign In</title>
166
125
  <style>
167
126
  body { font-family: -apple-system, system-ui, sans-serif; max-width: 400px; margin: 80px auto; text-align: center; }
@@ -258,240 +217,228 @@ window.emailSignIn = async function() {
258
217
  </script>
259
218
  </body></html>`;
260
219
  }
261
-
262
220
  function browserAuth(fbConfig) {
263
- return new Promise((resolve, reject) => {
264
- const server = http.createServer((req, res) => {
265
- if (req.method === 'GET' && req.url === '/login') {
266
- res.writeHead(200, { 'Content-Type': 'text/html' });
267
- res.end(buildLoginPage(fbConfig));
268
- return;
269
- }
270
-
271
- if (req.method === 'POST' && req.url === '/auth-callback') {
272
- let body = '';
273
- req.on('data', chunk => { body += chunk; });
274
- req.on('end', () => {
275
- res.writeHead(200, { 'Content-Type': 'text/plain' });
276
- res.end('OK');
277
- server.close();
278
- resolve(JSON.parse(body));
221
+ return new Promise((resolve, reject) => {
222
+ const server = http.createServer((req, res) => {
223
+ if (req.method === 'GET' && req.url === '/login') {
224
+ res.writeHead(200, { 'Content-Type': 'text/html' });
225
+ res.end(buildLoginPage(fbConfig));
226
+ return;
227
+ }
228
+ if (req.method === 'POST' && req.url === '/auth-callback') {
229
+ let body = '';
230
+ req.on('data', (chunk) => { body += chunk; });
231
+ req.on('end', () => {
232
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
233
+ res.end('OK');
234
+ server.close();
235
+ resolve(JSON.parse(body));
236
+ });
237
+ return;
238
+ }
239
+ res.writeHead(404);
240
+ res.end('Not found');
241
+ });
242
+ server.listen(0, () => {
243
+ const addr = server.address();
244
+ // After listen(0) succeeds, address() returns an AddressInfo
245
+ // object. If we get null (server closed mid-flight) or a string
246
+ // (unix socket path) something went wrong; fail loudly rather
247
+ // than hand the user a `http://localhost:0/login` URL that
248
+ // browsers reject and hangs the auth flow indefinitely.
249
+ if (typeof addr !== 'object' || !addr) {
250
+ server.close();
251
+ reject(new Error(`browserAuth: server.address() returned ${JSON.stringify(addr)}`));
252
+ return;
253
+ }
254
+ const url = `http://localhost:${addr.port}/login`;
255
+ console.log(`Opening browser for sign-in...`);
256
+ console.log(`If the browser doesn't open, visit: ${url}`);
257
+ openBrowser(url);
279
258
  });
280
- return;
281
- }
282
-
283
- res.writeHead(404);
284
- res.end('Not found');
285
- });
286
-
287
- server.listen(0, () => {
288
- const port = server.address().port;
289
- const url = `http://localhost:${port}/login`;
290
- console.log(`Opening browser for sign-in...`);
291
- console.log(`If the browser doesn't open, visit: ${url}`);
292
- openBrowser(url);
293
259
  });
294
- });
295
260
  }
296
-
297
- // --- Interactive setup ---
298
-
299
261
  async function setupConfig() {
300
- console.log('');
301
- console.log('Welcome to the Compound sync daemon! Let\'s set things up.');
302
- console.log('');
303
-
304
- // Default to prod; --env flag overrides for internal use
305
- const projectId = envOverride || 'doubling-compound-prod';
306
-
307
- const fbConfig = firebaseConfigs[projectId];
308
-
309
- // Browser sign-in to discover orgs
310
- console.log('');
311
- const authResult = await browserAuth(fbConfig);
312
- const app = initializeApp(fbConfig, 'setup');
313
- const auth = getAuth(app);
314
-
315
- // Initialize App Check before re-authenticating in Node.js
316
- const appCheckUrl = appCheckEndpoints[projectId];
317
- if (appCheckUrl && authResult.firebaseIdToken) {
318
- await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
319
- }
320
-
321
- const db = getFirestore(app);
322
-
323
- let userCredential;
324
- if (authResult.method === 'google') {
325
- const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
326
- userCredential = await signInWithCredential(auth, credential);
327
- } else {
328
- userCredential = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
329
- }
330
-
331
- const userId = userCredential.user.uid;
332
- const userEmail = userCredential.user.email;
333
- console.log(`Signed in as ${userEmail}`);
334
-
335
- // List available orgs for this user
336
- console.log('');
337
- console.log('Fetching your organizations...');
338
- const membersSnapshot = await getDocs(
339
- query(collection(db, 'orgMembers'), where('userId', '==', userId))
340
- );
341
-
342
- const orgIds = [];
343
- membersSnapshot.forEach(doc => {
344
- orgIds.push(doc.data().orgId);
345
- });
346
-
347
- if (orgIds.length === 0) {
348
- console.error('No organizations found for your account.');
349
- process.exit(1);
350
- }
351
-
352
- const orgs = [];
353
- for (const orgId of orgIds) {
354
- const orgDoc = await getDoc(doc(db, 'orgs', orgId));
355
- if (orgDoc.exists()) {
356
- orgs.push({ id: orgDoc.id, ...orgDoc.data() });
262
+ console.log('');
263
+ console.log('Welcome to the Compound sync daemon! Let\'s set things up.');
264
+ console.log('');
265
+ // Default to prod; --env flag overrides for internal use
266
+ const projectId = envOverride || 'doubling-compound-prod';
267
+ const fbConfig = firebaseConfigs[projectId];
268
+ if (!fbConfig) {
269
+ console.error(`Unknown project: ${projectId}`);
270
+ process.exit(1);
357
271
  }
358
- }
359
-
360
- console.log('');
361
- console.log('Your organizations:');
362
- orgs.forEach((org, i) => {
363
- console.log(` ${i + 1}. ${org.name}`);
364
- });
365
- console.log('');
366
-
367
- let selectedIndices;
368
- if (orgs.length === 1) {
369
- const confirm = await ask(`Use "${orgs[0].name}"? [Y/n]: `) || 'y';
370
- if (confirm.toLowerCase() !== 'y') process.exit(0);
371
- selectedIndices = [0];
372
- } else {
373
- const choice = await ask(
374
- `Select organizations to sync (e.g. "1,3" or "all") [1-${orgs.length}]: `,
375
- );
376
- if (choice.toLowerCase() === 'all') {
377
- selectedIndices = orgs.map((_, i) => i);
378
- } else {
379
- selectedIndices = choice
380
- .split(',')
381
- .map(s => parseInt(s.trim(), 10) - 1)
382
- .filter(i => Number.isInteger(i) && i >= 0 && i < orgs.length);
383
- if (selectedIndices.length === 0) {
384
- console.error('Invalid selection.');
272
+ // Browser sign-in to discover orgs
273
+ console.log('');
274
+ const authResult = await browserAuth(fbConfig);
275
+ const app = initializeApp(fbConfig, 'setup');
276
+ const auth = getAuth(app);
277
+ // Initialize App Check before re-authenticating in Node.js
278
+ const appCheckUrl = appCheckEndpoints[projectId];
279
+ if (appCheckUrl && authResult.firebaseIdToken) {
280
+ await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
281
+ }
282
+ const db = getFirestore(app);
283
+ let userCredential;
284
+ if (authResult.method === 'google') {
285
+ const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
286
+ userCredential = await signInWithCredential(auth, credential);
287
+ }
288
+ else {
289
+ userCredential = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
290
+ }
291
+ const userId = userCredential.user.uid;
292
+ const userEmail = userCredential.user.email;
293
+ console.log(`Signed in as ${userEmail}`);
294
+ // List available orgs for this user
295
+ console.log('');
296
+ console.log('Fetching your organizations...');
297
+ const membersSnapshot = await getDocs(query(collection(db, 'orgMembers'), where('userId', '==', userId)));
298
+ const orgIds = [];
299
+ membersSnapshot.forEach((d) => {
300
+ const orgId = d.data()['orgId'];
301
+ if (typeof orgId === 'string' && orgId)
302
+ orgIds.push(orgId);
303
+ });
304
+ if (orgIds.length === 0) {
305
+ console.error('No organizations found for your account.');
385
306
  process.exit(1);
386
- }
387
307
  }
388
- }
389
-
390
- // Per-org local path
391
- const orgEntries = [];
392
- for (const idx of selectedIndices) {
393
- const org = orgs[idx];
308
+ const orgs = [];
309
+ for (const orgId of orgIds) {
310
+ const orgDoc = await getDoc(doc(db, 'orgs', orgId));
311
+ if (orgDoc.exists()) {
312
+ const data = orgDoc.data();
313
+ const name = data['name'];
314
+ orgs.push({ id: orgDoc.id, name: typeof name === 'string' ? name : orgDoc.id });
315
+ }
316
+ }
394
317
  console.log('');
395
- const defaultPath = path.join(os.homedir(), org.name + ' Workspace');
396
- const localPath = await ask(
397
- `Local sync folder for "${org.name}" [${defaultPath}]: `,
398
- ) || defaultPath;
399
- const resolvedPath = expandHome(localPath);
400
-
401
- if (!fs.existsSync(resolvedPath)) {
402
- const create = await ask(`"${resolvedPath}" doesn't exist. Create it? [Y/n]: `) || 'y';
403
- if (create.toLowerCase() === 'y') {
404
- fs.mkdirSync(resolvedPath, { recursive: true });
405
- console.log('Created.');
406
- } else {
407
- process.exit(0);
408
- }
318
+ console.log('Your organizations:');
319
+ orgs.forEach((org, i) => {
320
+ console.log(` ${i + 1}. ${org.name}`);
321
+ });
322
+ console.log('');
323
+ let selectedIndices;
324
+ const firstOrg = orgs[0];
325
+ if (orgs.length === 1 && firstOrg) {
326
+ const confirm = await ask(`Use "${firstOrg.name}"? [Y/n]: `) || 'y';
327
+ if (confirm.toLowerCase() !== 'y')
328
+ process.exit(0);
329
+ selectedIndices = [0];
409
330
  }
410
- orgEntries.push({ orgId: org.id, localPath: resolvedPath });
411
- }
412
-
413
- // Save config (no credentials stored). saveConfig creates parent dirs
414
- // so paths like ~/.config/compound-sync/work.json work without manual mkdir.
415
- saveConfig(CONFIG_PATH, { projectId, orgs: orgEntries });
416
- console.log('');
417
- console.log(`Config saved to ${CONFIG_PATH}`);
418
- console.log('');
419
-
420
- return {
421
- projectId,
422
- orgs: orgEntries,
423
- _userId: userId,
424
- _app: app,
425
- _db: db,
426
- };
331
+ else {
332
+ const choice = await ask(`Select organizations to sync (e.g. "1,3" or "all") [1-${orgs.length}]: `);
333
+ if (choice.toLowerCase() === 'all') {
334
+ selectedIndices = orgs.map((_, i) => i);
335
+ }
336
+ else {
337
+ selectedIndices = choice
338
+ .split(',')
339
+ .map((s) => parseInt(s.trim(), 10) - 1)
340
+ .filter((i) => Number.isInteger(i) && i >= 0 && i < orgs.length);
341
+ if (selectedIndices.length === 0) {
342
+ console.error('Invalid selection.');
343
+ process.exit(1);
344
+ }
345
+ }
346
+ }
347
+ // Per-org local path
348
+ const orgEntries = [];
349
+ for (const idx of selectedIndices) {
350
+ const org = orgs[idx];
351
+ if (!org)
352
+ continue;
353
+ console.log('');
354
+ const defaultPath = path.join(os.homedir(), org.name + ' Workspace');
355
+ const localPath = await ask(`Local sync folder for "${org.name}" [${defaultPath}]: `) || defaultPath;
356
+ const resolvedPath = expandHome(localPath);
357
+ if (!fs.existsSync(resolvedPath)) {
358
+ const create = await ask(`"${resolvedPath}" doesn't exist. Create it? [Y/n]: `) || 'y';
359
+ if (create.toLowerCase() === 'y') {
360
+ fs.mkdirSync(resolvedPath, { recursive: true });
361
+ console.log('Created.');
362
+ }
363
+ else {
364
+ process.exit(0);
365
+ }
366
+ }
367
+ orgEntries.push({ orgId: org.id, localPath: resolvedPath });
368
+ }
369
+ // Save config (no credentials stored). saveConfig creates parent dirs
370
+ // so paths like ~/.config/compound-sync/work.json work without manual mkdir.
371
+ saveConfig(CONFIG_PATH, { projectId, orgs: orgEntries });
372
+ console.log('');
373
+ console.log(`Config saved to ${CONFIG_PATH}`);
374
+ console.log('');
375
+ return {
376
+ projectId,
377
+ orgs: orgEntries,
378
+ _userId: userId,
379
+ _app: app,
380
+ _db: db,
381
+ };
427
382
  }
428
-
429
- // --- App Check (custom provider for CLI) ---
430
-
431
383
  // Fetch an App Check token using a Firebase ID token (before Node.js sign-in)
432
384
  async function fetchAppCheckToken(tokenEndpoint, firebaseIdToken) {
433
- const response = await fetch(tokenEndpoint, {
434
- method: 'POST',
435
- headers: {
436
- 'Authorization': `Bearer ${firebaseIdToken}`,
437
- 'Content-Type': 'application/json',
438
- },
439
- });
440
-
441
- if (!response.ok) {
442
- const errBody = await response.text().catch(() => '');
443
- console.error(`App Check token request failed (${response.status}): ${errBody}`);
444
- throw new Error(`App Check token request failed: ${response.status}`);
445
- }
446
-
447
- return await response.json();
385
+ const response = await fetch(tokenEndpoint, {
386
+ method: 'POST',
387
+ headers: {
388
+ 'Authorization': `Bearer ${firebaseIdToken}`,
389
+ 'Content-Type': 'application/json',
390
+ },
391
+ });
392
+ if (!response.ok) {
393
+ const errBody = await response.text().catch(() => '');
394
+ console.error(`App Check token request failed (${response.status}): ${errBody}`);
395
+ throw new Error(`App Check token request failed: ${response.status}`);
396
+ }
397
+ return await response.json();
448
398
  }
449
-
450
399
  // Initialize App Check with a pre-fetched ID token (used on first sign-in)
451
400
  async function initAppCheckWithIdToken(app, tokenEndpoint, firebaseIdToken) {
452
- console.log('Initializing App Check...');
453
- // Pre-fetch the first token
454
- const initial = await fetchAppCheckToken(tokenEndpoint, firebaseIdToken);
455
- let cachedToken = {
456
- token: initial.token,
457
- expireTimeMillis: Date.now() + initial.ttlMillis,
458
- };
459
-
460
- const customProvider = new CustomProvider({
461
- getToken: async () => {
462
- // If token is still valid, return cached
463
- if (cachedToken.expireTimeMillis > Date.now() + 60000) {
464
- return cachedToken;
465
- }
466
- // Otherwise refresh — at this point the user should be signed in
467
- const auth = getAuth(app);
468
- const user = auth.currentUser;
469
- if (!user) return cachedToken; // fallback
470
- const idToken = await user.getIdToken();
471
- const refreshed = await fetchAppCheckToken(tokenEndpoint, idToken);
472
- cachedToken = {
473
- token: refreshed.token,
474
- expireTimeMillis: Date.now() + refreshed.ttlMillis,
475
- };
476
- return cachedToken;
477
- },
478
- });
479
-
480
- initializeAppCheck(app, {
481
- provider: customProvider,
482
- isTokenAutoRefreshEnabled: true,
483
- });
484
- console.log('App Check initialized.');
401
+ console.log('Initializing App Check...');
402
+ // Pre-fetch the first token
403
+ const initial = await fetchAppCheckToken(tokenEndpoint, firebaseIdToken);
404
+ let cachedToken = {
405
+ token: initial.token,
406
+ expireTimeMillis: Date.now() + initial.ttlMillis,
407
+ };
408
+ const customProvider = new CustomProvider({
409
+ getToken: async () => {
410
+ // If token is still valid, return cached
411
+ if (cachedToken.expireTimeMillis > Date.now() + 60000) {
412
+ return cachedToken;
413
+ }
414
+ // Otherwise refresh — at this point the user should be signed in
415
+ const auth = getAuth(app);
416
+ const user = auth.currentUser;
417
+ if (!user)
418
+ return cachedToken; // fallback
419
+ const idToken = await user.getIdToken();
420
+ const refreshed = await fetchAppCheckToken(tokenEndpoint, idToken);
421
+ cachedToken = {
422
+ token: refreshed.token,
423
+ expireTimeMillis: Date.now() + refreshed.ttlMillis,
424
+ };
425
+ return cachedToken;
426
+ },
427
+ });
428
+ initializeAppCheck(app, {
429
+ provider: customProvider,
430
+ isTokenAutoRefreshEnabled: true,
431
+ });
432
+ console.log('App Check initialized.');
485
433
  }
486
-
487
434
  // Initialize App Check with an already-signed-in user (used during setup reuse)
488
435
  async function initAppCheck(app, auth, tokenEndpoint) {
489
- const user = auth.currentUser;
490
- if (!user) throw new Error('Not signed in');
491
- const idToken = await user.getIdToken();
492
- await initAppCheckWithIdToken(app, tokenEndpoint, idToken);
436
+ const user = auth.currentUser;
437
+ if (!user)
438
+ throw new Error('Not signed in');
439
+ const idToken = await user.getIdToken();
440
+ await initAppCheckWithIdToken(app, tokenEndpoint, idToken);
493
441
  }
494
-
495
442
  // --- Non-interactive account discovery (for the desktop app) ---
496
443
  //
497
444
  // Sign in (restoring a persisted session if present, else via the
@@ -502,97 +449,92 @@ async function initAppCheck(app, auth, tokenEndpoint) {
502
449
  // The desktop parses that to register the account and write its
503
450
  // per-account config. Exits when done.
504
451
  async function discoverOrgs(projectId) {
505
- const fbConfig = firebaseConfigs[projectId];
506
- if (!fbConfig) {
507
- console.error(`Unknown project: ${projectId}`);
508
- process.exit(1);
509
- }
510
-
511
- // Use the DEFAULT app name (not a named 'discover' app): Firebase
512
- // derives its auth-persistence storage key from the app name, and the
513
- // daemon reads the default-app key. A named app here would persist
514
- // under a different key, so the daemon wouldn't find the session and
515
- // would prompt for sign-in a second time. discover exits before the
516
- // daemon block runs, so there's no app-name collision.
517
- const app = initializeApp(fbConfig);
518
- const authFilePath = resolveAuthFilePath({
519
- authFileEnv: process.env.COMPOUND_AUTH_FILE,
520
- homeDir: os.homedir(),
521
- projectId,
522
- accountKey: 'discover',
523
- });
524
- // Pre-mint App Check on the app, THEN initializeAuth. The ordering
525
- // matters: see setup-auth-with-pre-mint.js for the call-order
526
- // contract and why the 1.9.3 code (initializeAuth before pre-mint)
527
- // raced the SDK's startup :lookup and lost.
528
- const appCheckUrl = appCheckEndpoints[projectId];
529
- const { auth } = await setupAuthWithPreMint(
530
- { app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl },
531
- {
532
- readPersistedRefreshToken,
533
- exchangeRefreshTokenForIdToken,
534
- initAppCheckWithIdToken,
535
- makeFilePersistence,
536
- initializeAuth,
537
- },
538
- );
539
-
540
- let userId;
541
- let userEmail;
542
-
543
- if (auth.currentUser) {
544
- userId = auth.currentUser.uid;
545
- userEmail = auth.currentUser.email;
546
- // App Check is already initialized above when we had a persisted
547
- // user; no need to re-init.
548
- } else {
549
- const authResult = await browserAuth(fbConfig);
550
- if (appCheckUrl && authResult.firebaseIdToken) {
551
- await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
452
+ const fbConfig = firebaseConfigs[projectId];
453
+ if (!fbConfig) {
454
+ console.error(`Unknown project: ${projectId}`);
455
+ process.exit(1);
552
456
  }
553
- let cred;
554
- if (authResult.method === 'google') {
555
- cred = await signInWithCredential(
556
- auth,
557
- GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken),
558
- );
559
- } else {
560
- cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
457
+ // Use the DEFAULT app name (not a named 'discover' app): Firebase
458
+ // derives its auth-persistence storage key from the app name, and the
459
+ // daemon reads the default-app key. A named app here would persist
460
+ // under a different key, so the daemon wouldn't find the session and
461
+ // would prompt for sign-in a second time. discover exits before the
462
+ // daemon block runs, so there's no app-name collision.
463
+ const app = initializeApp(fbConfig);
464
+ const authFilePath = resolveAuthFilePath({
465
+ authFileEnv: process.env['COMPOUND_AUTH_FILE'],
466
+ homeDir: os.homedir(),
467
+ projectId,
468
+ accountKey: 'discover',
469
+ });
470
+ // Pre-mint App Check on the app, THEN initializeAuth. The ordering
471
+ // matters: see setup-auth-with-pre-mint.js for the call-order
472
+ // contract and why the 1.9.3 code (initializeAuth before pre-mint)
473
+ // raced the SDK's startup :lookup and lost.
474
+ const appCheckUrl = appCheckEndpoints[projectId];
475
+ const { auth } = await setupAuthWithPreMint({ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl }, {
476
+ readPersistedRefreshToken,
477
+ exchangeRefreshTokenForIdToken,
478
+ initAppCheckWithIdToken,
479
+ makeFilePersistence,
480
+ initializeAuth,
481
+ });
482
+ let userId;
483
+ let userEmail;
484
+ if (auth.currentUser) {
485
+ userId = auth.currentUser.uid;
486
+ userEmail = auth.currentUser.email;
487
+ // App Check is already initialized above when we had a persisted
488
+ // user; no need to re-init.
561
489
  }
562
- userId = cred.user.uid;
563
- userEmail = cred.user.email;
564
- }
565
-
566
- const db = getFirestore(app);
567
- const snapshot = await getDocs(
568
- query(collection(db, 'orgMembers'), where('userId', '==', userId)),
569
- );
570
- const orgIds = [];
571
- snapshot.forEach(d => orgIds.push(d.data().orgId));
572
- const orgs = [];
573
- for (const orgId of orgIds) {
574
- const orgDoc = await getDoc(doc(db, 'orgs', orgId));
575
- if (orgDoc.exists()) orgs.push({ id: orgDoc.id, name: orgDoc.data().name });
576
- }
577
-
578
- console.log(`COMPOUND_DISCOVER ${JSON.stringify({ userId, email: userEmail, orgs })}`);
579
- process.exit(0);
490
+ else {
491
+ const authResult = await browserAuth(fbConfig);
492
+ if (appCheckUrl && authResult.firebaseIdToken) {
493
+ await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
494
+ }
495
+ let cred;
496
+ if (authResult.method === 'google') {
497
+ cred = await signInWithCredential(auth, GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken));
498
+ }
499
+ else {
500
+ cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
501
+ }
502
+ userId = cred.user.uid;
503
+ userEmail = cred.user.email;
504
+ }
505
+ const db = getFirestore(app);
506
+ const snapshot = await getDocs(query(collection(db, 'orgMembers'), where('userId', '==', userId)));
507
+ const orgIds = [];
508
+ snapshot.forEach((d) => {
509
+ const orgId = d.data()['orgId'];
510
+ if (typeof orgId === 'string' && orgId)
511
+ orgIds.push(orgId);
512
+ });
513
+ const orgs = [];
514
+ for (const orgId of orgIds) {
515
+ const orgDoc = await getDoc(doc(db, 'orgs', orgId));
516
+ if (orgDoc.exists()) {
517
+ const name = orgDoc.data()['name'];
518
+ // Fall back to the org id when the doc has no usable `name`
519
+ // field; matches setupConfig's behavior and gives the desktop
520
+ // org picker something to render instead of a blank button.
521
+ orgs.push({ id: orgDoc.id, name: typeof name === 'string' && name ? name : orgDoc.id });
522
+ }
523
+ }
524
+ console.log(`COMPOUND_DISCOVER ${JSON.stringify({ userId, email: userEmail, orgs })}`);
525
+ process.exit(0);
580
526
  }
581
-
582
527
  // --- Config ---
583
-
584
528
  const args = process.argv.slice(2);
585
-
586
529
  // Handle --version and --help early so they short-circuit before any
587
530
  // Firebase init or browser-auth side effects.
588
531
  if (args.includes('--version') || args.includes('-v')) {
589
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
590
- console.log(pkg.version);
591
- process.exit(0);
532
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
533
+ console.log(pkg.version);
534
+ process.exit(0);
592
535
  }
593
-
594
536
  if (args.includes('--help') || args.includes('-h')) {
595
- console.log(`compound-sync - Doubling Compound sync daemon
537
+ console.log(`compound-sync - Doubling Compound sync daemon
596
538
 
597
539
  Usage:
598
540
  compound-sync Start the daemon (interactive setup on first run)
@@ -603,164 +545,171 @@ Usage:
603
545
  compound-sync --help, -h Print this help and exit
604
546
 
605
547
  More: https://github.com/Doubling-Inc/doubling-compound`);
606
- process.exit(0);
548
+ process.exit(0);
607
549
  }
608
-
609
550
  let configFile = 'config.json';
610
551
  let forceSetup = false;
611
552
  let envOverride = null;
612
553
  let discoverMode = false;
613
554
  for (let i = 0; i < args.length; i++) {
614
- if (args[i] === '--config' && args[i + 1]) { configFile = args[++i]; }
615
- if (args[i] === '--setup') { forceSetup = true; }
616
- if (args[i] === '--env' && args[i + 1]) { envOverride = `doubling-compound-${args[++i]}`; }
617
- // Non-interactive account discovery for the desktop app: sign in
618
- // (persisting credentials to COMPOUND_AUTH_FILE), print the user's
619
- // orgs as a COMPOUND_DISCOVER JSON line, and exit. The desktop
620
- // parses it to register the account and write its config.
621
- if (args[i] === '--discover') { discoverMode = true; }
555
+ if (args[i] === '--config' && args[i + 1]) {
556
+ configFile = args[++i];
557
+ }
558
+ if (args[i] === '--setup') {
559
+ forceSetup = true;
560
+ }
561
+ if (args[i] === '--env' && args[i + 1]) {
562
+ envOverride = `doubling-compound-${args[++i]}`;
563
+ }
564
+ // Non-interactive account discovery for the desktop app: sign in
565
+ // (persisting credentials to COMPOUND_AUTH_FILE), print the user's
566
+ // orgs as a COMPOUND_DISCOVER JSON line, and exit. The desktop
567
+ // parses it to register the account and write its config.
568
+ if (args[i] === '--discover') {
569
+ discoverMode = true;
570
+ }
622
571
  }
623
-
624
572
  if (discoverMode) {
625
- await discoverOrgs(envOverride || 'doubling-compound-prod');
626
- // discoverOrgs exits the process.
573
+ await discoverOrgs(envOverride || 'doubling-compound-prod');
574
+ // discoverOrgs exits the process.
627
575
  }
628
-
629
576
  const CONFIG_PATH = resolveConfigPath(configFile, __dirname);
630
577
  let config = null;
631
578
  if (!forceSetup) {
632
- try {
633
- config = loadConfig(CONFIG_PATH);
634
- } catch (err) {
635
- console.error(`Failed to load ${CONFIG_PATH}: ${err.message}`);
636
- process.exit(1);
637
- }
579
+ try {
580
+ config = loadConfig(CONFIG_PATH);
581
+ }
582
+ catch (err) {
583
+ const msg = err instanceof Error ? err.message : String(err);
584
+ console.error(`Failed to load ${CONFIG_PATH}: ${msg}`);
585
+ process.exit(1);
586
+ }
638
587
  }
639
-
640
588
  // Interactive setup if no config exists or --setup was passed
641
589
  if (!config) {
642
- config = await setupConfig();
590
+ config = await setupConfig();
643
591
  }
644
-
645
592
  // --- Initialize Firebase client SDK ---
646
-
647
- let app, db, userId;
648
-
649
- if (config._app) {
650
- // Reuse app and auth from setup
651
- app = config._app;
652
- db = config._db;
653
- userId = config._userId;
654
-
655
- // Initialize App Check if endpoint exists for this env
656
- const appCheckUrl = appCheckEndpoints[config.projectId];
657
- if (appCheckUrl) {
658
- const auth = getAuth(app);
659
- await initAppCheck(app, auth, appCheckUrl);
660
- }
661
- } else {
662
- const fbConfig = firebaseConfigs[config.projectId];
663
- if (!fbConfig) {
664
- console.error(`Unknown project: ${config.projectId}`);
665
- process.exit(1);
666
- }
667
-
668
- app = initializeApp(fbConfig);
669
- db = getFirestore(app);
670
-
671
- // File-backed auth persistence. A previously-signed-in session is
672
- // restored here with no browser. The desktop app sets
673
- // COMPOUND_AUTH_FILE per account; the CLI derives a per-(project,
674
- // account) path under ~/.config/compound-sync. initializeAuth must
675
- // run before any getAuth(app) for this app.
676
- const authFilePath = resolveAuthFilePath({
677
- authFileEnv: process.env.COMPOUND_AUTH_FILE,
678
- homeDir: os.homedir(),
679
- projectId: config.projectId,
680
- accountKey: config.email || config.orgId,
681
- });
682
- // Pre-mint App Check on the app, THEN initializeAuth. The ordering
683
- // matters: see setup-auth-with-pre-mint.js for the call-order
684
- // contract and why the 1.9.3 code (initializeAuth before pre-mint)
685
- // raced the SDK's startup :lookup and lost.
686
- const appCheckUrl = appCheckEndpoints[config.projectId];
687
- const { auth, appCheckPreMinted } = await setupAuthWithPreMint(
688
- { app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl },
689
- {
690
- readPersistedRefreshToken,
691
- exchangeRefreshTokenForIdToken,
692
- initAppCheckWithIdToken,
693
- makeFilePersistence,
694
- initializeAuth,
695
- },
696
- );
697
-
698
- if (auth.currentUser) {
699
- // Silent re-auth: session restored from persisted credentials, no
700
- // browser. App Check was already pre-minted above when we had a
701
- // persisted user, so no need to re-init.
702
- userId = auth.currentUser.uid;
703
- if (appCheckUrl && !appCheckPreMinted) {
704
- // Defensive: persisted user existed but pre-mint failed; still try
705
- // to bring App Check up via the current user's ID token so the
706
- // rest of the daemon's calls aren't blocked.
707
- const idToken = await auth.currentUser.getIdToken();
708
- await initAppCheckWithIdToken(app, appCheckUrl, idToken);
593
+ // Type predicate: did setupConfig produce the "hot handles" result
594
+ // (with _app/_db/_userId already initialized), or did we load a
595
+ // vanilla NormalizedConfig from disk?
596
+ function hasHotHandles(c) {
597
+ return '_app' in c;
598
+ }
599
+ let app;
600
+ let db;
601
+ let userId;
602
+ if (hasHotHandles(config)) {
603
+ // Reuse app and auth from setup
604
+ app = config._app;
605
+ db = config._db;
606
+ userId = config._userId;
607
+ // Initialize App Check if endpoint exists for this env
608
+ const appCheckUrl = appCheckEndpoints[config.projectId];
609
+ if (appCheckUrl) {
610
+ const auth = getAuth(app);
611
+ await initAppCheck(app, auth, appCheckUrl);
709
612
  }
710
- console.log(`Signed in (silent) as ${auth.currentUser.email || userId}`);
711
- } else {
712
- // First run for this account: interactive browser sign-in. The
713
- // result is persisted, so later launches take the silent path.
714
- console.log('');
715
- const authResult = await browserAuth(fbConfig);
716
-
717
- // Initialize App Check BEFORE signing in on the Node.js side, using
718
- // the Firebase ID token from the browser.
719
- if (appCheckUrl && authResult.firebaseIdToken) {
720
- await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
613
+ }
614
+ else {
615
+ const fbConfig = firebaseConfigs[config.projectId];
616
+ if (!fbConfig) {
617
+ console.error(`Unknown project: ${config.projectId}`);
618
+ process.exit(1);
721
619
  }
722
-
723
- if (authResult.method === 'google') {
724
- const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
725
- const cred = await signInWithCredential(auth, credential);
726
- userId = cred.user.uid;
727
- console.log(`Signed in as ${cred.user.email}`);
728
- } else {
729
- const cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
730
- userId = cred.user.uid;
731
- console.log(`Signed in as ${cred.user.email}`);
620
+ app = initializeApp(fbConfig);
621
+ db = getFirestore(app);
622
+ // File-backed auth persistence. A previously-signed-in session is
623
+ // restored here with no browser. The desktop app sets
624
+ // COMPOUND_AUTH_FILE per account; the CLI derives a per-(project,
625
+ // account) path under ~/.config/compound-sync. initializeAuth must
626
+ // run before any getAuth(app) for this app.
627
+ //
628
+ // accountKey: use the first org's id as a stable per-config
629
+ // discriminator. NormalizedConfig drops the legacy top-level `email`
630
+ // and `orgId` fields, so the prior `config.email || config.orgId`
631
+ // lookup collapsed to undefined and every CLI config under the same
632
+ // project collided on the same `<project>__default.json` auth file.
633
+ // The first org id is always present (normalizeConfig rejects empty
634
+ // orgs[]) and uniquely identifies a config across the multi-org
635
+ // schema. The desktop app overrides this entirely via
636
+ // COMPOUND_AUTH_FILE, so this only matters for the bare CLI path.
637
+ const accountKey = config.orgs[0]?.orgId;
638
+ const authFilePath = resolveAuthFilePath({
639
+ authFileEnv: process.env['COMPOUND_AUTH_FILE'],
640
+ homeDir: os.homedir(),
641
+ projectId: config.projectId,
642
+ accountKey,
643
+ });
644
+ // Pre-mint App Check on the app, THEN initializeAuth. The ordering
645
+ // matters: see setup-auth-with-pre-mint.js for the call-order
646
+ // contract and why the 1.9.3 code (initializeAuth before pre-mint)
647
+ // raced the SDK's startup :lookup and lost.
648
+ const appCheckUrl = appCheckEndpoints[config.projectId];
649
+ const { auth, appCheckPreMinted } = await setupAuthWithPreMint({ app, authFilePath, apiKey: fbConfig.apiKey, appCheckUrl }, {
650
+ readPersistedRefreshToken,
651
+ exchangeRefreshTokenForIdToken,
652
+ initAppCheckWithIdToken,
653
+ makeFilePersistence,
654
+ initializeAuth,
655
+ });
656
+ if (auth.currentUser) {
657
+ // Silent re-auth: session restored from persisted credentials, no
658
+ // browser. App Check was already pre-minted above when we had a
659
+ // persisted user, so no need to re-init.
660
+ userId = auth.currentUser.uid;
661
+ if (appCheckUrl && !appCheckPreMinted) {
662
+ // Defensive: persisted user existed but pre-mint failed; still try
663
+ // to bring App Check up via the current user's ID token so the
664
+ // rest of the daemon's calls aren't blocked.
665
+ const idToken = await auth.currentUser.getIdToken();
666
+ await initAppCheckWithIdToken(app, appCheckUrl, idToken);
667
+ }
668
+ console.log(`Signed in (silent) as ${auth.currentUser.email || userId}`);
669
+ }
670
+ else {
671
+ // First run for this account: interactive browser sign-in. The
672
+ // result is persisted, so later launches take the silent path.
673
+ console.log('');
674
+ const authResult = await browserAuth(fbConfig);
675
+ // Initialize App Check BEFORE signing in on the Node.js side, using
676
+ // the Firebase ID token from the browser.
677
+ if (appCheckUrl && authResult.firebaseIdToken) {
678
+ await initAppCheckWithIdToken(app, appCheckUrl, authResult.firebaseIdToken);
679
+ }
680
+ if (authResult.method === 'google') {
681
+ const credential = GoogleAuthProvider.credential(authResult.idToken, authResult.accessToken);
682
+ const cred = await signInWithCredential(auth, credential);
683
+ userId = cred.user.uid;
684
+ console.log(`Signed in as ${cred.user.email}`);
685
+ }
686
+ else {
687
+ const cred = await signInWithEmailAndPassword(auth, authResult.email, authResult.password);
688
+ userId = cred.user.uid;
689
+ console.log(`Signed in as ${cred.user.email}`);
690
+ }
732
691
  }
733
- }
734
-
735
692
  }
736
-
737
693
  const USER_ID = userId;
738
694
  const storage = getStorage(app);
739
-
740
695
  // Read the bundled package version so the log line confirms which
741
696
  // daemon copy is actually running. Saves the "did the rebuild include
742
697
  // the fix?" diagnostic round-trip when a desktop bundle is mid-update.
743
- const __syncPkgVersion = JSON.parse(
744
- fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
745
- ).version;
746
-
698
+ const __syncPkgVersion = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8')).version;
747
699
  console.log(`Sync daemon starting (compound-sync ${__syncPkgVersion})...`);
748
700
  console.log(` Env: ${config.projectId}`);
749
701
  console.log(` User: ${USER_ID}`);
750
702
  console.log(` Orgs: ${config.orgs.length}`);
751
-
752
703
  // Per-org runtime, extracted to ./org-sync.js so it is importable and
753
704
  // testable against the emulator (DOU-156). Inject the module-level
754
705
  // db / storage / userId here.
755
706
  function setupOrgSync({ orgId, localPath }) {
756
- return startOrgSync({ db, storage, userId: USER_ID, orgId, localPath: expandHome(localPath) });
707
+ return startOrgSync({ db, storage, userId: USER_ID, orgId, localPath: expandHome(localPath) });
757
708
  }
758
-
759
709
  // --- Main ---
760
-
761
710
  for (const orgEntry of config.orgs) {
762
- await setupOrgSync(orgEntry);
711
+ await setupOrgSync(orgEntry);
763
712
  }
764
-
765
713
  console.log('');
766
714
  console.log('Sync running. Press Ctrl+C to stop.');
715
+ //# sourceMappingURL=sync.js.map