@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 +106 -8
- package/gfix.ts +6 -2
- package/glib/gctypes.ts +1 -0
- package/glib/gutils.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'),
|