@bobfrankston/gcards 0.1.30 → 0.1.32
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 +9 -2
- package/gfix.ts +49 -28
- package/glib/gctypes.ts +1 -0
- package/package.json +2 -2
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 (First
|
|
132
|
+
/** Get sort key from contact: fileAs (Last, First) > 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='First
|
|
142
|
+
// Sort contacts by sortKey (fileAs='Last, First' > 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) => {
|
|
@@ -157,6 +157,7 @@ function saveIndex(paths: UserPaths, index: ContactIndex): void {
|
|
|
157
157
|
// This file tracks all contacts synced from Google.
|
|
158
158
|
//
|
|
159
159
|
// Special fields:
|
|
160
|
+
// url: Online URL for viewing/editing the contact in Google Contacts
|
|
160
161
|
// _delete: Set to 'force' to mark a contact for deletion on next push
|
|
161
162
|
// Set to 'queue' to mark for interactive deletion review
|
|
162
163
|
// hasPhoto: Indicates contact has a non-default photo
|
|
@@ -633,10 +634,13 @@ async function syncContacts(user: string, options: { full: boolean; verbose: boo
|
|
|
633
634
|
const existingDelete = index.contacts[person.resourceName]?._delete;
|
|
634
635
|
|
|
635
636
|
const sortKey = getSortKey(person);
|
|
637
|
+
const contactId = person.resourceName.split('/')[1];
|
|
638
|
+
const url = `https://contacts.google.com/person/${contactId}`;
|
|
636
639
|
index.contacts[person.resourceName] = {
|
|
637
640
|
resourceName: person.resourceName,
|
|
638
641
|
displayName,
|
|
639
642
|
updatedAt: new Date().toISOString(),
|
|
643
|
+
url,
|
|
640
644
|
sortKey,
|
|
641
645
|
...(hasPhoto && { hasPhoto }),
|
|
642
646
|
...(starred && { starred }),
|
|
@@ -1135,10 +1139,13 @@ async function pushContacts(user: string, options: { yes: boolean; verbose: bool
|
|
|
1135
1139
|
|
|
1136
1140
|
// Add to index
|
|
1137
1141
|
const sortKey = getSortKey(created);
|
|
1142
|
+
const contactId = created.resourceName.split('/')[1];
|
|
1143
|
+
const url = `https://contacts.google.com/person/${contactId}`;
|
|
1138
1144
|
index.contacts[created.resourceName] = {
|
|
1139
1145
|
resourceName: created.resourceName,
|
|
1140
1146
|
displayName: change.displayName,
|
|
1141
1147
|
updatedAt: new Date().toISOString(),
|
|
1148
|
+
url,
|
|
1142
1149
|
sortKey,
|
|
1143
1150
|
...(hasPhoto && { hasPhoto }),
|
|
1144
1151
|
...(starred && { starred }),
|
package/gfix.ts
CHANGED
|
@@ -395,7 +395,7 @@ async function runBirthdayExtract(user: string, mode: 'inspect' | 'apply'): Prom
|
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
/** Fix fileAs fields: normalize "Last, First"
|
|
398
|
+
/** Fix fileAs fields: normalize to "Last, First" format and clean spurious characters
|
|
399
399
|
* Also fix displayNameLastFirst to proper "Last, First" format */
|
|
400
400
|
async function runFileAsFix(user: string, mode: 'inspect' | 'apply'): Promise<void> {
|
|
401
401
|
const paths = getUserPaths(user);
|
|
@@ -438,42 +438,63 @@ async function runFileAsFix(user: string, mode: 'inspect' | 'apply'): Promise<vo
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
// Fix fileAs: "Last, First"
|
|
441
|
+
// Fix fileAs: should be "Last, First" format
|
|
442
442
|
if (contact.fileAses?.length) {
|
|
443
443
|
const fileAs = contact.fileAses[0].value || '';
|
|
444
|
+
const expectedFileAs = givenName && familyName ? `${familyName}, ${givenName}` : '';
|
|
444
445
|
|
|
445
|
-
// Check
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
446
|
+
// Check for malformed fileAs (e.g., "-, Eric Giler", "- Eric", etc.)
|
|
447
|
+
const malformedMatch = fileAs.match(/^[-,\s]+(.+)$/);
|
|
448
|
+
if (malformedMatch && givenName && familyName) {
|
|
449
|
+
// Spurious dash/comma prefix
|
|
450
|
+
console.log(` ${displayName}: fileAs "${fileAs}" -> "${expectedFileAs}" (cleaned malformed)`);
|
|
451
|
+
if (mode === 'apply') {
|
|
452
|
+
contact.fileAses[0].value = expectedFileAs;
|
|
453
|
+
modified = true;
|
|
454
|
+
}
|
|
455
|
+
fileAsFixedCount++;
|
|
456
|
+
} else if (expectedFileAs && fileAs !== expectedFileAs) {
|
|
457
|
+
// Check if fileAs is "First Last" format (wrong) or just doesn't match
|
|
458
|
+
const isFirstLast = fileAs === `${givenName} ${familyName}`;
|
|
459
|
+
const commaMatch = fileAs.match(/^(.+),\s*(.+)$/);
|
|
460
|
+
|
|
461
|
+
if (isFirstLast) {
|
|
462
|
+
// Wrong format: "First Last" instead of "Last, First"
|
|
463
|
+
console.log(` ${displayName}: fileAs "${fileAs}" -> "${expectedFileAs}" (wrong format)`);
|
|
464
|
+
if (mode === 'apply') {
|
|
465
|
+
contact.fileAses[0].value = expectedFileAs;
|
|
466
|
+
modified = true;
|
|
467
|
+
}
|
|
468
|
+
fileAsFixedCount++;
|
|
469
|
+
} else if (commaMatch) {
|
|
470
|
+
const [, lastPart, firstPart] = commaMatch;
|
|
471
|
+
// Verify the parts match
|
|
472
|
+
const firstMatches = firstPart.trim().toLowerCase() === givenName.toLowerCase();
|
|
473
|
+
const lastMatches = lastPart.trim().toLowerCase() === familyName.toLowerCase();
|
|
474
|
+
|
|
475
|
+
if (!firstMatches || !lastMatches) {
|
|
476
|
+
// Parts don't match - needs review
|
|
468
477
|
needsReview.push({ givenName, familyName, fileAs, jsonFile: file });
|
|
469
478
|
skippedCount++;
|
|
470
479
|
}
|
|
471
|
-
} else {
|
|
472
|
-
//
|
|
473
|
-
needsReview.push({ givenName
|
|
480
|
+
} else if (fileAs && !commaMatch) {
|
|
481
|
+
// Not in "Last, First" format at all
|
|
482
|
+
needsReview.push({ givenName, familyName, fileAs, jsonFile: file });
|
|
474
483
|
skippedCount++;
|
|
475
484
|
}
|
|
476
485
|
}
|
|
486
|
+
} else if (givenName && familyName) {
|
|
487
|
+
// Missing fileAs - add it
|
|
488
|
+
const expectedFileAs = `${familyName}, ${givenName}`;
|
|
489
|
+
console.log(` ${displayName}: Adding fileAs "${expectedFileAs}"`);
|
|
490
|
+
if (mode === 'apply') {
|
|
491
|
+
contact.fileAses = [{
|
|
492
|
+
metadata: { primary: true },
|
|
493
|
+
value: expectedFileAs
|
|
494
|
+
}];
|
|
495
|
+
modified = true;
|
|
496
|
+
}
|
|
497
|
+
fileAsFixedCount++;
|
|
477
498
|
}
|
|
478
499
|
|
|
479
500
|
if (modified) {
|
package/glib/gctypes.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface IndexEntry {
|
|
|
29
29
|
resourceName: string;
|
|
30
30
|
displayName: string;
|
|
31
31
|
updatedAt: string;
|
|
32
|
+
url?: string; /** Online URL for viewing/editing contact */
|
|
32
33
|
sortKey?: string; /** Sort key: fileAs, displayNameLastFirst, or displayName */
|
|
33
34
|
hasPhoto?: boolean; /** Has non-default photo */
|
|
34
35
|
starred?: boolean; /** In starred/favorites group */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/gcards",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Google Contacts cleanup and management tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "gcards.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"author": "Bob Frankston",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^
|
|
32
|
+
"@types/node": "^25.0.3",
|
|
33
33
|
"tsx": "^4.21.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|