@bobfrankston/gcards 0.1.22 → 0.1.24

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/gcards.ts CHANGED
@@ -129,7 +129,7 @@ async function refreshAccessToken(): Promise<string> {
129
129
  return currentAccessToken;
130
130
  }
131
131
 
132
- /** Get sort key from contact: fileAs > displayNameLastFirst > displayName */
132
+ /** Get sort key from contact: fileAs (First Last) > displayNameLastFirst (Last, First) > displayName */
133
133
  function getSortKey(person: GooglePerson): string {
134
134
  const fileAs = person.fileAses?.[0]?.value;
135
135
  if (fileAs) return fileAs;
@@ -139,7 +139,7 @@ function getSortKey(person: GooglePerson): string {
139
139
  }
140
140
 
141
141
  function saveIndex(paths: UserPaths, index: ContactIndex): void {
142
- // Sort contacts by sortKey (fileAs > displayNameLastFirst > displayName)
142
+ // Sort contacts by sortKey (fileAs='First Last' > displayNameLastFirst='Last, First' > displayName)
143
143
  const sortedContacts: Record<string, IndexEntry> = {};
144
144
  const entries = Object.entries(index.contacts);
145
145
  entries.sort((a, b) => {
@@ -151,7 +151,23 @@ function saveIndex(paths: UserPaths, index: ContactIndex): void {
151
151
  sortedContacts[key] = value;
152
152
  }
153
153
  index.contacts = sortedContacts;
154
- fs.writeFileSync(paths.indexFile, JSON.stringify(index, null, 2));
154
+
155
+ // Add helpful comments to the file
156
+ const comment = `// index.json - Active contacts index
157
+ // This file tracks all contacts synced from Google.
158
+ //
159
+ // Special fields:
160
+ // _delete: Set to 'force' to mark a contact for deletion on next push
161
+ // Set to 'queue' to mark for interactive deletion review
162
+ // hasPhoto: Indicates contact has a non-default photo
163
+ // starred: Indicates contact is starred in Google
164
+ // guids: Array of GUID values from userDefined fields
165
+ //
166
+ // To delete a contact: Add "_delete": "force" to the entry, then run 'gcards push'
167
+ // To queue for review: Add "_delete": "queue" to the entry
168
+ //
169
+ `;
170
+ fs.writeFileSync(paths.indexFile, comment + JSON.stringify(index, null, 2));
155
171
  }
156
172
 
157
173
  /** Deleted contacts index (separate from active index) */
@@ -179,7 +195,18 @@ function saveDeleted(paths: UserPaths, deletedIndex: DeletedIndex): void {
179
195
  sorted[key] = value;
180
196
  }
181
197
  deletedIndex.deleted = sorted;
182
- fs.writeFileSync(paths.deletedFile, JSON.stringify(deletedIndex, null, 2));
198
+
199
+ // Add helpful comments
200
+ const comment = `// deleted.json - Tombstone index of deleted contacts
201
+ // This file tracks contacts that have been deleted from Google.
202
+ // These entries are kept for historical reference and audit purposes.
203
+ //
204
+ // Fields:
205
+ // deletedAt: ISO timestamp when the contact was deleted
206
+ // _delete: Reason for deletion ('server', 'force', 'queue', etc.)
207
+ //
208
+ `;
209
+ fs.writeFileSync(paths.deletedFile, comment + JSON.stringify(deletedIndex, null, 2));
183
210
  }
184
211
 
185
212
  interface SavedPhoto {
@@ -218,8 +245,23 @@ function savePhotos(paths: UserPaths, photosIndex: PhotosIndex): void {
218
245
  }
219
246
  photosIndex.photos = sorted;
220
247
 
248
+ // Add helpful comments
249
+ const comment = `// _photos.json - Photos preserved during merge operations
250
+ // This file saves photo URLs when duplicate contacts are merged.
251
+ // Photos are collected from source contacts before they're marked for deletion.
252
+ // Also captures photos from contacts deleted by Google during sync.
253
+ //
254
+ // Generated by: 'gfix merge' (primary), 'gcards sync' (deleted contacts)
255
+ // View: photos.html is generated alongside this file
256
+ //
257
+ // Fields:
258
+ // displayName: Contact's display name when photo was saved
259
+ // photos: Array of photo objects with url and sourceType
260
+ // deletedAt: ISO timestamp when the photo was saved
261
+ //
262
+ `;
221
263
  // Save JSON data file
222
- fs.writeFileSync(paths.photosFile, JSON.stringify(photosIndex, null, 2));
264
+ fs.writeFileSync(paths.photosFile, comment + JSON.stringify(photosIndex, null, 2));
223
265
 
224
266
  // Generate HTML view
225
267
  const html = `<!DOCTYPE html>
@@ -272,7 +314,24 @@ function loadDeleteQueue(paths: UserPaths): DeleteQueue {
272
314
 
273
315
  function saveDeleteQueue(paths: UserPaths, queue: DeleteQueue): void {
274
316
  queue.updatedAt = new Date().toISOString();
275
- fs.writeFileSync(paths.deleteQueueFile, JSON.stringify(queue, null, 2));
317
+
318
+ // Add helpful comments
319
+ const comment = `// _delete.json - Pending deletion queue
320
+ // This file contains contacts marked for deletion that require review.
321
+ // Run 'gcards push' to process deletions (will prompt for confirmation unless --yes flag is used).
322
+ //
323
+ // Fields:
324
+ // resourceName: Google resource name (people/xxxxx)
325
+ // displayName: Contact's display name
326
+ // reason: Reason for deletion (e.g., '*photo', '*starred')
327
+ // _delete: Deletion mode ('queue', 'force', etc.)
328
+ // queuedAt: ISO timestamp when added to queue
329
+ //
330
+ // To skip deletion: Remove the entry from this file
331
+ // To force deletion: Change _delete value to 'force'
332
+ //
333
+ `;
334
+ fs.writeFileSync(paths.deleteQueueFile, comment + JSON.stringify(queue, null, 2));
276
335
  }
277
336
 
278
337
  function extractNonDefaultPhotos(contact: GooglePerson): SavedPhoto[] {
@@ -312,7 +371,12 @@ function loadSyncToken(paths: UserPaths): string {
312
371
  }
313
372
 
314
373
  function saveSyncToken(paths: UserPaths, token: string): void {
315
- fs.writeFileSync(paths.syncTokenFile, JSON.stringify({ syncToken: token, savedAt: new Date().toISOString() }, null, 2));
374
+ const comment = `// sync-token.json - Google Contacts API sync token
375
+ // This token enables incremental sync, fetching only changes since last sync.
376
+ // Delete this file to force a full sync on next run.
377
+ //
378
+ `;
379
+ fs.writeFileSync(paths.syncTokenFile, comment + JSON.stringify({ syncToken: token, savedAt: new Date().toISOString() }, null, 2));
316
380
  }
317
381
 
318
382
  async function sleep(ms: number): Promise<void> {
@@ -390,7 +454,14 @@ function saveContact(paths: UserPaths, person: GooglePerson): void {
390
454
 
391
455
  const id = person.resourceName.replace('people/', '');
392
456
  const filePath = path.join(paths.contactsDir, `${id}.json`);
393
- fs.writeFileSync(filePath, JSON.stringify(person, null, 2));
457
+
458
+ // Add helpful comment for contact files
459
+ const comment = `// Contact file from Google People API
460
+ // To delete this contact: Add "_delete": "force" field at root level, then run 'gcards push'
461
+ // To modify: Edit this file, then run 'gcards push' to sync changes to Google
462
+ //
463
+ `;
464
+ fs.writeFileSync(filePath, comment + JSON.stringify(person, null, 2));
394
465
  }
395
466
 
396
467
  function deleteContactFile(paths: UserPaths, resourceName: string): void {
@@ -614,6 +685,30 @@ async function syncContacts(user: string, options: { full: boolean; verbose: boo
614
685
  const activeContacts = Object.keys(index.contacts).length;
615
686
  const tombstones = Object.keys(deletedIndex.deleted).length;
616
687
 
688
+ // Move orphaned files if doing full sync
689
+ let orphaned = 0;
690
+ if (options.full && fs.existsSync(paths.contactsDir)) {
691
+ const contactFiles = fs.readdirSync(paths.contactsDir).filter(f => f.endsWith('.json'));
692
+ const indexedResourceNames = new Set(
693
+ Object.keys(index.contacts).map(rn => rn.replace('people/', '') + '.json')
694
+ );
695
+
696
+ for (const file of contactFiles) {
697
+ if (!indexedResourceNames.has(file)) {
698
+ if (!fs.existsSync(paths.orphansDir)) {
699
+ fs.mkdirSync(paths.orphansDir, { recursive: true });
700
+ }
701
+ const src = path.join(paths.contactsDir, file);
702
+ const dst = path.join(paths.orphansDir, file);
703
+ fs.renameSync(src, dst);
704
+ orphaned++;
705
+ if (options.verbose) {
706
+ console.log(`\n Orphaned: ${file}`);
707
+ }
708
+ }
709
+ }
710
+ }
711
+
617
712
  console.log(`\n\nSync complete:`);
618
713
  console.log(` Processed: ${totalProcessed}`);
619
714
  console.log(` Added: ${added}`);
@@ -622,6 +717,9 @@ async function syncContacts(user: string, options: { full: boolean; verbose: boo
622
717
  if (conflicts > 0) {
623
718
  console.log(` Conflicts: ${conflicts} (delete local file and sync to resolve)`);
624
719
  }
720
+ if (orphaned > 0) {
721
+ console.log(` Orphaned: ${orphaned} (moved to orphans/)`);
722
+ }
625
723
  console.log(` Active contacts: ${activeContacts}`);
626
724
  console.log(` Tombstones: ${tombstones}`);
627
725
  }
package/gfix.ts CHANGED
@@ -26,7 +26,10 @@ function loadDeleteQueue(paths: UserPaths): DeleteQueue {
26
26
 
27
27
  function saveDeleteQueue(paths: UserPaths, queue: DeleteQueue): void {
28
28
  queue.updatedAt = new Date().toISOString();
29
- fs.writeFileSync(paths.deleteQueueFile, JSON.stringify(queue, null, 2));
29
+
30
+ // Add helpful comments
31
+ const comment = `// _delete.json - Pending deletion queue\n// This file contains contacts marked for deletion that require review.\n// Run 'gcards push' to process deletions (will prompt for confirmation unless --yes flag is used).\n//\n// Fields:\n// resourceName: Google resource name (people/xxxxx)\n// displayName: Contact's display name\n// reason: Reason for deletion (e.g., '*photo', '*starred')\n// _delete: Deletion mode ('queue', 'force', etc.)\n// queuedAt: ISO timestamp when added to queue\n//\n// To skip deletion: Remove the entry from this file\n// To force deletion: Change _delete value to 'force'\n//\n`;
32
+ fs.writeFileSync(paths.deleteQueueFile, comment + JSON.stringify(queue, null, 2));
30
33
  }
31
34
 
32
35
  interface BirthdayEntry {
@@ -2326,8 +2329,9 @@ Duplicate detection and merge:
2326
2329
  Note: merge prefers contacts with photos. If all have same photo URL, safe to merge.
2327
2330
 
2328
2331
  Photo review and comparison:
2329
- - gfix photos -u bob # Generate photos.html for visual review
2332
+ - gfix photos -u bob # Generate photos.html showing ALL contacts with photos
2330
2333
  - gfix photocompare -u bob # Find duplicates by URL and content hash
2334
+ - gfix merge -u bob # Generates photos.html with saved photos from merged contacts
2331
2335
  Output: photos.html with 100x100 photos, names, and links to Google Contacts
2332
2336
 
2333
2337
  Export contacts to another user:
package/glib/gctypes.ts CHANGED
@@ -93,6 +93,7 @@ export interface UserPaths {
93
93
  userDir: string;
94
94
  contactsDir: string; /** Active contacts synced from Google */
95
95
  deletedDir: string; /** Backup of deleted contact files */
96
+ orphansDir: string; /** Dead files not in index (moved during sync --full) */
96
97
  toDeleteDir: string; /** User requests to delete (move files here) */
97
98
  toAddDir: string; /** User requests to add new contacts */
98
99
  fixLogDir: string; /** Logs from gfix operations */
package/glib/gutils.ts CHANGED
@@ -58,6 +58,7 @@ export function getUserPaths(user: string): UserPaths {
58
58
  userDir,
59
59
  contactsDir: path.join(userDir, 'contacts'),
60
60
  deletedDir: path.join(userDir, 'deleted'),
61
+ orphansDir: path.join(userDir, 'orphans'),
61
62
  toDeleteDir: path.join(userDir, '_delete'),
62
63
  toAddDir: path.join(userDir, '_add'),
63
64
  fixLogDir: path.join(userDir, 'fix-logs'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcards",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Google Contacts cleanup and management tool",
5
5
  "type": "module",
6
6
  "main": "gcards.ts",