@bobfrankston/gcards 0.1.14 → 0.1.16
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/.claude/settings.local.json +2 -1
- package/gcards.ts +21 -1
- package/gfix.ts +175 -56
- package/glib/gutils.ts +11 -2
- package/package.json +3 -2
package/gcards.ts
CHANGED
|
@@ -79,6 +79,7 @@ function cleanupEscapeHandler(): void {
|
|
|
79
79
|
process.stdin.pause();
|
|
80
80
|
process.stdin.removeAllListeners('data');
|
|
81
81
|
}
|
|
82
|
+
process.stdin.unref(); // Allow Node to exit even if stdin is open
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
async function getAccessToken(user: string, writeAccess = false, forceRefresh = false): Promise<string> {
|
|
@@ -257,7 +258,14 @@ function saveSyncToken(paths: UserPaths, token: string): void {
|
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
async function sleep(ms: number): Promise<void> {
|
|
260
|
-
return new Promise(resolve =>
|
|
261
|
+
return new Promise(resolve => {
|
|
262
|
+
const timeout = setTimeout(resolve, ms);
|
|
263
|
+
// Allow escape to interrupt sleep
|
|
264
|
+
if (escapePressed) {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
resolve();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
async function fetchContactsWithRetry(
|
|
@@ -1053,6 +1061,12 @@ async function pushContacts(user: string, options: { yes: boolean; verbose: bool
|
|
|
1053
1061
|
problems.push(errorMsg);
|
|
1054
1062
|
errorCount++;
|
|
1055
1063
|
|
|
1064
|
+
// Check for etag mismatch - needs resync
|
|
1065
|
+
if (error.message?.includes('FAILED_PRECONDITION') || error.message?.includes('etag')) {
|
|
1066
|
+
console.log(` → Contact changed on Google. Run 'gcards sync' then retry.`);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1056
1070
|
// Get AI explanation if available and error is from API
|
|
1057
1071
|
if (error instanceof ApiError && error.payload) {
|
|
1058
1072
|
if (isAIAvailable()) {
|
|
@@ -1107,6 +1121,12 @@ async function pushContacts(user: string, options: { yes: boolean; verbose: bool
|
|
|
1107
1121
|
const skipped = skippedPhoto + skippedStarred;
|
|
1108
1122
|
const skippedDetails = skipped > 0 ? ` (${skippedPhoto} photo, ${skippedStarred} starred)` : '';
|
|
1109
1123
|
console.log(`\nPush complete: ${successCount} succeeded, ${errorCount} failed, ${skipped} skipped${skippedDetails}`);
|
|
1124
|
+
|
|
1125
|
+
// Check if any errors were etag related
|
|
1126
|
+
const etagErrors = problems.filter(p => p.includes('FAILED_PRECONDITION') || p.includes('etag'));
|
|
1127
|
+
if (etagErrors.length > 0) {
|
|
1128
|
+
console.log(`\n${etagErrors.length} contact(s) had etag conflicts. Run 'gcards sync' to refresh, then retry push.`);
|
|
1129
|
+
}
|
|
1110
1130
|
}
|
|
1111
1131
|
|
|
1112
1132
|
async function main(): Promise<void> {
|
package/gfix.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import readline from 'readline';
|
|
14
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
14
15
|
import type { GooglePerson, GoogleName, GooglePhoneNumber, GoogleEmailAddress, GoogleBirthday } from './glib/types.ts';
|
|
15
16
|
import type { DeleteQueue, DeleteQueueEntry, UserPaths } from './glib/gctypes.ts';
|
|
16
17
|
import { DATA_DIR, resolveUser, getUserPaths, loadIndex, getAllUsers, normalizeUser, matchUsers } from './glib/gutils.ts';
|
|
@@ -746,7 +747,36 @@ function parseFullName(fullName: string): ParsedName {
|
|
|
746
747
|
};
|
|
747
748
|
}
|
|
748
749
|
|
|
749
|
-
/**
|
|
750
|
+
/** Check if string looks like a phone number */
|
|
751
|
+
function looksLikePhone(s: string): boolean {
|
|
752
|
+
const digits = s.replace(/\D/g, '');
|
|
753
|
+
return digits.length >= 7 && digits.length <= 15 && /^[\d\s\-.()+]+$/.test(s);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Fix repeated words in a string - returns cleaned string or null if no fix needed */
|
|
757
|
+
function fixRepeatedWords(s: string): string | null {
|
|
758
|
+
const words = s.split(/\s+/);
|
|
759
|
+
if (words.length < 3) return null;
|
|
760
|
+
|
|
761
|
+
// Count consecutive repeated words
|
|
762
|
+
const seen = new Set<string>();
|
|
763
|
+
const result: string[] = [];
|
|
764
|
+
for (const word of words) {
|
|
765
|
+
const lower = word.toLowerCase().replace(/[.,]+$/, ''); // normalize
|
|
766
|
+
if (!seen.has(lower)) {
|
|
767
|
+
seen.add(lower);
|
|
768
|
+
result.push(word.replace(/[.]+$/, '')); // remove trailing dots
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Only return if we actually removed something
|
|
773
|
+
if (result.length < words.length) {
|
|
774
|
+
return result.join(' ').trim();
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Fix names: parse givenName into first/middle/last, clean fileAs, handle nameless contacts */
|
|
750
780
|
async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<void> {
|
|
751
781
|
const paths = getUserPaths(user);
|
|
752
782
|
|
|
@@ -767,61 +797,93 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
767
797
|
|
|
768
798
|
const changeLogs: NameChangeLog[] = [];
|
|
769
799
|
const problems: string[] = [];
|
|
800
|
+
const needsAction: { file: string; displayName: string; reason: string }[] = [];
|
|
770
801
|
let contactsModified = 0;
|
|
771
802
|
|
|
803
|
+
// Track nameless contacts for deletion
|
|
804
|
+
const emailOnly: { file: string; resourceName: string; email: string }[] = [];
|
|
805
|
+
const phoneOnly: { file: string; resourceName: string; phone: string }[] = [];
|
|
806
|
+
const emailOnlyFile = path.join(paths.userDir, 'emailonly.json');
|
|
807
|
+
const phoneOnlyFile = path.join(paths.userDir, 'phoneonly.json');
|
|
808
|
+
const needsActionFile = path.join(paths.userDir, 'needsaction.txt');
|
|
809
|
+
|
|
772
810
|
for (const file of files) {
|
|
773
811
|
const filePath = path.join(paths.contactsDir, file);
|
|
774
|
-
|
|
812
|
+
let contact: GooglePerson;
|
|
813
|
+
try {
|
|
814
|
+
contact = parseJsonc(fs.readFileSync(filePath, 'utf-8')) as GooglePerson;
|
|
815
|
+
} catch (e: any) {
|
|
816
|
+
needsAction.push({ file, displayName: '(parse error)', reason: e.message });
|
|
817
|
+
console.log(` [NEEDS-ACTION:parse-error] ${file}: ${e.message}`);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
775
820
|
|
|
776
821
|
const name = contact.names?.[0];
|
|
777
|
-
|
|
822
|
+
|
|
823
|
+
// Handle contacts with no names -> DELETE
|
|
824
|
+
if (!name || !name.givenName) {
|
|
825
|
+
const email = contact.emailAddresses?.[0]?.value;
|
|
826
|
+
const phone = contact.phoneNumbers?.[0]?.value;
|
|
827
|
+
if (email) {
|
|
828
|
+
emailOnly.push({ file, resourceName: contact.resourceName, email });
|
|
829
|
+
console.log(` [DELETE:email-only] ${email} (${file})`);
|
|
830
|
+
} else if (phone) {
|
|
831
|
+
phoneOnly.push({ file, resourceName: contact.resourceName, phone });
|
|
832
|
+
console.log(` [DELETE:phone-only] ${phone} (${file})`);
|
|
833
|
+
} else {
|
|
834
|
+
emailOnly.push({ file, resourceName: contact.resourceName, email: '(empty contact)' });
|
|
835
|
+
console.log(` [DELETE:empty] no name/email/phone (${file})`);
|
|
836
|
+
}
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Check if name is actually a phone number -> DELETE
|
|
841
|
+
if (looksLikePhone(name.givenName)) {
|
|
842
|
+
phoneOnly.push({ file, resourceName: contact.resourceName, phone: name.givenName });
|
|
843
|
+
console.log(` [DELETE:phone-only] "${name.givenName}" (${file})`);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Check if name is actually an email address (including malformed like "foo. bar@x. y. com") -> DELETE
|
|
848
|
+
const nameNormalized = name.givenName.replace(/\.\s+/g, '.').replace(/\s+\./g, '.');
|
|
849
|
+
if (nameNormalized.includes('@') && nameNormalized.match(/^[^\s]+@[^\s]+\.[^\s]+$/)) {
|
|
850
|
+
const cleanEmail = nameNormalized.toLowerCase();
|
|
851
|
+
emailOnly.push({ file, resourceName: contact.resourceName, email: cleanEmail });
|
|
852
|
+
console.log(` [DELETE:email-in-name] "${name.givenName}" -> ${cleanEmail} (${file})`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
778
855
|
|
|
779
856
|
const changes: string[] = [];
|
|
780
857
|
let modified = false;
|
|
781
858
|
let parsedFirst = '';
|
|
782
859
|
let parsedLast = '';
|
|
783
860
|
|
|
784
|
-
//
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
861
|
+
// Fix repeated words in givenName (like "Brian Taraci Taraci Taraci...")
|
|
862
|
+
const fixedGivenName = fixRepeatedWords(name.givenName);
|
|
863
|
+
if (fixedGivenName) {
|
|
864
|
+
changes.push(`givenName: "${name.givenName.slice(0, 40)}..." -> "${fixedGivenName}" (removed repeated words)`);
|
|
865
|
+
if (mode === 'apply') {
|
|
866
|
+
name.givenName = fixedGivenName;
|
|
867
|
+
modified = true;
|
|
791
868
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Fix repeated words in familyName
|
|
872
|
+
if (name.familyName) {
|
|
873
|
+
const fixedFamilyName = fixRepeatedWords(name.familyName);
|
|
874
|
+
if (fixedFamilyName) {
|
|
875
|
+
changes.push(`familyName: "${name.familyName.slice(0, 40)}..." -> "${fixedFamilyName}" (removed repeated words)`);
|
|
876
|
+
if (mode === 'apply') {
|
|
877
|
+
name.familyName = fixedFamilyName;
|
|
878
|
+
modified = true;
|
|
879
|
+
}
|
|
797
880
|
}
|
|
798
881
|
}
|
|
799
882
|
|
|
800
883
|
// If givenName exists but no familyName, parse it
|
|
801
884
|
if (name.givenName && !name.familyName) {
|
|
802
|
-
// Check for repeated words (corrupted data like "Smith Smith Smith...")
|
|
803
|
-
const words = name.givenName.split(/\s+/);
|
|
804
|
-
if (words.length > 4) {
|
|
805
|
-
const wordCounts = new Map<string, number>();
|
|
806
|
-
for (const w of words) {
|
|
807
|
-
wordCounts.set(w.toLowerCase(), (wordCounts.get(w.toLowerCase()) || 0) + 1);
|
|
808
|
-
}
|
|
809
|
-
const maxCount = Math.max(...wordCounts.values());
|
|
810
|
-
if (maxCount > 3) {
|
|
811
|
-
const displayName = name.displayName || name.givenName.slice(0, 50) + '...' || 'Unknown';
|
|
812
|
-
problems.push(`${file}: repeated words (${maxCount}x) - corrupted data`);
|
|
813
|
-
console.log(` [CORRUPTED] ${displayName} (${file})`);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
885
|
|
|
818
|
-
//
|
|
819
|
-
if (name.givenName.includes('@')) {
|
|
820
|
-
const displayName = name.displayName || name.givenName || 'Unknown';
|
|
821
|
-
problems.push(`${file}: "${name.givenName}" - email address, not a name`);
|
|
822
|
-
console.log(` [EMAIL] ${displayName} (${file}): "${name.givenName}"`);
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
886
|
+
// Note: email-in-name is now handled earlier as DELETE
|
|
825
887
|
|
|
826
888
|
const parsed = parseFullName(name.givenName);
|
|
827
889
|
|
|
@@ -829,7 +891,7 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
829
891
|
if (parsed.isCompany) {
|
|
830
892
|
const displayName = name.displayName || name.givenName || 'Unknown';
|
|
831
893
|
problems.push(`${file}: "${name.givenName}" - looks like a company name`);
|
|
832
|
-
console.log(` [
|
|
894
|
+
console.log(` [SKIP:company] ${displayName} (${file})`);
|
|
833
895
|
continue;
|
|
834
896
|
}
|
|
835
897
|
|
|
@@ -837,7 +899,7 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
837
899
|
if (parsed.ambiguous) {
|
|
838
900
|
const displayName = name.displayName || name.givenName || 'Unknown';
|
|
839
901
|
problems.push(`${file}: "${name.givenName}" - ambiguous (could be "Last, First" or unknown suffix)`);
|
|
840
|
-
console.log(` [
|
|
902
|
+
console.log(` [SKIP:ambiguous] ${displayName} (${file})`);
|
|
841
903
|
continue;
|
|
842
904
|
}
|
|
843
905
|
|
|
@@ -845,7 +907,7 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
845
907
|
if (!parsed.last) {
|
|
846
908
|
const displayName = name.displayName || name.givenName || 'Unknown';
|
|
847
909
|
problems.push(`${file}: "${name.givenName}" - no last name detected`);
|
|
848
|
-
console.log(` [
|
|
910
|
+
console.log(` [SKIP:no-last-name] ${displayName} (${file})`);
|
|
849
911
|
continue;
|
|
850
912
|
}
|
|
851
913
|
|
|
@@ -997,17 +1059,58 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
997
1059
|
console.log(`\nProblems file: ${problemsPath}`);
|
|
998
1060
|
}
|
|
999
1061
|
|
|
1062
|
+
// Write needsaction.txt for manual fixing
|
|
1063
|
+
if (needsAction.length > 0) {
|
|
1064
|
+
const content = needsAction.map(c => `${c.file}\t${c.displayName}\t${c.reason}`).join('\n');
|
|
1065
|
+
fs.writeFileSync(needsActionFile, content);
|
|
1066
|
+
console.log(`\nNeeds action file: ${needsActionFile}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Handle email-only and phone-only contacts
|
|
1070
|
+
if (mode === 'apply' && (emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1071
|
+
if (!fs.existsSync(paths.toDeleteDir)) {
|
|
1072
|
+
fs.mkdirSync(paths.toDeleteDir, { recursive: true });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (emailOnly.length > 0) {
|
|
1076
|
+
for (const entry of emailOnly) {
|
|
1077
|
+
const src = path.join(paths.contactsDir, entry.file);
|
|
1078
|
+
const dst = path.join(paths.toDeleteDir, entry.file);
|
|
1079
|
+
fs.renameSync(src, dst);
|
|
1080
|
+
}
|
|
1081
|
+
const existing = fs.existsSync(emailOnlyFile) ? JSON.parse(fs.readFileSync(emailOnlyFile, 'utf-8')) : [];
|
|
1082
|
+
existing.push(...emailOnly.map(e => ({ file: e.file, resourceName: e.resourceName, email: e.email, movedAt: new Date().toISOString() })));
|
|
1083
|
+
fs.writeFileSync(emailOnlyFile, JSON.stringify(existing, null, 2));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (phoneOnly.length > 0) {
|
|
1087
|
+
for (const entry of phoneOnly) {
|
|
1088
|
+
const src = path.join(paths.contactsDir, entry.file);
|
|
1089
|
+
const dst = path.join(paths.toDeleteDir, entry.file);
|
|
1090
|
+
fs.renameSync(src, dst);
|
|
1091
|
+
}
|
|
1092
|
+
const existing = fs.existsSync(phoneOnlyFile) ? JSON.parse(fs.readFileSync(phoneOnlyFile, 'utf-8')) : [];
|
|
1093
|
+
existing.push(...phoneOnly.map(e => ({ file: e.file, resourceName: e.resourceName, phone: e.phone, movedAt: new Date().toISOString() })));
|
|
1094
|
+
fs.writeFileSync(phoneOnlyFile, JSON.stringify(existing, null, 2));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1000
1098
|
console.log(`\n${'='.repeat(50)}`);
|
|
1001
1099
|
console.log(`Summary:`);
|
|
1002
1100
|
console.log(` Contacts with changes: ${changeLogs.length}`);
|
|
1101
|
+
console.log(` To delete (email/phone only): ${emailOnly.length + phoneOnly.length}`);
|
|
1102
|
+
console.log(` Needs manual action (see needsaction.txt): ${needsAction.length}`);
|
|
1003
1103
|
console.log(` Ambiguous (see problems.txt): ${problems.length}`);
|
|
1004
1104
|
if (mode === 'apply') {
|
|
1005
1105
|
console.log(` Contacts modified: ${contactsModified}`);
|
|
1106
|
+
if (emailOnly.length > 0 || phoneOnly.length > 0) {
|
|
1107
|
+
console.log(` Moved to _delete/: ${emailOnly.length + phoneOnly.length} (recovery: emailonly.json, phoneonly.json)`);
|
|
1108
|
+
}
|
|
1006
1109
|
}
|
|
1007
|
-
if (mode === 'inspect' && changeLogs.length > 0) {
|
|
1110
|
+
if (mode === 'inspect' && (changeLogs.length > 0 || emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1008
1111
|
console.log(`\nTo apply fixes, run: gfix names -u ${user} --apply`);
|
|
1009
1112
|
}
|
|
1010
|
-
if (mode === 'apply' && contactsModified > 0) {
|
|
1113
|
+
if (mode === 'apply' && (contactsModified > 0 || emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1011
1114
|
console.log(`\nAfter review, run: gcards push -u ${user}`);
|
|
1012
1115
|
}
|
|
1013
1116
|
}
|
|
@@ -1858,26 +1961,42 @@ async function main(): Promise<void> {
|
|
|
1858
1961
|
let targetUser = '';
|
|
1859
1962
|
let applyFlag = false;
|
|
1860
1963
|
const positionalArgs: string[] = [];
|
|
1964
|
+
const unrecognized: string[] = [];
|
|
1861
1965
|
|
|
1862
1966
|
for (let i = 0; i < args.length; i++) {
|
|
1863
1967
|
const arg = args[i];
|
|
1864
|
-
|
|
1865
|
-
user
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1968
|
+
switch (arg) {
|
|
1969
|
+
case '-u': case '--user': case '-user':
|
|
1970
|
+
user = args[++i] || '';
|
|
1971
|
+
break;
|
|
1972
|
+
case '-to': case '--to':
|
|
1973
|
+
targetUser = args[++i] || '';
|
|
1974
|
+
break;
|
|
1975
|
+
case '--apply':
|
|
1976
|
+
applyFlag = true;
|
|
1977
|
+
break;
|
|
1978
|
+
case '-limit': case '--limit':
|
|
1979
|
+
processLimit = parseInt(args[++i], 10) || 0;
|
|
1980
|
+
break;
|
|
1981
|
+
default:
|
|
1982
|
+
if (!arg.startsWith('-')) {
|
|
1983
|
+
if (!command) {
|
|
1984
|
+
command = arg;
|
|
1985
|
+
} else {
|
|
1986
|
+
positionalArgs.push(arg);
|
|
1987
|
+
}
|
|
1988
|
+
} else {
|
|
1989
|
+
unrecognized.push(arg);
|
|
1990
|
+
}
|
|
1878
1991
|
}
|
|
1879
1992
|
}
|
|
1880
1993
|
|
|
1994
|
+
if (unrecognized.length > 0) {
|
|
1995
|
+
console.error(`Unrecognized argument(s): ${unrecognized.join(', ')}`);
|
|
1996
|
+
console.error(`Use 'gfix' for usage information.`);
|
|
1997
|
+
process.exit(1);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1881
2000
|
if (!command) {
|
|
1882
2001
|
showUsage();
|
|
1883
2002
|
return;
|
package/glib/gutils.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
8
9
|
import type { GcardsConfig, ContactIndex, UserPaths } from './gctypes.ts';
|
|
9
10
|
|
|
10
11
|
export const APP_DIR = path.dirname(import.meta.dirname); // Parent of glib/
|
|
@@ -34,7 +35,11 @@ export const CREDENTIALS_FILE = path.join(APP_DIR, 'credentials.json');
|
|
|
34
35
|
|
|
35
36
|
export function loadConfig(): GcardsConfig {
|
|
36
37
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
37
|
-
|
|
38
|
+
try {
|
|
39
|
+
return parseJsonc(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
40
|
+
} catch (e: any) {
|
|
41
|
+
throw new Error(`Failed to parse ${CONFIG_FILE}: ${e.message}`);
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
return {};
|
|
40
45
|
}
|
|
@@ -76,7 +81,11 @@ export function ensureUserDir(user: string): void {
|
|
|
76
81
|
|
|
77
82
|
export function loadIndex(paths: UserPaths): ContactIndex {
|
|
78
83
|
if (fs.existsSync(paths.indexFile)) {
|
|
79
|
-
|
|
84
|
+
try {
|
|
85
|
+
return parseJsonc(fs.readFileSync(paths.indexFile, 'utf-8'));
|
|
86
|
+
} catch (e: any) {
|
|
87
|
+
throw new Error(`Failed to parse ${paths.indexFile}: ${e.message}`);
|
|
88
|
+
}
|
|
80
89
|
}
|
|
81
90
|
return { contacts: {}, lastSync: '' };
|
|
82
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/gcards",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Google Contacts cleanup and management tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "gcards.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"tsx": "^4.21.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@bobfrankston/miscassists": "file:../../../projects/NodeJS/miscassists"
|
|
35
|
+
"@bobfrankston/miscassists": "file:../../../projects/NodeJS/miscassists",
|
|
36
|
+
"jsonc-parser": "^3.3.1"
|
|
36
37
|
}
|
|
37
38
|
}
|