@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.
@@ -22,7 +22,8 @@
22
22
  "Bash(npx tsx:*)",
23
23
  "WebFetch(domain:lh3.googleusercontent.com)",
24
24
  "Bash(curl:*)",
25
- "Bash(bun build:*)"
25
+ "Bash(bun build:*)",
26
+ "Bash(xargs -I {} sh -c 'echo \"\"=== {} ===\"\" && head -20 {}')"
26
27
  ],
27
28
  "deny": [],
28
29
  "ask": []
package/gfix.ts CHANGED
@@ -746,7 +746,13 @@ function parseFullName(fullName: string): ParsedName {
746
746
  };
747
747
  }
748
748
 
749
- /** Fix names: parse givenName into first/middle/last, clean fileAs */
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
- if (!name) continue;
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
- if (arg === '--full' || arg === '-f') {
33
- options.full = true;
34
- } else if (arg === '--yes' || arg === '-y') {
35
- options.yes = true;
36
- } else if (arg === '--help' || arg === '-h') {
37
- options.help = true;
38
- } else if (arg === '--verbose' || arg === '-v') {
39
- options.verbose = true;
40
- } else if (arg === '--all' || arg === '-all' || arg === '-a') {
41
- options.all = true;
42
- } else if ((arg === '--user' || arg === '-user' || arg === '-u') && i + 1 < args.length) {
43
- options.user = args[++i];
44
- } else if ((arg === '--limit' || arg === '-limit') && i + 1 < args.length) {
45
- options.limit = parseInt(args[++i], 10) || 0;
46
- } else if ((arg === '--since' || arg === '-since') && i + 1 < args.length) {
47
- options.since = args[++i];
48
- } else if (!arg.startsWith('-') && !options.command) {
49
- options.command = arg;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcards",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Google Contacts cleanup and management tool",
5
5
  "type": "module",
6
6
  "main": "gcards.ts",