@bobfrankston/gcards 0.1.5 → 0.1.6
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/README.md +23 -0
- package/gcards.ts +2 -1
- package/gfix.ts +360 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ gfix birthday -u user --apply # Extract to CSV and remove
|
|
|
47
47
|
|
|
48
48
|
gfix undup -u user # Find duplicate contacts -> merger.json
|
|
49
49
|
gfix merge -u user # Merge duplicates locally (then use gcards push)
|
|
50
|
+
|
|
51
|
+
gfix export -u source -to target "pattern" # Export contacts to another user
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
## Google People API Fields
|
|
@@ -217,6 +219,27 @@ gcards sync -a
|
|
|
217
219
|
gcards push -a
|
|
218
220
|
```
|
|
219
221
|
|
|
222
|
+
## Exporting Contacts Between Users
|
|
223
|
+
|
|
224
|
+
Transfer contacts from one user's account to another:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
gfix export -u bob -to alice "John*" # Export contacts matching "John*"
|
|
228
|
+
gfix export -u bob -to ali "*@example.com" # Export by email (partial user match)
|
|
229
|
+
gfix export -u bob -to alice c1234567890 # Export specific contact by ID
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Features:**
|
|
233
|
+
- `-u` source user (partial match supported)
|
|
234
|
+
- `-to` target user (partial match - "ali" matches "alice" if unique)
|
|
235
|
+
- Pattern supports `*` (any chars) and `?` (single char) wildcards
|
|
236
|
+
- Matches on: displayName, givenName, familyName, "Last, First", email
|
|
237
|
+
- Searches both `contacts/` and `deleted/` directories
|
|
238
|
+
- Multiple matches prompt for selection: number(s), `*` for all, `q` to quit
|
|
239
|
+
- Creates cleaned file in target's `_add/` as `x<sourceId>.json`
|
|
240
|
+
|
|
241
|
+
After export, run `gcards push -u <target>` to upload.
|
|
242
|
+
|
|
220
243
|
## Setup
|
|
221
244
|
|
|
222
245
|
1. Create Google Cloud project
|
package/gcards.ts
CHANGED
|
@@ -79,7 +79,8 @@ async function getAccessToken(user: string, writeAccess = false, forceRefresh =
|
|
|
79
79
|
tokenDirectory: paths.userDir,
|
|
80
80
|
tokenFileName,
|
|
81
81
|
credentialsKey: 'web',
|
|
82
|
-
includeOfflineAccess: true
|
|
82
|
+
includeOfflineAccess: true,
|
|
83
|
+
prompt: 'select_account' // Always show account picker for new auth
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
if (!token) {
|
package/gfix.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
|
+
import readline from 'readline';
|
|
13
14
|
import type { GooglePerson, GoogleName, GooglePhoneNumber, GoogleEmailAddress, GoogleBirthday } from './glib/types.ts';
|
|
14
15
|
import type { DeleteQueue, DeleteQueueEntry, UserPaths } from './glib/gctypes.ts';
|
|
15
|
-
import { DATA_DIR, resolveUser, getUserPaths, loadIndex } from './glib/gutils.ts';
|
|
16
|
+
import { DATA_DIR, resolveUser, getUserPaths, loadIndex, getAllUsers, normalizeUser } from './glib/gutils.ts';
|
|
16
17
|
import { mergeContacts as mergeContactData, collectSourcePhotos, type MergeEntry, type PhotoEntry } from './glib/gmerge.ts';
|
|
17
18
|
|
|
18
19
|
function loadDeleteQueue(paths: UserPaths): DeleteQueue {
|
|
@@ -1405,6 +1406,321 @@ async function runMerge(user: string, limit: number): Promise<void> {
|
|
|
1405
1406
|
console.log(`\nRun 'gcards push -u ${user}' to apply changes to Google.`);
|
|
1406
1407
|
}
|
|
1407
1408
|
|
|
1409
|
+
// ============================================================
|
|
1410
|
+
// Export Feature - Transfer contacts between users
|
|
1411
|
+
// ============================================================
|
|
1412
|
+
|
|
1413
|
+
interface ContactMatch {
|
|
1414
|
+
id: string; /** JSON file id (without .json) */
|
|
1415
|
+
displayName: string;
|
|
1416
|
+
emails: string[];
|
|
1417
|
+
filePath: string;
|
|
1418
|
+
source: 'contacts' | 'deleted';
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/** Find a contact by ID in contacts or deleted directories */
|
|
1422
|
+
function findContactById(paths: UserPaths, id: string): { contact: GooglePerson; filePath: string; source: 'contacts' | 'deleted' } {
|
|
1423
|
+
const cleanId = id.replace(/\.json$/i, '');
|
|
1424
|
+
|
|
1425
|
+
// Check contacts directory
|
|
1426
|
+
const contactPath = path.join(paths.contactsDir, `${cleanId}.json`);
|
|
1427
|
+
if (fs.existsSync(contactPath)) {
|
|
1428
|
+
return {
|
|
1429
|
+
contact: JSON.parse(fs.readFileSync(contactPath, 'utf-8')),
|
|
1430
|
+
filePath: contactPath,
|
|
1431
|
+
source: 'contacts'
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Check deleted directory
|
|
1436
|
+
const deletedPath = path.join(paths.deletedDir, `${cleanId}.json`);
|
|
1437
|
+
if (fs.existsSync(deletedPath)) {
|
|
1438
|
+
return {
|
|
1439
|
+
contact: JSON.parse(fs.readFileSync(deletedPath, 'utf-8')),
|
|
1440
|
+
filePath: deletedPath,
|
|
1441
|
+
source: 'deleted'
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Match users by partial name - returns matching user names */
|
|
1449
|
+
function matchUsers(pattern: string): string[] {
|
|
1450
|
+
const allUsers = getAllUsers();
|
|
1451
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
1452
|
+
|
|
1453
|
+
return allUsers.filter(user => user.toLowerCase().includes(normalizedPattern));
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** Convert wildcard pattern to regex (supports * and ?) */
|
|
1457
|
+
function wildcardToRegex(pattern: string): RegExp {
|
|
1458
|
+
const escaped = pattern
|
|
1459
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
1460
|
+
.replace(/\*/g, '.*') // * matches anything
|
|
1461
|
+
.replace(/\?/g, '.'); // ? matches single char
|
|
1462
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** Search contacts by name or email pattern */
|
|
1466
|
+
function searchContacts(paths: UserPaths, pattern: string): ContactMatch[] {
|
|
1467
|
+
const regex = wildcardToRegex(pattern);
|
|
1468
|
+
const matches: ContactMatch[] = [];
|
|
1469
|
+
|
|
1470
|
+
// Search in contacts directory
|
|
1471
|
+
if (fs.existsSync(paths.contactsDir)) {
|
|
1472
|
+
for (const file of fs.readdirSync(paths.contactsDir).filter(f => f.endsWith('.json'))) {
|
|
1473
|
+
const filePath = path.join(paths.contactsDir, file);
|
|
1474
|
+
const contact = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as GooglePerson;
|
|
1475
|
+
if (matchesContact(contact, regex)) {
|
|
1476
|
+
matches.push({
|
|
1477
|
+
id: file.replace(/\.json$/, ''),
|
|
1478
|
+
displayName: getContactDisplayName(contact),
|
|
1479
|
+
emails: (contact.emailAddresses || []).map(e => e.value).filter(Boolean) as string[],
|
|
1480
|
+
filePath,
|
|
1481
|
+
source: 'contacts'
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Search in deleted directory
|
|
1488
|
+
if (fs.existsSync(paths.deletedDir)) {
|
|
1489
|
+
for (const file of fs.readdirSync(paths.deletedDir).filter(f => f.endsWith('.json'))) {
|
|
1490
|
+
const filePath = path.join(paths.deletedDir, file);
|
|
1491
|
+
const contact = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as GooglePerson;
|
|
1492
|
+
if (matchesContact(contact, regex)) {
|
|
1493
|
+
matches.push({
|
|
1494
|
+
id: file.replace(/\.json$/, ''),
|
|
1495
|
+
displayName: getContactDisplayName(contact),
|
|
1496
|
+
emails: (contact.emailAddresses || []).map(e => e.value).filter(Boolean) as string[],
|
|
1497
|
+
filePath,
|
|
1498
|
+
source: 'deleted'
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return matches;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Check if contact matches regex on name or email */
|
|
1508
|
+
function matchesContact(contact: GooglePerson, regex: RegExp): boolean {
|
|
1509
|
+
// Check display name
|
|
1510
|
+
const name = contact.names?.[0];
|
|
1511
|
+
if (name) {
|
|
1512
|
+
if (regex.test(name.displayName || '')) return true;
|
|
1513
|
+
if (regex.test(name.givenName || '')) return true;
|
|
1514
|
+
if (regex.test(name.familyName || '')) return true;
|
|
1515
|
+
// "Last, First" format
|
|
1516
|
+
if (name.givenName && name.familyName) {
|
|
1517
|
+
if (regex.test(`${name.familyName}, ${name.givenName}`)) return true;
|
|
1518
|
+
if (regex.test(`${name.givenName} ${name.familyName}`)) return true;
|
|
1519
|
+
}
|
|
1520
|
+
if (regex.test(name.displayNameLastFirst || '')) return true;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Check emails
|
|
1524
|
+
for (const email of contact.emailAddresses || []) {
|
|
1525
|
+
if (regex.test(email.value || '')) return true;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** Get display name for a contact */
|
|
1532
|
+
function getContactDisplayName(contact: GooglePerson): string {
|
|
1533
|
+
const name = contact.names?.[0];
|
|
1534
|
+
return name?.displayName || name?.givenName || name?.familyName || 'Unknown';
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/** Clean contact for export - remove Google-specific metadata */
|
|
1538
|
+
function cleanContactForExport(contact: GooglePerson): GooglePerson {
|
|
1539
|
+
const cleaned = { ...contact };
|
|
1540
|
+
|
|
1541
|
+
// Remove resource-specific fields that will be regenerated
|
|
1542
|
+
delete cleaned.resourceName;
|
|
1543
|
+
delete cleaned.etag;
|
|
1544
|
+
delete (cleaned as any)._delete;
|
|
1545
|
+
delete (cleaned as any)._deleted;
|
|
1546
|
+
|
|
1547
|
+
// Remove metadata that links to source account
|
|
1548
|
+
if (cleaned.metadata) {
|
|
1549
|
+
delete cleaned.metadata.sources;
|
|
1550
|
+
delete cleaned.metadata.previousResourceNames;
|
|
1551
|
+
delete cleaned.metadata.linkedPeopleResourceNames;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Clean field metadata (source references)
|
|
1555
|
+
const cleanFieldMetadata = (items: any[]) => {
|
|
1556
|
+
if (!items) return;
|
|
1557
|
+
for (const item of items) {
|
|
1558
|
+
if (item.metadata) {
|
|
1559
|
+
delete item.metadata.source;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
cleanFieldMetadata(cleaned.names);
|
|
1565
|
+
cleanFieldMetadata(cleaned.emailAddresses);
|
|
1566
|
+
cleanFieldMetadata(cleaned.phoneNumbers);
|
|
1567
|
+
cleanFieldMetadata(cleaned.addresses);
|
|
1568
|
+
cleanFieldMetadata(cleaned.organizations);
|
|
1569
|
+
cleanFieldMetadata(cleaned.urls);
|
|
1570
|
+
cleanFieldMetadata(cleaned.biographies);
|
|
1571
|
+
cleanFieldMetadata(cleaned.photos);
|
|
1572
|
+
cleanFieldMetadata(cleaned.birthdays);
|
|
1573
|
+
cleanFieldMetadata(cleaned.relations);
|
|
1574
|
+
cleanFieldMetadata(cleaned.events);
|
|
1575
|
+
cleanFieldMetadata(cleaned.memberships);
|
|
1576
|
+
cleanFieldMetadata(cleaned.userDefined);
|
|
1577
|
+
|
|
1578
|
+
return cleaned;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/** Prompt user for selection when multiple matches */
|
|
1582
|
+
async function promptSelection(matches: ContactMatch[]): Promise<string[]> {
|
|
1583
|
+
console.log('\nMultiple contacts found:');
|
|
1584
|
+
for (let i = 0; i < matches.length; i++) {
|
|
1585
|
+
const m = matches[i];
|
|
1586
|
+
const emailStr = m.emails.length > 0 ? ` <${m.emails[0]}>` : '';
|
|
1587
|
+
const srcStr = m.source === 'deleted' ? ' [deleted]' : '';
|
|
1588
|
+
console.log(` ${i + 1}. ${m.displayName}${emailStr}${srcStr} (${m.id})`);
|
|
1589
|
+
}
|
|
1590
|
+
console.log('\nEnter selection: number(s) separated by commas, * for all, or q to quit');
|
|
1591
|
+
|
|
1592
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1593
|
+
|
|
1594
|
+
return new Promise((resolve) => {
|
|
1595
|
+
rl.question('> ', (answer) => {
|
|
1596
|
+
rl.close();
|
|
1597
|
+
const trimmed = answer.trim().toLowerCase();
|
|
1598
|
+
|
|
1599
|
+
if (trimmed === 'q' || trimmed === '') {
|
|
1600
|
+
resolve([]);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (trimmed === '*') {
|
|
1605
|
+
resolve(matches.map(m => m.id));
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Parse comma-separated numbers
|
|
1610
|
+
const selected: string[] = [];
|
|
1611
|
+
for (const part of trimmed.split(',')) {
|
|
1612
|
+
const num = parseInt(part.trim(), 10);
|
|
1613
|
+
if (num >= 1 && num <= matches.length) {
|
|
1614
|
+
selected.push(matches[num - 1].id);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
resolve(selected);
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** Export contacts from source user to target user */
|
|
1623
|
+
async function runExport(sourceUser: string, targetPattern: string, contactPattern: string): Promise<void> {
|
|
1624
|
+
// Resolve target user by partial match
|
|
1625
|
+
const targetMatches = matchUsers(targetPattern);
|
|
1626
|
+
|
|
1627
|
+
if (targetMatches.length === 0) {
|
|
1628
|
+
console.error(`No users match pattern: ${targetPattern}`);
|
|
1629
|
+
console.error(`Available users: ${getAllUsers().join(', ')}`);
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (targetMatches.length > 1) {
|
|
1634
|
+
console.error(`Multiple users match pattern "${targetPattern}":`);
|
|
1635
|
+
for (const u of targetMatches) {
|
|
1636
|
+
console.error(` - ${u}`);
|
|
1637
|
+
}
|
|
1638
|
+
console.error('Please be more specific.');
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const targetUser = targetMatches[0];
|
|
1643
|
+
|
|
1644
|
+
if (normalizeUser(sourceUser) === normalizeUser(targetUser)) {
|
|
1645
|
+
console.error('Source and target users cannot be the same.');
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const sourcePaths = getUserPaths(sourceUser);
|
|
1650
|
+
const targetPaths = getUserPaths(targetUser);
|
|
1651
|
+
|
|
1652
|
+
// Ensure target _add directory exists
|
|
1653
|
+
if (!fs.existsSync(targetPaths.toAddDir)) {
|
|
1654
|
+
fs.mkdirSync(targetPaths.toAddDir, { recursive: true });
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Find contacts to export
|
|
1658
|
+
let contactIds: string[] = [];
|
|
1659
|
+
|
|
1660
|
+
// Check if contactPattern looks like an ID (alphanumeric, possibly with .json)
|
|
1661
|
+
const cleanPattern = contactPattern.replace(/\.json$/i, '');
|
|
1662
|
+
const found = findContactById(sourcePaths, cleanPattern);
|
|
1663
|
+
|
|
1664
|
+
if (found) {
|
|
1665
|
+
// Direct ID match
|
|
1666
|
+
contactIds = [cleanPattern];
|
|
1667
|
+
} else {
|
|
1668
|
+
// Search by name/email pattern
|
|
1669
|
+
const matches = searchContacts(sourcePaths, contactPattern);
|
|
1670
|
+
|
|
1671
|
+
if (matches.length === 0) {
|
|
1672
|
+
console.error(`No contacts match pattern: ${contactPattern}`);
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (matches.length === 1) {
|
|
1677
|
+
contactIds = [matches[0].id];
|
|
1678
|
+
} else {
|
|
1679
|
+
contactIds = await promptSelection(matches);
|
|
1680
|
+
if (contactIds.length === 0) {
|
|
1681
|
+
console.log('No contacts selected.');
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Export each selected contact
|
|
1688
|
+
let exported = 0;
|
|
1689
|
+
for (const id of contactIds) {
|
|
1690
|
+
const result = findContactById(sourcePaths, id);
|
|
1691
|
+
if (!result) {
|
|
1692
|
+
console.error(`Contact not found: ${id}`);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const { contact, source } = result;
|
|
1697
|
+
const displayName = getContactDisplayName(contact);
|
|
1698
|
+
|
|
1699
|
+
// Clean the contact for export
|
|
1700
|
+
const cleaned = cleanContactForExport(contact);
|
|
1701
|
+
|
|
1702
|
+
// Generate unique filename: x<sourceId>.json
|
|
1703
|
+
const exportFileName = `x${id}.json`;
|
|
1704
|
+
const exportPath = path.join(targetPaths.toAddDir, exportFileName);
|
|
1705
|
+
|
|
1706
|
+
// Check for existing file
|
|
1707
|
+
if (fs.existsSync(exportPath)) {
|
|
1708
|
+
console.log(` [SKIP] ${displayName} - already exists: ${exportFileName}`);
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
fs.writeFileSync(exportPath, JSON.stringify(cleaned, null, 2));
|
|
1713
|
+
console.log(` ${displayName} -> ${targetUser}/_add/${exportFileName}${source === 'deleted' ? ' [from deleted]' : ''}`);
|
|
1714
|
+
exported++;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
console.log(`\n${'='.repeat(50)}`);
|
|
1718
|
+
console.log(`Exported: ${exported} contact(s) to ${targetUser}/_add/`);
|
|
1719
|
+
if (exported > 0) {
|
|
1720
|
+
console.log(`\nTo upload, run: gcards push -u ${targetUser}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1408
1724
|
/** Reset ALL delete markers: index.json, contact files, _delete.json, _delete/ folder */
|
|
1409
1725
|
async function runReset(user: string): Promise<void> {
|
|
1410
1726
|
const paths = getUserPaths(user);
|
|
@@ -1486,9 +1802,11 @@ Commands:
|
|
|
1486
1802
|
undup Find duplicate contacts (same name + overlapping email) -> merger.json
|
|
1487
1803
|
merge Merge duplicates locally (then use gcards push)
|
|
1488
1804
|
reset Clear all _delete flags from index.json
|
|
1805
|
+
export Transfer contact(s) to another user's _add directory
|
|
1489
1806
|
|
|
1490
1807
|
Options:
|
|
1491
|
-
-u, --user <name> User profile to process
|
|
1808
|
+
-u, --user <name> User profile to process (source for export)
|
|
1809
|
+
-to <user> Target user for export (partial match OK)
|
|
1492
1810
|
--apply For birthday/fileas/names: actually apply changes
|
|
1493
1811
|
-limit <n> Process only first n contacts (for testing)
|
|
1494
1812
|
|
|
@@ -1518,6 +1836,16 @@ Duplicate detection and merge:
|
|
|
1518
1836
|
- gcards push -u bob # Push changes to Google
|
|
1519
1837
|
- gcards sync -u bob --full # Resync to get updated contacts
|
|
1520
1838
|
|
|
1839
|
+
Export contacts to another user:
|
|
1840
|
+
- gfix export -u bob -to alice "John*" # Export contacts matching "John*" to alice
|
|
1841
|
+
- gfix export -u bob -to ali "*@example.com" # Export by email pattern (ali matches alice)
|
|
1842
|
+
- gfix export -u bob -to alice c1234567890 # Export by contact ID (with or without .json)
|
|
1843
|
+
- Pattern supports * (any chars) and ? (single char) wildcards
|
|
1844
|
+
- Matches on: displayName, givenName, familyName, "Last, First", email
|
|
1845
|
+
- Searches both contacts/ and deleted/ directories
|
|
1846
|
+
- Multiple matches prompt for selection: number(s), * for all, q to quit
|
|
1847
|
+
- Creates cleaned file in target's _add/ directory as x<sourceId>.json
|
|
1848
|
+
|
|
1521
1849
|
Workflow:
|
|
1522
1850
|
1. gfix inspect -u bob # Review proposed changes
|
|
1523
1851
|
2. gfix apply -u bob # Apply changes to local files
|
|
@@ -1533,18 +1861,26 @@ async function main(): Promise<void> {
|
|
|
1533
1861
|
|
|
1534
1862
|
let command = '';
|
|
1535
1863
|
let user = '';
|
|
1864
|
+
let targetUser = '';
|
|
1536
1865
|
let applyFlag = false;
|
|
1866
|
+
const positionalArgs: string[] = [];
|
|
1537
1867
|
|
|
1538
1868
|
for (let i = 0; i < args.length; i++) {
|
|
1539
1869
|
const arg = args[i];
|
|
1540
1870
|
if ((arg === '-u' || arg === '--user' || arg === '-user') && i + 1 < args.length) {
|
|
1541
1871
|
user = args[++i];
|
|
1872
|
+
} else if ((arg === '-to' || arg === '--to') && i + 1 < args.length) {
|
|
1873
|
+
targetUser = args[++i];
|
|
1542
1874
|
} else if (arg === '--apply') {
|
|
1543
1875
|
applyFlag = true;
|
|
1544
1876
|
} else if ((arg === '-limit' || arg === '--limit') && i + 1 < args.length) {
|
|
1545
1877
|
processLimit = parseInt(args[++i], 10) || 0;
|
|
1546
|
-
} else if (!arg.startsWith('-')
|
|
1547
|
-
command
|
|
1878
|
+
} else if (!arg.startsWith('-')) {
|
|
1879
|
+
if (!command) {
|
|
1880
|
+
command = arg;
|
|
1881
|
+
} else {
|
|
1882
|
+
positionalArgs.push(arg);
|
|
1883
|
+
}
|
|
1548
1884
|
}
|
|
1549
1885
|
}
|
|
1550
1886
|
|
|
@@ -1553,6 +1889,26 @@ async function main(): Promise<void> {
|
|
|
1553
1889
|
return;
|
|
1554
1890
|
}
|
|
1555
1891
|
|
|
1892
|
+
// Export command has different argument handling
|
|
1893
|
+
if (command === 'export') {
|
|
1894
|
+
if (!user) {
|
|
1895
|
+
console.error('Export requires -u <sourceUser>');
|
|
1896
|
+
process.exit(1);
|
|
1897
|
+
}
|
|
1898
|
+
if (!targetUser) {
|
|
1899
|
+
console.error('Export requires -to <targetUser>');
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
if (positionalArgs.length === 0) {
|
|
1903
|
+
console.error('Export requires a contact pattern or ID');
|
|
1904
|
+
console.error('Usage: gfix export -u <source> -to <target> <pattern>');
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
const resolvedSource = resolveUser(user);
|
|
1908
|
+
await runExport(resolvedSource, targetUser, positionalArgs[0]);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1556
1912
|
const resolvedUser = resolveUser(user);
|
|
1557
1913
|
|
|
1558
1914
|
if (processLimit > 0) {
|