@doubling/compound-sync 1.2.1 → 1.3.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/dual-write.js +21 -8
  2. package/package.json +1 -1
  3. package/sync.js +43 -7
package/dual-write.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  doc, getDocs, setDoc, updateDoc, deleteDoc, query, where,
20
20
  } from 'firebase/firestore';
21
21
  import {
22
- ref as storageRef, uploadString, deleteObject,
22
+ ref as storageRef, uploadString, deleteObject, getBytes,
23
23
  } from 'firebase/storage';
24
24
 
25
25
  // ---- Pure helpers ----
@@ -53,11 +53,26 @@ export async function deleteBlob({ storage, orgId, fileId }) {
53
53
  }
54
54
  }
55
55
 
56
+ /**
57
+ * Read file content from Cloud Storage. Throws on any failure
58
+ * (missing blob, permission error, network). The Firestore content
59
+ * field is no longer written or read as of PR 6, so the read path
60
+ * has no fallback. Backfill (PR 4) is a prerequisite.
61
+ */
62
+ export async function readFileContent({ storage, orgId, fileId }) {
63
+ const blobPath = storagePathFor(orgId, fileId);
64
+ const bytes = await getBytes(storageRef(storage, blobPath));
65
+ return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
66
+ }
67
+
56
68
  // ---- Core dual-write ----
57
69
 
58
70
  /**
59
- * Write a file's content to both the Firestore `content` field and a
60
- * Cloud Storage blob at `orgs/{orgId}/files/{fileId}/current.md`.
71
+ * Write a file's content to Cloud Storage at
72
+ * orgs/{orgId}/files/{fileId}/current.md and update the Firestore
73
+ * metadata doc with storagePath/contentHash/size. As of PR 6 the
74
+ * Firestore `content` field is no longer written; the blob is the
75
+ * sole source of file content.
61
76
  *
62
77
  * Returns one of:
63
78
  * { action: 'skipped', reason: 'shared-scope' }
@@ -66,8 +81,8 @@ export async function deleteBlob({ storage, orgId, fileId }) {
66
81
  * { action: 'updated', fileId, storageError? }
67
82
  *
68
83
  * `storageError` is populated when the Firestore write succeeded but
69
- * the Storage upload failed. Callers should log and continue; the next
70
- * save will re-upload because the hash on the metadata will not match.
84
+ * the Storage upload failed. Callers should log and retry; the next
85
+ * save attempts the upload again.
71
86
  */
72
87
  export async function pushFileToCloud({
73
88
  filesRef,
@@ -109,7 +124,6 @@ export async function pushFileToCloud({
109
124
  const fileId = newDocRef.id;
110
125
  const fileData = {
111
126
  path: docPath,
112
- content,
113
127
  createdAt: now,
114
128
  updatedAt: now,
115
129
  scope,
@@ -132,11 +146,10 @@ export async function pushFileToCloud({
132
146
 
133
147
  const docSnap = snapshot.docs[0];
134
148
  const existing = docSnap.data();
135
- if (existing.content === content && existing.contentHash === contentHash) {
149
+ if (existing.contentHash === contentHash) {
136
150
  return { action: 'noop', fileId: docSnap.id };
137
151
  }
138
152
  await updateDoc(docSnap.ref, {
139
- content,
140
153
  updatedAt: now,
141
154
  storagePath: storagePathFor(orgId, docSnap.id),
142
155
  contentHash,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doubling/compound-sync",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Bidirectional sync between Compound and local markdown files",
5
5
  "type": "module",
6
6
  "bin": {
package/sync.js CHANGED
@@ -11,7 +11,9 @@
11
11
  * Shared by Me/ ← symlinks to files you've shared
12
12
  * Shared with Me/ ← files shared with you (read-only)
13
13
  *
14
- * Usage: node sync.js [--config <file>] [--setup]
14
+ * Usage: node sync.js [--config <file>] [--setup] [--env <name>]
15
+ * node sync.js --version
16
+ * node sync.js --help
15
17
  */
16
18
 
17
19
  import { initializeApp } from 'firebase/app';
@@ -30,7 +32,7 @@ import os from 'os';
30
32
  import http from 'http';
31
33
  import { exec } from 'child_process';
32
34
  import { fileURLToPath } from 'url';
33
- import { pushFileToCloud, deleteFileFromCloud } from './dual-write.js';
35
+ import { pushFileToCloud, deleteFileFromCloud, readFileContent } from './dual-write.js';
34
36
 
35
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
38
 
@@ -420,6 +422,30 @@ async function initAppCheck(app, auth, tokenEndpoint) {
420
422
  // --- Config ---
421
423
 
422
424
  const args = process.argv.slice(2);
425
+
426
+ // Handle --version and --help early so they short-circuit before any
427
+ // Firebase init or browser-auth side effects.
428
+ if (args.includes('--version') || args.includes('-v')) {
429
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
430
+ console.log(pkg.version);
431
+ process.exit(0);
432
+ }
433
+
434
+ if (args.includes('--help') || args.includes('-h')) {
435
+ console.log(`compound-sync - Doubling Compound sync daemon
436
+
437
+ Usage:
438
+ compound-sync Start the daemon (interactive setup on first run)
439
+ compound-sync --config <file> Use a specific config file
440
+ compound-sync --setup Force the setup wizard
441
+ compound-sync --env <name> Override env (sandbox|dev|prod)
442
+ compound-sync --version, -v Print version and exit
443
+ compound-sync --help, -h Print this help and exit
444
+
445
+ More: https://github.com/Doubling-Inc/doubling-compound`);
446
+ process.exit(0);
447
+ }
448
+
423
449
  let configFile = 'config.json';
424
450
  let forceSetup = false;
425
451
  let envOverride = null;
@@ -615,7 +641,7 @@ ensureFolderStructure();
615
641
 
616
642
  // --- Firestore → Local (multiple listeners) ---
617
643
 
618
- function handleFirestoreChange(change, getLocalPath) {
644
+ async function handleFirestoreChange(change, getLocalPath) {
619
645
  const data = change.doc.data();
620
646
  const docPath = data.path;
621
647
  if (!docPath) return;
@@ -648,7 +674,16 @@ function handleFirestoreChange(change, getLocalPath) {
648
674
  console.log(` [firestore → local] ${action} ${docPath} (${data.scope || 'team'})`);
649
675
  ensureDir(localPath);
650
676
  suppressLocal.add(localPath);
651
- fs.writeFileSync(localPath, data.content || '', 'utf-8');
677
+
678
+ // Read content from Cloud Storage. As of PR 6 there is no
679
+ // fallback; any read failure propagates to the caller's catch
680
+ // handler (handleFirestoreChange's promise rejection logger).
681
+ const content = await readFileContent({
682
+ storage,
683
+ orgId: ORG_ID,
684
+ fileId: change.doc.id,
685
+ });
686
+ fs.writeFileSync(localPath, content || '', 'utf-8');
652
687
  lastKnownUpdate.set(trackingKey, remoteUpdated);
653
688
  setTimeout(() => suppressLocal.delete(localPath), 1000);
654
689
  }
@@ -663,7 +698,7 @@ function startFirestoreListeners() {
663
698
  snapshot.docChanges().forEach(change => {
664
699
  handleFirestoreChange(change, (data) => {
665
700
  return path.join(LOCAL_PATH, teamFolder, data.path);
666
- });
701
+ }).catch(err => console.error('Firestore change handler error:', err && err.message));
667
702
  });
668
703
  }, err => console.error(`Team "${teamFolder}" listener error:`, err));
669
704
  console.log(` Listening: ${teamFolder}`);
@@ -676,7 +711,8 @@ function startFirestoreListeners() {
676
711
  const data = change.doc.data();
677
712
  const realPath = path.join(LOCAL_PATH, PRIVATE_FOLDER, data.path);
678
713
 
679
- handleFirestoreChange(change, () => realPath);
714
+ handleFirestoreChange(change, () => realPath)
715
+ .catch(err => console.error('Firestore change handler error:', err && err.message));
680
716
 
681
717
  if (change.type !== 'removed') {
682
718
  updateSharedByMeSymlink(data, realPath);
@@ -695,7 +731,7 @@ function startFirestoreListeners() {
695
731
  if (data.ownerId === USER_ID) return;
696
732
  handleFirestoreChange(change, () => {
697
733
  return path.join(LOCAL_PATH, SHARED_WITH_ME_FOLDER, data.path);
698
- });
734
+ }).catch(err => console.error('Firestore change handler error:', err && err.message));
699
735
  });
700
736
  }, err => console.error('Shared with me listener error:', err));
701
737
  console.log(' Listening: Shared with Me');