@bobfrankston/gcards 0.1.13 → 0.1.15
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/gfix.ts +63 -4
- package/glib/parsecli.ts +39 -18
- package/package.json +1 -1
package/gfix.ts
CHANGED
|
@@ -746,7 +746,13 @@ function parseFullName(fullName: string): ParsedName {
|
|
|
746
746
|
};
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
/**
|
|
749
|
+
/** Check if string looks like a phone number */
|
|
750
|
+
function looksLikePhone(s: string): boolean {
|
|
751
|
+
const digits = s.replace(/\D/g, '');
|
|
752
|
+
return digits.length >= 7 && digits.length <= 15 && /^[\d\s\-.()+]+$/.test(s);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Fix names: parse givenName into first/middle/last, clean fileAs, handle nameless contacts */
|
|
750
756
|
async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<void> {
|
|
751
757
|
const paths = getUserPaths(user);
|
|
752
758
|
|
|
@@ -769,12 +775,32 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
769
775
|
const problems: string[] = [];
|
|
770
776
|
let contactsModified = 0;
|
|
771
777
|
|
|
778
|
+
// Track nameless contacts for deletion
|
|
779
|
+
const emailOnly: { file: string; resourceName: string; email: string }[] = [];
|
|
780
|
+
const phoneOnly: { file: string; resourceName: string; phone: string }[] = [];
|
|
781
|
+
const emailOnlyFile = path.join(paths.userDir, 'emailonly.json');
|
|
782
|
+
const phoneOnlyFile = path.join(paths.userDir, 'phoneonly.json');
|
|
783
|
+
|
|
772
784
|
for (const file of files) {
|
|
773
785
|
const filePath = path.join(paths.contactsDir, file);
|
|
774
786
|
const contact = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as GooglePerson;
|
|
775
787
|
|
|
776
788
|
const name = contact.names?.[0];
|
|
777
|
-
|
|
789
|
+
|
|
790
|
+
// Handle contacts with no names or phone-as-name
|
|
791
|
+
if (!name || !name.givenName) {
|
|
792
|
+
const email = contact.emailAddresses?.[0]?.value || '(no email)';
|
|
793
|
+
emailOnly.push({ file, resourceName: contact.resourceName, email });
|
|
794
|
+
console.log(` [EMAIL-ONLY] ${file}: ${email}`);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Check if name is actually a phone number
|
|
799
|
+
if (looksLikePhone(name.givenName)) {
|
|
800
|
+
phoneOnly.push({ file, resourceName: contact.resourceName, phone: name.givenName });
|
|
801
|
+
console.log(` [PHONE-ONLY] ${file}: "${name.givenName}"`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
778
804
|
|
|
779
805
|
const changes: string[] = [];
|
|
780
806
|
let modified = false;
|
|
@@ -997,17 +1023,50 @@ async function runNamesFix(user: string, mode: 'inspect' | 'apply'): Promise<voi
|
|
|
997
1023
|
console.log(`\nProblems file: ${problemsPath}`);
|
|
998
1024
|
}
|
|
999
1025
|
|
|
1026
|
+
// Handle email-only and phone-only contacts
|
|
1027
|
+
if (mode === 'apply' && (emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1028
|
+
if (!fs.existsSync(paths.toDeleteDir)) {
|
|
1029
|
+
fs.mkdirSync(paths.toDeleteDir, { recursive: true });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (emailOnly.length > 0) {
|
|
1033
|
+
for (const entry of emailOnly) {
|
|
1034
|
+
const src = path.join(paths.contactsDir, entry.file);
|
|
1035
|
+
const dst = path.join(paths.toDeleteDir, entry.file);
|
|
1036
|
+
fs.renameSync(src, dst);
|
|
1037
|
+
}
|
|
1038
|
+
const existing = fs.existsSync(emailOnlyFile) ? JSON.parse(fs.readFileSync(emailOnlyFile, 'utf-8')) : [];
|
|
1039
|
+
existing.push(...emailOnly.map(e => ({ file: e.file, resourceName: e.resourceName, email: e.email, movedAt: new Date().toISOString() })));
|
|
1040
|
+
fs.writeFileSync(emailOnlyFile, JSON.stringify(existing, null, 2));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (phoneOnly.length > 0) {
|
|
1044
|
+
for (const entry of phoneOnly) {
|
|
1045
|
+
const src = path.join(paths.contactsDir, entry.file);
|
|
1046
|
+
const dst = path.join(paths.toDeleteDir, entry.file);
|
|
1047
|
+
fs.renameSync(src, dst);
|
|
1048
|
+
}
|
|
1049
|
+
const existing = fs.existsSync(phoneOnlyFile) ? JSON.parse(fs.readFileSync(phoneOnlyFile, 'utf-8')) : [];
|
|
1050
|
+
existing.push(...phoneOnly.map(e => ({ file: e.file, resourceName: e.resourceName, phone: e.phone, movedAt: new Date().toISOString() })));
|
|
1051
|
+
fs.writeFileSync(phoneOnlyFile, JSON.stringify(existing, null, 2));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1000
1055
|
console.log(`\n${'='.repeat(50)}`);
|
|
1001
1056
|
console.log(`Summary:`);
|
|
1002
1057
|
console.log(` Contacts with changes: ${changeLogs.length}`);
|
|
1058
|
+
console.log(` Email-only (no name): ${emailOnly.length}`);
|
|
1059
|
+
console.log(` Phone-only (phone as name): ${phoneOnly.length}`);
|
|
1003
1060
|
console.log(` Ambiguous (see problems.txt): ${problems.length}`);
|
|
1004
1061
|
if (mode === 'apply') {
|
|
1005
1062
|
console.log(` Contacts modified: ${contactsModified}`);
|
|
1063
|
+
if (emailOnly.length > 0) console.log(` Email-only moved to _delete/ (recovery: emailonly.json)`);
|
|
1064
|
+
if (phoneOnly.length > 0) console.log(` Phone-only moved to _delete/ (recovery: phoneonly.json)`);
|
|
1006
1065
|
}
|
|
1007
|
-
if (mode === 'inspect' && changeLogs.length > 0) {
|
|
1066
|
+
if (mode === 'inspect' && (changeLogs.length > 0 || emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1008
1067
|
console.log(`\nTo apply fixes, run: gfix names -u ${user} --apply`);
|
|
1009
1068
|
}
|
|
1010
|
-
if (mode === 'apply' && contactsModified > 0) {
|
|
1069
|
+
if (mode === 'apply' && (contactsModified > 0 || emailOnly.length > 0 || phoneOnly.length > 0)) {
|
|
1011
1070
|
console.log(`\nAfter review, run: gcards push -u ${user}`);
|
|
1012
1071
|
}
|
|
1013
1072
|
}
|
package/glib/parsecli.ts
CHANGED
|
@@ -27,29 +27,50 @@ export function parseArgs(args: string[]): CliOptions {
|
|
|
27
27
|
since: ''
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const unrecognized: string[] = [];
|
|
31
|
+
|
|
30
32
|
for (let i = 0; i < args.length; i++) {
|
|
31
33
|
const arg = args[i];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
switch (arg) {
|
|
35
|
+
case '--full': case '-full': case '-f':
|
|
36
|
+
options.full = true;
|
|
37
|
+
break;
|
|
38
|
+
case '--yes': case '-y':
|
|
39
|
+
options.yes = true;
|
|
40
|
+
break;
|
|
41
|
+
case '--help': case '-h':
|
|
42
|
+
options.help = true;
|
|
43
|
+
break;
|
|
44
|
+
case '--verbose': case '-v':
|
|
45
|
+
options.verbose = true;
|
|
46
|
+
break;
|
|
47
|
+
case '--all': case '-all': case '-a':
|
|
48
|
+
options.all = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--user': case '-user': case '-u':
|
|
51
|
+
options.user = args[++i] || '';
|
|
52
|
+
break;
|
|
53
|
+
case '--limit': case '-limit':
|
|
54
|
+
options.limit = parseInt(args[++i], 10) || 0;
|
|
55
|
+
break;
|
|
56
|
+
case '--since': case '-since':
|
|
57
|
+
options.since = args[++i] || '';
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
if (!arg.startsWith('-') && !options.command) {
|
|
61
|
+
options.command = arg;
|
|
62
|
+
} else {
|
|
63
|
+
unrecognized.push(arg);
|
|
64
|
+
}
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
if (unrecognized.length > 0) {
|
|
69
|
+
console.error(`Unrecognized argument(s): ${unrecognized.join(', ')}`);
|
|
70
|
+
console.error(`Use --help for usage information.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
return options;
|
|
54
75
|
}
|
|
55
76
|
|