inboxd 1.0.12 → 1.1.0

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/src/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { program } = require('commander');
4
- const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, archiveEmails, groupEmailsBySender, getEmailContent, searchEmails, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
4
+ const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, markAsUnread, archiveEmails, unarchiveEmails, groupEmailsBySender, getEmailContent, searchEmails, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
5
+ const { logArchives, getRecentArchives, getArchiveLogPath, removeArchiveLogEntries } = require('./archive-log');
5
6
  const { getState, updateLastCheck, markEmailsSeen, getNewEmailIds, clearOldSeenEmails } = require('./state');
6
7
  const { notifyNewEmails } = require('./notifier');
7
8
  const { authorize, addAccount, getAccounts, getAccountEmail, removeAccount, removeAllAccounts, renameTokenFile, validateCredentialsFile, hasCredentials, isConfigured, installCredentials } = require('./gmail-auth');
8
- const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries } = require('./deletion-log');
9
+ const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries, getStats: getDeletionStats, analyzePatterns } = require('./deletion-log');
9
10
  const { getSkillStatus, checkForUpdate, installSkill, SKILL_DEST_DIR, SOURCE_MARKER } = require('./skill-installer');
10
- const { logSentEmail, getSentLogPath } = require('./sent-log');
11
+ const { logSentEmail, getSentLogPath, getSentStats } = require('./sent-log');
11
12
  const readline = require('readline');
12
13
  const path = require('path');
13
14
  const os = require('os');
@@ -378,8 +379,15 @@ async function main() {
378
379
  program
379
380
  .command('accounts')
380
381
  .description('List all configured accounts')
381
- .action(async () => {
382
+ .option('--json', 'Output as JSON')
383
+ .action(async (options) => {
382
384
  const accounts = getAccounts();
385
+
386
+ if (options.json) {
387
+ console.log(JSON.stringify({ accounts }, null, 2));
388
+ return;
389
+ }
390
+
383
391
  if (accounts.length === 0) {
384
392
  console.log(chalk.gray('No accounts configured. Run: inbox setup'));
385
393
  return;
@@ -805,9 +813,18 @@ async function main() {
805
813
  }
806
814
 
807
815
  if (!options.confirm) {
808
- console.log(chalk.yellow('\nThis will send the email above.'));
809
- console.log(chalk.gray('Use --confirm to skip this prompt, or --dry-run to preview without sending.\n'));
810
- return;
816
+ const rl = readline.createInterface({
817
+ input: process.stdin,
818
+ output: process.stdout,
819
+ });
820
+
821
+ const answer = await prompt(rl, chalk.yellow('\nSend this email? (y/N): '));
822
+ rl.close();
823
+
824
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
825
+ console.log(chalk.gray('Cancelled. Email was not sent.\n'));
826
+ return;
827
+ }
811
828
  }
812
829
 
813
830
  console.log(chalk.cyan('\nSending...'));
@@ -888,9 +905,18 @@ async function main() {
888
905
  }
889
906
 
890
907
  if (!options.confirm) {
891
- console.log(chalk.yellow('\nThis will send the reply above.'));
892
- console.log(chalk.gray('Use --confirm to skip this prompt, or --dry-run to preview without sending.\n'));
893
- return;
908
+ const rl = readline.createInterface({
909
+ input: process.stdin,
910
+ output: process.stdout,
911
+ });
912
+
913
+ const answer = await prompt(rl, chalk.yellow('\nSend this reply? (y/N): '));
914
+ rl.close();
915
+
916
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
917
+ console.log(chalk.gray('Cancelled. Reply was not sent.\n'));
918
+ return;
919
+ }
894
920
  }
895
921
 
896
922
  console.log(chalk.cyan('\nSending reply...'));
@@ -933,6 +959,7 @@ async function main() {
933
959
  .option('--confirm', 'Skip confirmation prompt')
934
960
  .option('--dry-run', 'Show what would be deleted without deleting')
935
961
  .option('--force', 'Override safety warnings (required for short patterns or large matches)')
962
+ .option('--json', 'Output as JSON (for --dry-run)')
936
963
  .action(async (options) => {
937
964
  try {
938
965
  let emailsToDelete = [];
@@ -1075,6 +1102,20 @@ async function main() {
1075
1102
  });
1076
1103
 
1077
1104
  if (options.dryRun) {
1105
+ if (options.json) {
1106
+ console.log(JSON.stringify({
1107
+ dryRun: true,
1108
+ count: emailsToDelete.length,
1109
+ emails: emailsToDelete.map(e => ({
1110
+ id: e.id,
1111
+ account: e.account || 'default',
1112
+ from: e.from,
1113
+ subject: e.subject,
1114
+ date: e.date
1115
+ }))
1116
+ }, null, 2));
1117
+ return;
1118
+ }
1078
1119
  console.log(chalk.yellow(`\nDry run: ${emailsToDelete.length} email(s) would be deleted.`));
1079
1120
  // Output IDs for programmatic use
1080
1121
  console.log(chalk.gray(`\nIDs: ${emailsToDelete.map(e => e.id).join(',')}`));
@@ -1140,10 +1181,21 @@ async function main() {
1140
1181
  .command('deletion-log')
1141
1182
  .description('View recent email deletions')
1142
1183
  .option('-n, --days <number>', 'Show deletions from last N days', '30')
1184
+ .option('--json', 'Output as JSON')
1143
1185
  .action(async (options) => {
1144
1186
  const days = parseInt(options.days, 10);
1145
1187
  const deletions = getRecentDeletions(days);
1146
1188
 
1189
+ if (options.json) {
1190
+ console.log(JSON.stringify({
1191
+ days,
1192
+ count: deletions.length,
1193
+ logPath: getLogPath(),
1194
+ deletions
1195
+ }, null, 2));
1196
+ return;
1197
+ }
1198
+
1147
1199
  if (deletions.length === 0) {
1148
1200
  console.log(chalk.gray(`No deletions in the last ${days} days.`));
1149
1201
  console.log(chalk.gray(`Log file: ${getLogPath()}`));
@@ -1174,11 +1226,131 @@ async function main() {
1174
1226
  console.log(chalk.gray(`\nLog file: ${getLogPath()}`));
1175
1227
  });
1176
1228
 
1229
+ program
1230
+ .command('stats')
1231
+ .description('Show email activity statistics')
1232
+ .option('-n, --days <number>', 'Period in days', '30')
1233
+ .option('--json', 'Output as JSON')
1234
+ .action(async (options) => {
1235
+ const days = parseInt(options.days, 10);
1236
+ const deletionStats = getDeletionStats(days);
1237
+ const sentStats = getSentStats(days);
1238
+
1239
+ if (options.json) {
1240
+ console.log(JSON.stringify({
1241
+ period: days,
1242
+ deleted: deletionStats,
1243
+ sent: sentStats,
1244
+ }, null, 2));
1245
+ return;
1246
+ }
1247
+
1248
+ console.log(boxen(chalk.bold(`Email Activity (Last ${days} days)`), {
1249
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
1250
+ borderStyle: 'round',
1251
+ borderColor: 'cyan',
1252
+ }));
1253
+
1254
+ // Deletion stats
1255
+ console.log(chalk.bold('\nDeleted: ') + chalk.white(`${deletionStats.total} emails`));
1256
+
1257
+ if (deletionStats.total > 0) {
1258
+ // By account
1259
+ const accountEntries = Object.entries(deletionStats.byAccount);
1260
+ if (accountEntries.length > 0) {
1261
+ const accountStr = accountEntries.map(([acc, cnt]) => `${acc} (${cnt})`).join(', ');
1262
+ console.log(chalk.gray(` By account: ${accountStr}`));
1263
+ }
1264
+
1265
+ // Top senders
1266
+ if (deletionStats.topSenders.length > 0) {
1267
+ const senderStr = deletionStats.topSenders
1268
+ .slice(0, 5)
1269
+ .map(s => `${s.domain} (${s.count})`)
1270
+ .join(', ');
1271
+ console.log(chalk.gray(` Top senders: ${senderStr}`));
1272
+ }
1273
+ }
1274
+
1275
+ // Sent stats
1276
+ console.log(chalk.bold('\nSent: ') + chalk.white(`${sentStats.total} emails`));
1277
+
1278
+ if (sentStats.total > 0) {
1279
+ console.log(chalk.gray(` Replies: ${sentStats.replies}, New: ${sentStats.newEmails}`));
1280
+
1281
+ const accountEntries = Object.entries(sentStats.byAccount);
1282
+ if (accountEntries.length > 1) {
1283
+ const accountStr = accountEntries.map(([acc, cnt]) => `${acc} (${cnt})`).join(', ');
1284
+ console.log(chalk.gray(` By account: ${accountStr}`));
1285
+ }
1286
+ }
1287
+
1288
+ console.log('');
1289
+ });
1290
+
1291
+ program
1292
+ .command('cleanup-suggest')
1293
+ .description('Get smart cleanup suggestions based on deletion patterns')
1294
+ .option('-n, --days <number>', 'Period to analyze', '30')
1295
+ .option('--json', 'Output as JSON')
1296
+ .action(async (options) => {
1297
+ const days = parseInt(options.days, 10);
1298
+ const analysis = analyzePatterns(days);
1299
+
1300
+ if (options.json) {
1301
+ console.log(JSON.stringify(analysis, null, 2));
1302
+ return;
1303
+ }
1304
+
1305
+ console.log(boxen(chalk.bold('Cleanup Suggestions'), {
1306
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
1307
+ borderStyle: 'round',
1308
+ borderColor: 'cyan',
1309
+ }));
1310
+
1311
+ if (analysis.totalDeleted === 0) {
1312
+ console.log(chalk.gray(`\nNo deletions in the last ${days} days to analyze.\n`));
1313
+ return;
1314
+ }
1315
+
1316
+ console.log(chalk.gray(`\nBased on ${analysis.totalDeleted} deletions in the last ${days} days:\n`));
1317
+
1318
+ // Frequent deleters
1319
+ if (analysis.frequentDeleters.length > 0) {
1320
+ console.log(chalk.bold('You frequently delete emails from:'));
1321
+ analysis.frequentDeleters.slice(0, 5).forEach(sender => {
1322
+ console.log(chalk.yellow(` ${sender.domain}`) + chalk.gray(` (${sender.deletedCount} deleted this month)`));
1323
+ console.log(chalk.cyan(` → ${sender.suggestion}`));
1324
+ });
1325
+ console.log('');
1326
+ }
1327
+
1328
+ // Never-read senders
1329
+ if (analysis.neverReadSenders.length > 0) {
1330
+ console.log(chalk.bold('Never-read senders (deleted unread):'));
1331
+ analysis.neverReadSenders.slice(0, 5).forEach(sender => {
1332
+ console.log(chalk.yellow(` ${sender.domain}`) + chalk.gray(` (${sender.deletedCount} emails)`));
1333
+ console.log(chalk.cyan(` → ${sender.suggestion}`));
1334
+ });
1335
+ console.log('');
1336
+ }
1337
+
1338
+ if (analysis.frequentDeleters.length === 0 && analysis.neverReadSenders.length === 0) {
1339
+ console.log(chalk.gray('No strong patterns detected. Your inbox management looks good!\n'));
1340
+ }
1341
+
1342
+ // Helpful tip
1343
+ console.log(chalk.gray('Tip: Use these commands to act on suggestions:'));
1344
+ console.log(chalk.gray(' inbox delete --sender "domain.com" --dry-run'));
1345
+ console.log(chalk.gray(' inbox search -q "from:sender@domain.com"\n'));
1346
+ });
1347
+
1177
1348
  program
1178
1349
  .command('restore')
1179
1350
  .description('Restore deleted emails from trash')
1180
1351
  .option('--ids <ids>', 'Comma-separated message IDs to restore')
1181
1352
  .option('--last <number>', 'Restore the N most recent deletions', parseInt)
1353
+ .option('--json', 'Output as JSON')
1182
1354
  .action(async (options) => {
1183
1355
  try {
1184
1356
  let emailsToRestore = [];
@@ -1258,11 +1430,40 @@ async function main() {
1258
1430
  // Clean up log
1259
1431
  if (successfulIds.length > 0) {
1260
1432
  removeLogEntries(successfulIds);
1261
- console.log(chalk.gray(`\nRemoved ${successfulIds.length} entries from deletion log.`));
1433
+ if (!options.json) {
1434
+ console.log(chalk.gray(`\nRemoved ${successfulIds.length} entries from deletion log.`));
1435
+ }
1436
+ }
1437
+
1438
+ // JSON output at the end
1439
+ if (options.json) {
1440
+ const allResults = [];
1441
+ for (const [account, emails] of Object.entries(byAccount)) {
1442
+ const ids = emails.map(e => e.id);
1443
+ emails.forEach(e => {
1444
+ const wasSuccessful = successfulIds.includes(e.id);
1445
+ allResults.push({
1446
+ id: e.id,
1447
+ account,
1448
+ from: e.from,
1449
+ subject: e.subject,
1450
+ success: wasSuccessful
1451
+ });
1452
+ });
1453
+ }
1454
+ console.log(JSON.stringify({
1455
+ restored: successfulIds.length,
1456
+ failed: emailsToRestore.length - successfulIds.length,
1457
+ results: allResults
1458
+ }, null, 2));
1262
1459
  }
1263
1460
 
1264
1461
  } catch (error) {
1265
- console.error(chalk.red('Error restoring emails:'), error.message);
1462
+ if (options.json) {
1463
+ console.log(JSON.stringify({ error: error.message }, null, 2));
1464
+ } else {
1465
+ console.error(chalk.red('Error restoring emails:'), error.message);
1466
+ }
1266
1467
  process.exit(1);
1267
1468
  }
1268
1469
  });
@@ -1325,6 +1526,64 @@ async function main() {
1325
1526
  }
1326
1527
  });
1327
1528
 
1529
+ program
1530
+ .command('mark-unread')
1531
+ .description('Mark emails as unread')
1532
+ .requiredOption('--ids <ids>', 'Comma-separated message IDs to mark as unread')
1533
+ .option('-a, --account <name>', 'Account name')
1534
+ .action(async (options) => {
1535
+ try {
1536
+ const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
1537
+
1538
+ if (ids.length === 0) {
1539
+ console.log(chalk.yellow('No message IDs provided.'));
1540
+ return;
1541
+ }
1542
+
1543
+ // Get account - if not specified, try to find from configured accounts
1544
+ let account = options.account;
1545
+ if (!account) {
1546
+ const accounts = getAccounts();
1547
+ if (accounts.length === 1) {
1548
+ account = accounts[0].name;
1549
+ } else if (accounts.length > 1) {
1550
+ console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
1551
+ console.log(chalk.gray('Available accounts:'));
1552
+ accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
1553
+ return;
1554
+ } else {
1555
+ account = 'default';
1556
+ }
1557
+ }
1558
+
1559
+ console.log(chalk.cyan(`Marking ${ids.length} email(s) as unread...`));
1560
+
1561
+ const results = await markAsUnread(account, ids);
1562
+
1563
+ const succeeded = results.filter(r => r.success).length;
1564
+ const failed = results.filter(r => !r.success).length;
1565
+
1566
+ if (succeeded > 0) {
1567
+ console.log(chalk.green(`\nMarked ${succeeded} email(s) as unread.`));
1568
+ }
1569
+ if (failed > 0) {
1570
+ console.log(chalk.red(`Failed to mark ${failed} email(s) as unread.`));
1571
+ results.filter(r => !r.success).forEach(r => {
1572
+ console.log(chalk.red(` - ${r.id}: ${r.error}`));
1573
+ });
1574
+ }
1575
+
1576
+ } catch (error) {
1577
+ if (error.message.includes('403') || error.code === 403) {
1578
+ console.error(chalk.red('Permission denied. You may need to re-authenticate with updated scopes.'));
1579
+ console.error(chalk.yellow('Run: inbox auth -a <account>'));
1580
+ } else {
1581
+ console.error(chalk.red('Error marking emails as unread:'), error.message);
1582
+ }
1583
+ process.exit(1);
1584
+ }
1585
+ });
1586
+
1328
1587
  program
1329
1588
  .command('archive')
1330
1589
  .description('Archive emails (remove from inbox, keep in All Mail)')
@@ -1388,6 +1647,11 @@ async function main() {
1388
1647
  console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
1389
1648
  }
1390
1649
 
1650
+ // Log archives BEFORE actually archiving (for undo)
1651
+ const emailsWithAccount = emailsToArchive.map(e => ({ ...e, account }));
1652
+ logArchives(emailsWithAccount);
1653
+ console.log(chalk.gray(`Logged to: ${getArchiveLogPath()}`));
1654
+
1391
1655
  const results = await archiveEmails(account, emailsToArchive.map(e => e.id));
1392
1656
 
1393
1657
  const succeeded = results.filter(r => r.success).length;
@@ -1395,6 +1659,7 @@ async function main() {
1395
1659
 
1396
1660
  if (succeeded > 0) {
1397
1661
  console.log(chalk.green(`\nArchived ${succeeded} email(s).`));
1662
+ console.log(chalk.gray(`Tip: Use 'inbox unarchive --last ${succeeded}' to undo.`));
1398
1663
  }
1399
1664
  if (failed > 0) {
1400
1665
  console.log(chalk.red(`Failed to archive ${failed} email(s).`));
@@ -1414,22 +1679,271 @@ async function main() {
1414
1679
  }
1415
1680
  });
1416
1681
 
1682
+ program
1683
+ .command('unarchive')
1684
+ .description('Restore archived emails back to inbox')
1685
+ .option('--ids <ids>', 'Comma-separated message IDs to unarchive')
1686
+ .option('--last <number>', 'Unarchive the N most recent archives', parseInt)
1687
+ .option('--json', 'Output as JSON')
1688
+ .action(async (options) => {
1689
+ try {
1690
+ let emailsToUnarchive = [];
1691
+
1692
+ // Scenario 1: Unarchive by explicit IDs
1693
+ if (options.ids) {
1694
+ const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
1695
+ const log = getRecentArchives(30);
1696
+
1697
+ for (const id of ids) {
1698
+ // Find in log first to get the account
1699
+ const entry = log.find(e => e.id === id);
1700
+ if (entry) {
1701
+ emailsToUnarchive.push(entry);
1702
+ } else {
1703
+ if (!options.json) {
1704
+ console.log(chalk.yellow(`Warning: ID ${id} not found in local archive log.`));
1705
+ console.log(chalk.gray(`Cannot determine account automatically. Please unarchive manually via Gmail.`));
1706
+ }
1707
+ }
1708
+ }
1709
+ }
1710
+ // Scenario 2: Unarchive last N items
1711
+ else if (options.last) {
1712
+ const count = options.last;
1713
+ const archives = getRecentArchives(30);
1714
+ // Sort by archivedAt desc
1715
+ archives.sort((a, b) => new Date(b.archivedAt) - new Date(a.archivedAt));
1716
+
1717
+ emailsToUnarchive = archives.slice(0, count);
1718
+ } else {
1719
+ if (options.json) {
1720
+ console.log(JSON.stringify({ error: 'Must specify either --ids or --last' }, null, 2));
1721
+ } else {
1722
+ console.log(chalk.red('Error: Must specify either --ids or --last'));
1723
+ console.log(chalk.gray('Examples:'));
1724
+ console.log(chalk.gray(' inbox unarchive --last 1'));
1725
+ console.log(chalk.gray(' inbox unarchive --ids 12345,67890'));
1726
+ }
1727
+ return;
1728
+ }
1729
+
1730
+ if (emailsToUnarchive.length === 0) {
1731
+ if (options.json) {
1732
+ console.log(JSON.stringify({ unarchived: 0, failed: 0, results: [] }, null, 2));
1733
+ } else {
1734
+ console.log(chalk.yellow('No emails found to unarchive.'));
1735
+ }
1736
+ return;
1737
+ }
1738
+
1739
+ if (!options.json) {
1740
+ console.log(chalk.cyan(`Unarchiving ${emailsToUnarchive.length} email(s)...`));
1741
+ }
1742
+
1743
+ // Group by account to batch API calls
1744
+ const byAccount = {};
1745
+ for (const email of emailsToUnarchive) {
1746
+ if (!byAccount[email.account]) {
1747
+ byAccount[email.account] = [];
1748
+ }
1749
+ byAccount[email.account].push(email);
1750
+ }
1751
+
1752
+ const successfulIds = [];
1753
+
1754
+ for (const [account, emails] of Object.entries(byAccount)) {
1755
+ const ids = emails.map(e => e.id);
1756
+ if (!options.json) {
1757
+ console.log(chalk.gray(`Unarchiving ${ids.length} email(s) for account "${account}"...`));
1758
+ }
1759
+
1760
+ const results = await unarchiveEmails(account, ids);
1761
+
1762
+ const succeeded = results.filter(r => r.success);
1763
+ const failed = results.filter(r => !r.success);
1764
+
1765
+ if (!options.json) {
1766
+ if (succeeded.length > 0) {
1767
+ console.log(chalk.green(` ✓ Unarchived ${succeeded.length} email(s)`));
1768
+ }
1769
+ if (failed.length > 0) {
1770
+ console.log(chalk.red(` ✗ Failed to unarchive ${failed.length} email(s)`));
1771
+ failed.forEach(r => {
1772
+ console.log(chalk.gray(` - ID ${r.id}: ${r.error}`));
1773
+ });
1774
+ }
1775
+ }
1776
+
1777
+ successfulIds.push(...succeeded.map(r => r.id));
1778
+ }
1779
+
1780
+ // Clean up log
1781
+ if (successfulIds.length > 0) {
1782
+ removeArchiveLogEntries(successfulIds);
1783
+ if (!options.json) {
1784
+ console.log(chalk.gray(`\nRemoved ${successfulIds.length} entries from archive log.`));
1785
+ }
1786
+ }
1787
+
1788
+ // JSON output
1789
+ if (options.json) {
1790
+ const allResults = [];
1791
+ for (const [account, emails] of Object.entries(byAccount)) {
1792
+ emails.forEach(e => {
1793
+ const wasSuccessful = successfulIds.includes(e.id);
1794
+ allResults.push({
1795
+ id: e.id,
1796
+ account,
1797
+ from: e.from,
1798
+ subject: e.subject,
1799
+ success: wasSuccessful
1800
+ });
1801
+ });
1802
+ }
1803
+ console.log(JSON.stringify({
1804
+ unarchived: successfulIds.length,
1805
+ failed: emailsToUnarchive.length - successfulIds.length,
1806
+ results: allResults
1807
+ }, null, 2));
1808
+ }
1809
+
1810
+ } catch (error) {
1811
+ if (options.json) {
1812
+ console.log(JSON.stringify({ error: error.message }, null, 2));
1813
+ } else {
1814
+ console.error(chalk.red('Error unarchiving emails:'), error.message);
1815
+ }
1816
+ process.exit(1);
1817
+ }
1818
+ });
1819
+
1417
1820
  program
1418
1821
  .command('install-service')
1419
- .description('Install background service (launchd) for macOS')
1822
+ .description('Install background service (launchd for macOS, systemd for Linux)')
1420
1823
  .option('-i, --interval <minutes>', 'Check interval in minutes', '5')
1824
+ .option('--uninstall', 'Remove the service instead of installing')
1421
1825
  .action(async (options) => {
1422
- // Platform check
1826
+ const { execSync } = require('child_process');
1827
+ const interval = parseInt(options.interval, 10);
1828
+ const homeDir = os.homedir();
1829
+
1830
+ // Linux systemd support
1831
+ if (process.platform === 'linux') {
1832
+ const systemdUserDir = path.join(homeDir, '.config/systemd/user');
1833
+ const servicePath = path.join(systemdUserDir, 'inboxd.service');
1834
+ const timerPath = path.join(systemdUserDir, 'inboxd.timer');
1835
+
1836
+ // Uninstall
1837
+ if (options.uninstall) {
1838
+ try {
1839
+ execSync('systemctl --user stop inboxd.timer 2>/dev/null', { stdio: 'ignore' });
1840
+ execSync('systemctl --user disable inboxd.timer 2>/dev/null', { stdio: 'ignore' });
1841
+ } catch {
1842
+ // Ignore - may not be running
1843
+ }
1844
+
1845
+ let removed = false;
1846
+ if (fs.existsSync(servicePath)) {
1847
+ fs.unlinkSync(servicePath);
1848
+ removed = true;
1849
+ }
1850
+ if (fs.existsSync(timerPath)) {
1851
+ fs.unlinkSync(timerPath);
1852
+ removed = true;
1853
+ }
1854
+
1855
+ if (removed) {
1856
+ try {
1857
+ execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
1858
+ } catch {
1859
+ // Ignore
1860
+ }
1861
+ console.log(chalk.green('\n✓ Service uninstalled.'));
1862
+ console.log(chalk.gray(` Removed: ${servicePath}`));
1863
+ console.log(chalk.gray(` Removed: ${timerPath}\n`));
1864
+ } else {
1865
+ console.log(chalk.gray('\nService was not installed.\n'));
1866
+ }
1867
+ return;
1868
+ }
1869
+
1870
+ // Install
1871
+ const nodePath = process.execPath;
1872
+ const scriptPath = path.resolve(__dirname, 'cli.js');
1873
+ const workingDir = path.resolve(__dirname, '..');
1874
+
1875
+ const serviceContent = `[Unit]
1876
+ Description=inboxd - Gmail monitoring and notifications
1877
+ After=network-online.target
1878
+ Wants=network-online.target
1879
+
1880
+ [Service]
1881
+ Type=oneshot
1882
+ ExecStart=${nodePath} ${scriptPath} check --quiet
1883
+ WorkingDirectory=${workingDir}
1884
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
1885
+
1886
+ [Install]
1887
+ WantedBy=default.target
1888
+ `;
1889
+
1890
+ const timerContent = `[Unit]
1891
+ Description=Run inboxd every ${interval} minutes
1892
+
1893
+ [Timer]
1894
+ OnBootSec=1min
1895
+ OnUnitActiveSec=${interval}min
1896
+ Persistent=true
1897
+
1898
+ [Install]
1899
+ WantedBy=timers.target
1900
+ `;
1901
+
1902
+ try {
1903
+ if (!fs.existsSync(systemdUserDir)) {
1904
+ fs.mkdirSync(systemdUserDir, { recursive: true });
1905
+ }
1906
+
1907
+ fs.writeFileSync(servicePath, serviceContent);
1908
+ fs.writeFileSync(timerPath, timerContent);
1909
+
1910
+ // Reload and enable
1911
+ execSync('systemctl --user daemon-reload');
1912
+ execSync('systemctl --user enable inboxd.timer');
1913
+ execSync('systemctl --user start inboxd.timer');
1914
+
1915
+ console.log(chalk.green('\n✓ Background service installed and running!'));
1916
+ console.log(chalk.gray(` Service: ${servicePath}`));
1917
+ console.log(chalk.gray(` Timer: ${timerPath}`));
1918
+ console.log(chalk.gray(` Interval: every ${interval} minutes\n`));
1919
+ console.log(chalk.white('The service will:'));
1920
+ console.log(chalk.gray(' • Check your inbox automatically'));
1921
+ console.log(chalk.gray(' • Send notifications for new emails'));
1922
+ console.log(chalk.gray(' • Start on login\n'));
1923
+ console.log(chalk.white('Useful commands:'));
1924
+ console.log(chalk.cyan(' systemctl --user status inboxd.timer') + chalk.gray(' # Check status'));
1925
+ console.log(chalk.cyan(' journalctl --user -u inboxd') + chalk.gray(' # View logs'));
1926
+ console.log(chalk.cyan(' inbox install-service --uninstall') + chalk.gray(' # Remove service\n'));
1927
+ } catch (error) {
1928
+ console.error(chalk.red('Error installing service:'), error.message);
1929
+ console.log(chalk.yellow('\nThe config files may have been created but could not be enabled.'));
1930
+ console.log(chalk.white('Try running manually:'));
1931
+ console.log(chalk.cyan(' systemctl --user daemon-reload'));
1932
+ console.log(chalk.cyan(' systemctl --user enable --now inboxd.timer\n'));
1933
+ }
1934
+ return;
1935
+ }
1936
+
1937
+ // Platform check for unsupported platforms
1423
1938
  if (process.platform !== 'darwin') {
1424
- console.log(chalk.red('\nError: install-service is only supported on macOS.'));
1425
- console.log(chalk.gray('This command uses launchd which is macOS-specific.\n'));
1939
+ console.log(chalk.red('\nError: install-service is only supported on macOS and Linux.'));
1940
+ console.log(chalk.gray('This command uses launchd (macOS) or systemd (Linux).\n'));
1426
1941
  console.log(chalk.white('For other platforms, you can set up a cron job or scheduled task:'));
1427
1942
  console.log(chalk.cyan(` */5 * * * * ${process.execPath} ${path.resolve(__dirname, 'cli.js')} check --quiet`));
1428
1943
  console.log('');
1429
1944
  return;
1430
1945
  }
1431
1946
 
1432
- const interval = parseInt(options.interval, 10);
1433
1947
  const seconds = interval * 60;
1434
1948
 
1435
1949
  // Determine paths