inboxd 1.0.13 → 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, markAsUnread, 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
  });
@@ -1446,6 +1647,11 @@ async function main() {
1446
1647
  console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
1447
1648
  }
1448
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
+
1449
1655
  const results = await archiveEmails(account, emailsToArchive.map(e => e.id));
1450
1656
 
1451
1657
  const succeeded = results.filter(r => r.success).length;
@@ -1453,6 +1659,7 @@ async function main() {
1453
1659
 
1454
1660
  if (succeeded > 0) {
1455
1661
  console.log(chalk.green(`\nArchived ${succeeded} email(s).`));
1662
+ console.log(chalk.gray(`Tip: Use 'inbox unarchive --last ${succeeded}' to undo.`));
1456
1663
  }
1457
1664
  if (failed > 0) {
1458
1665
  console.log(chalk.red(`Failed to archive ${failed} email(s).`));
@@ -1472,22 +1679,271 @@ async function main() {
1472
1679
  }
1473
1680
  });
1474
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
+
1475
1820
  program
1476
1821
  .command('install-service')
1477
- .description('Install background service (launchd) for macOS')
1822
+ .description('Install background service (launchd for macOS, systemd for Linux)')
1478
1823
  .option('-i, --interval <minutes>', 'Check interval in minutes', '5')
1824
+ .option('--uninstall', 'Remove the service instead of installing')
1479
1825
  .action(async (options) => {
1480
- // 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
1481
1938
  if (process.platform !== 'darwin') {
1482
- console.log(chalk.red('\nError: install-service is only supported on macOS.'));
1483
- 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'));
1484
1941
  console.log(chalk.white('For other platforms, you can set up a cron job or scheduled task:'));
1485
1942
  console.log(chalk.cyan(` */5 * * * * ${process.execPath} ${path.resolve(__dirname, 'cli.js')} check --quiet`));
1486
1943
  console.log('');
1487
1944
  return;
1488
1945
  }
1489
1946
 
1490
- const interval = parseInt(options.interval, 10);
1491
1947
  const seconds = interval * 60;
1492
1948
 
1493
1949
  // Determine paths