@prnv/tuck 1.2.1 → 1.3.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/dist/index.js CHANGED
@@ -351,6 +351,22 @@ var init_table = __esm({
351
351
  }
352
352
  });
353
353
 
354
+ // src/ui/progress.ts
355
+ import chalk6 from "chalk";
356
+ import ora2 from "ora";
357
+ var ICONS;
358
+ var init_progress = __esm({
359
+ "src/ui/progress.ts"() {
360
+ "use strict";
361
+ ICONS = {
362
+ pending: chalk6.dim("\u25CB"),
363
+ in_progress: chalk6.cyan("\u25CF"),
364
+ completed: chalk6.green("\u2713"),
365
+ error: chalk6.red("\u2717")
366
+ };
367
+ }
368
+ });
369
+
354
370
  // src/ui/index.ts
355
371
  var init_ui = __esm({
356
372
  "src/ui/index.ts"() {
@@ -360,6 +376,7 @@ var init_ui = __esm({
360
376
  init_prompts();
361
377
  init_spinner();
362
378
  init_table();
379
+ init_progress();
363
380
  }
364
381
  });
365
382
 
@@ -445,7 +462,7 @@ var init_constants = __esm({
445
462
 
446
463
  // src/lib/paths.ts
447
464
  import { homedir as homedir2 } from "os";
448
- import { join as join2, basename, dirname as dirname2, relative, isAbsolute, resolve } from "path";
465
+ import { join as join2, basename, dirname as dirname2, relative, isAbsolute, resolve, sep } from "path";
449
466
  import { stat, access } from "fs/promises";
450
467
  import { constants } from "fs";
451
468
  var expandPath, collapsePath, getTuckDir, getManifestPath, getConfigPath, getFilesDir, getCategoryDir, getDestinationPath, getRelativeDestination, sanitizeFilename, detectCategory, pathExists, isDirectory, isPathWithinHome, validateSafeSourcePath, generateFileId;
@@ -531,7 +548,7 @@ var init_paths = __esm({
531
548
  const expandedPath = expandPath(path);
532
549
  const normalizedPath = resolve(expandedPath);
533
550
  const normalizedHome = resolve(home);
534
- return normalizedPath.startsWith(normalizedHome + "/") || normalizedPath === normalizedHome;
551
+ return normalizedPath.startsWith(normalizedHome + sep) || normalizedPath === normalizedHome;
535
552
  };
536
553
  validateSafeSourcePath = (source) => {
537
554
  if (isAbsolute(source) && !source.startsWith(homedir2())) {
@@ -628,7 +645,7 @@ var init_config_schema = __esm({
628
645
  });
629
646
 
630
647
  // src/errors.ts
631
- import chalk6 from "chalk";
648
+ import chalk7 from "chalk";
632
649
  var TuckError, NotInitializedError, AlreadyInitializedError, FileNotFoundError, FileNotTrackedError, FileAlreadyTrackedError, GitError, ConfigError, ManifestError, PermissionError, GitHubCliError, BackupError, handleError;
633
650
  var init_errors = __esm({
634
651
  "src/errors.ts"() {
@@ -725,22 +742,22 @@ var init_errors = __esm({
725
742
  };
726
743
  handleError = (error) => {
727
744
  if (error instanceof TuckError) {
728
- console.error(chalk6.red("x"), error.message);
745
+ console.error(chalk7.red("x"), error.message);
729
746
  if (error.suggestions && error.suggestions.length > 0) {
730
747
  console.error();
731
- console.error(chalk6.dim("Suggestions:"));
732
- error.suggestions.forEach((s) => console.error(chalk6.dim(` \u2192 ${s}`)));
748
+ console.error(chalk7.dim("Suggestions:"));
749
+ error.suggestions.forEach((s) => console.error(chalk7.dim(` \u2192 ${s}`)));
733
750
  }
734
751
  process.exit(1);
735
752
  }
736
753
  if (error instanceof Error) {
737
- console.error(chalk6.red("x"), "An unexpected error occurred:", error.message);
754
+ console.error(chalk7.red("x"), "An unexpected error occurred:", error.message);
738
755
  if (process.env.DEBUG) {
739
756
  console.error(error.stack);
740
757
  }
741
758
  process.exit(1);
742
759
  }
743
- console.error(chalk6.red("x"), "An unknown error occurred");
760
+ console.error(chalk7.red("x"), "An unknown error occurred");
744
761
  process.exit(1);
745
762
  };
746
763
  }
@@ -1483,9 +1500,10 @@ var init_github = __esm({
1483
1500
 
1484
1501
  // src/lib/files.ts
1485
1502
  import { createHash } from "crypto";
1486
- import { readFile as readFile5, stat as stat4, readdir as readdir3, copyFile, symlink, unlink, rm as rm3 } from "fs/promises";
1487
- import { copy as copy3, ensureDir as ensureDir3 } from "fs-extra";
1488
- import { join as join7, dirname as dirname4 } from "path";
1503
+ import { readFile as readFile4, stat as stat4, lstat, readdir as readdir3, copyFile, symlink, unlink, rm as rm2 } from "fs/promises";
1504
+ import { copy as copy2, ensureDir as ensureDir2 } from "fs-extra";
1505
+ import { join as join6, dirname as dirname4 } from "path";
1506
+ import { constants as constants2 } from "fs";
1489
1507
  var getFileChecksum, getFileInfo, getDirectoryFiles, getDirectoryFileCount, copyFileOrDir, createSymlink, deleteFileOrDir;
1490
1508
  var init_files = __esm({
1491
1509
  "src/lib/files.ts"() {
@@ -1498,12 +1516,12 @@ var init_files = __esm({
1498
1516
  const files = await getDirectoryFiles(expandedPath);
1499
1517
  const hashes = [];
1500
1518
  for (const file of files) {
1501
- const content2 = await readFile5(file);
1519
+ const content2 = await readFile4(file);
1502
1520
  hashes.push(createHash("sha256").update(content2).digest("hex"));
1503
1521
  }
1504
1522
  return createHash("sha256").update(hashes.join("")).digest("hex");
1505
1523
  }
1506
- const content = await readFile5(expandedPath);
1524
+ const content = await readFile4(expandedPath);
1507
1525
  return createHash("sha256").update(content).digest("hex");
1508
1526
  };
1509
1527
  getFileInfo = async (filepath) => {
@@ -1529,14 +1547,27 @@ var init_files = __esm({
1529
1547
  getDirectoryFiles = async (dirpath) => {
1530
1548
  const expandedPath = expandPath(dirpath);
1531
1549
  const files = [];
1532
- const entries = await readdir3(expandedPath, { withFileTypes: true });
1550
+ let entries;
1551
+ try {
1552
+ entries = await readdir3(expandedPath, { withFileTypes: true });
1553
+ } catch (error) {
1554
+ return files;
1555
+ }
1533
1556
  for (const entry of entries) {
1534
- const entryPath = join7(expandedPath, entry.name);
1535
- if (entry.isDirectory()) {
1536
- const subFiles = await getDirectoryFiles(entryPath);
1537
- files.push(...subFiles);
1538
- } else if (entry.isFile()) {
1539
- files.push(entryPath);
1557
+ const entryPath = join6(expandedPath, entry.name);
1558
+ try {
1559
+ const lstats = await lstat(entryPath);
1560
+ if (lstats.isSymbolicLink()) {
1561
+ continue;
1562
+ }
1563
+ if (entry.isDirectory()) {
1564
+ const subFiles = await getDirectoryFiles(entryPath);
1565
+ files.push(...subFiles);
1566
+ } else if (entry.isFile()) {
1567
+ files.push(entryPath);
1568
+ }
1569
+ } catch {
1570
+ continue;
1540
1571
  }
1541
1572
  }
1542
1573
  return files.sort();
@@ -1551,11 +1582,12 @@ var init_files = __esm({
1551
1582
  if (!await pathExists(expandedSource)) {
1552
1583
  throw new FileNotFoundError(source);
1553
1584
  }
1554
- await ensureDir3(dirname4(expandedDest));
1585
+ await ensureDir2(dirname4(expandedDest));
1555
1586
  const sourceIsDir = await isDirectory(expandedSource);
1556
1587
  try {
1588
+ const shouldOverwrite = options?.overwrite ?? true;
1557
1589
  if (sourceIsDir) {
1558
- await copy3(expandedSource, expandedDest, { overwrite: options?.overwrite ?? true });
1590
+ await copy2(expandedSource, expandedDest, { overwrite: shouldOverwrite });
1559
1591
  const fileCount = await getDirectoryFileCount(expandedDest);
1560
1592
  const files = await getDirectoryFiles(expandedDest);
1561
1593
  let totalSize = 0;
@@ -1565,7 +1597,8 @@ var init_files = __esm({
1565
1597
  }
1566
1598
  return { source: expandedSource, destination: expandedDest, fileCount, totalSize };
1567
1599
  } else {
1568
- await copyFile(expandedSource, expandedDest);
1600
+ const copyFlags = shouldOverwrite ? 0 : constants2.COPYFILE_EXCL;
1601
+ await copyFile(expandedSource, expandedDest, copyFlags);
1569
1602
  const stats = await stat4(expandedDest);
1570
1603
  return { source: expandedSource, destination: expandedDest, fileCount: 1, totalSize: stats.size };
1571
1604
  }
@@ -1579,7 +1612,7 @@ var init_files = __esm({
1579
1612
  if (!await pathExists(expandedTarget)) {
1580
1613
  throw new FileNotFoundError(target);
1581
1614
  }
1582
- await ensureDir3(dirname4(expandedLink));
1615
+ await ensureDir2(dirname4(expandedLink));
1583
1616
  if (options?.overwrite && await pathExists(expandedLink)) {
1584
1617
  await unlink(expandedLink);
1585
1618
  }
@@ -1596,7 +1629,7 @@ var init_files = __esm({
1596
1629
  }
1597
1630
  try {
1598
1631
  if (await isDirectory(expandedPath)) {
1599
- await rm3(expandedPath, { recursive: true });
1632
+ await rm2(expandedPath, { recursive: true });
1600
1633
  } else {
1601
1634
  await unlink(expandedPath);
1602
1635
  }
@@ -1607,235 +1640,620 @@ var init_files = __esm({
1607
1640
  }
1608
1641
  });
1609
1642
 
1610
- // src/commands/add.ts
1611
- var add_exports = {};
1612
- __export(add_exports, {
1613
- addCommand: () => addCommand,
1614
- addFilesFromPaths: () => addFilesFromPaths
1643
+ // src/lib/backup.ts
1644
+ import { join as join7 } from "path";
1645
+ import { copy as copy3, ensureDir as ensureDir4, pathExists as pathExists3 } from "fs-extra";
1646
+ var getBackupDir, formatDateForBackup, getTimestampedBackupDir, createBackup;
1647
+ var init_backup = __esm({
1648
+ "src/lib/backup.ts"() {
1649
+ "use strict";
1650
+ init_constants();
1651
+ init_paths();
1652
+ getBackupDir = () => {
1653
+ return expandPath(BACKUP_DIR);
1654
+ };
1655
+ formatDateForBackup = (date) => {
1656
+ return date.toISOString().slice(0, 10);
1657
+ };
1658
+ getTimestampedBackupDir = (date) => {
1659
+ const backupRoot = getBackupDir();
1660
+ const timestamp = formatDateForBackup(date);
1661
+ return join7(backupRoot, timestamp);
1662
+ };
1663
+ createBackup = async (sourcePath, customBackupDir) => {
1664
+ const expandedSource = expandPath(sourcePath);
1665
+ const date = /* @__PURE__ */ new Date();
1666
+ if (!await pathExists(expandedSource)) {
1667
+ throw new Error(`Source path does not exist: ${sourcePath}`);
1668
+ }
1669
+ const backupRoot = customBackupDir ? expandPath(customBackupDir) : getTimestampedBackupDir(date);
1670
+ await ensureDir4(backupRoot);
1671
+ const collapsed = collapsePath(expandedSource);
1672
+ const backupName = collapsed.replace(/^~\//, "").replace(/\//g, "_").replace(/^\./, "dot-");
1673
+ const timestamp = date.toISOString().replace(/[:.]/g, "-").slice(11, 19);
1674
+ const backupPath = join7(backupRoot, `${backupName}_${timestamp}`);
1675
+ await copy3(expandedSource, backupPath, { overwrite: true });
1676
+ return {
1677
+ originalPath: expandedSource,
1678
+ backupPath,
1679
+ date
1680
+ };
1681
+ };
1682
+ }
1615
1683
  });
1616
- import { Command as Command2 } from "commander";
1617
- import { basename as basename3 } from "path";
1618
- import chalk7 from "chalk";
1619
- var PRIVATE_KEY_PATTERNS, SENSITIVE_FILE_PATTERNS, isPrivateKey, isSensitiveFile, validateAndPrepareFiles, addFiles, runInteractiveAdd, addFilesFromPaths, runAdd, addCommand;
1620
- var init_add = __esm({
1621
- "src/commands/add.ts"() {
1684
+
1685
+ // src/lib/hooks.ts
1686
+ import { exec } from "child_process";
1687
+ import { promisify as promisify2 } from "util";
1688
+ import chalk9 from "chalk";
1689
+ var execAsync, runHook, runPreSyncHook, runPostSyncHook, runPreRestoreHook, runPostRestoreHook;
1690
+ var init_hooks = __esm({
1691
+ "src/lib/hooks.ts"() {
1692
+ "use strict";
1693
+ init_config();
1694
+ init_logger();
1695
+ init_prompts();
1696
+ execAsync = promisify2(exec);
1697
+ runHook = async (hookType, tuckDir, options) => {
1698
+ if (options?.skipHooks) {
1699
+ return { success: true, skipped: true };
1700
+ }
1701
+ const config = await loadConfig(tuckDir);
1702
+ const command = config.hooks[hookType];
1703
+ if (!command) {
1704
+ return { success: true };
1705
+ }
1706
+ if (!options?.trustHooks) {
1707
+ console.log();
1708
+ console.log(chalk9.yellow.bold("WARNING: Hook Execution"));
1709
+ console.log(chalk9.dim("\u2500".repeat(50)));
1710
+ console.log(chalk9.white(`Hook type: ${chalk9.cyan(hookType)}`));
1711
+ console.log(chalk9.white("Command:"));
1712
+ console.log(chalk9.red(` ${command}`));
1713
+ console.log(chalk9.dim("\u2500".repeat(50)));
1714
+ console.log(
1715
+ chalk9.yellow(
1716
+ "SECURITY: Hooks can execute arbitrary commands on your system."
1717
+ )
1718
+ );
1719
+ console.log(
1720
+ chalk9.yellow(
1721
+ "Only proceed if you trust the source of this configuration."
1722
+ )
1723
+ );
1724
+ console.log();
1725
+ const confirmed = await prompts.confirm(
1726
+ "Execute this hook?",
1727
+ false
1728
+ // Default to NO for safety
1729
+ );
1730
+ if (!confirmed) {
1731
+ logger.warning(`Hook ${hookType} skipped by user`);
1732
+ return { success: true, skipped: true };
1733
+ }
1734
+ }
1735
+ if (!options?.silent) {
1736
+ logger.dim(`Running ${hookType} hook...`);
1737
+ }
1738
+ try {
1739
+ const { stdout, stderr } = await execAsync(command, {
1740
+ cwd: tuckDir,
1741
+ timeout: 3e4,
1742
+ // 30 second timeout
1743
+ env: {
1744
+ ...process.env,
1745
+ TUCK_DIR: tuckDir,
1746
+ TUCK_HOOK: hookType
1747
+ }
1748
+ });
1749
+ if (stdout && !options?.silent) {
1750
+ logger.dim(stdout.trim());
1751
+ }
1752
+ if (stderr && !options?.silent) {
1753
+ logger.warning(stderr.trim());
1754
+ }
1755
+ return { success: true, output: stdout };
1756
+ } catch (error) {
1757
+ const errorMessage = error instanceof Error ? error.message : String(error);
1758
+ if (!options?.silent) {
1759
+ logger.error(`Hook ${hookType} failed: ${errorMessage}`);
1760
+ }
1761
+ return { success: false, error: errorMessage };
1762
+ }
1763
+ };
1764
+ runPreSyncHook = async (tuckDir, options) => {
1765
+ return runHook("preSync", tuckDir, options);
1766
+ };
1767
+ runPostSyncHook = async (tuckDir, options) => {
1768
+ return runHook("postSync", tuckDir, options);
1769
+ };
1770
+ runPreRestoreHook = async (tuckDir, options) => {
1771
+ return runHook("preRestore", tuckDir, options);
1772
+ };
1773
+ runPostRestoreHook = async (tuckDir, options) => {
1774
+ return runHook("postRestore", tuckDir, options);
1775
+ };
1776
+ }
1777
+ });
1778
+
1779
+ // src/commands/restore.ts
1780
+ var restore_exports = {};
1781
+ __export(restore_exports, {
1782
+ restoreCommand: () => restoreCommand,
1783
+ runRestore: () => runRestore
1784
+ });
1785
+ import { Command } from "commander";
1786
+ import chalk10 from "chalk";
1787
+ import { join as join8 } from "path";
1788
+ import { chmod, stat as stat5 } from "fs/promises";
1789
+ var fixSSHPermissions, fixGPGPermissions, prepareFilesToRestore, restoreFiles, runInteractiveRestore, runRestore, runRestoreCommand, restoreCommand;
1790
+ var init_restore = __esm({
1791
+ "src/commands/restore.ts"() {
1622
1792
  "use strict";
1623
1793
  init_ui();
1624
1794
  init_paths();
1625
- init_config();
1626
1795
  init_manifest();
1796
+ init_config();
1627
1797
  init_files();
1798
+ init_backup();
1799
+ init_hooks();
1628
1800
  init_errors();
1629
1801
  init_constants();
1630
- PRIVATE_KEY_PATTERNS = [
1631
- /^id_rsa$/,
1632
- /^id_dsa$/,
1633
- /^id_ecdsa$/,
1634
- /^id_ed25519$/,
1635
- /^id_.*$/,
1636
- // Any id_ file without .pub
1637
- /\.pem$/,
1638
- /\.key$/,
1639
- /^.*_key$/
1640
- // aws_key, github_key, etc.
1641
- ];
1642
- SENSITIVE_FILE_PATTERNS = [
1643
- /^\.netrc$/,
1644
- /^\.aws\/credentials$/,
1645
- /^\.docker\/config\.json$/,
1646
- /^\.npmrc$/,
1647
- // May contain tokens
1648
- /^\.pypirc$/,
1649
- /^\.kube\/config$/,
1650
- /^\.ssh\/config$/,
1651
- /^\.gnupg\//,
1652
- /credentials/i,
1653
- /secrets?/i,
1654
- /tokens?\.json$/i,
1655
- /\.env$/,
1656
- /\.env\./
1657
- ];
1658
- isPrivateKey = (path) => {
1659
- const name = basename3(path);
1660
- if (path.includes(".ssh/") && !name.endsWith(".pub")) {
1661
- for (const pattern of PRIVATE_KEY_PATTERNS) {
1662
- if (pattern.test(name)) {
1663
- return true;
1664
- }
1665
- }
1802
+ fixSSHPermissions = async (path) => {
1803
+ const expandedPath = expandPath(path);
1804
+ if (!path.includes(".ssh/") && !path.endsWith(".ssh")) {
1805
+ return;
1666
1806
  }
1667
- if (name.endsWith(".pem") || name.endsWith(".key")) {
1668
- return true;
1807
+ try {
1808
+ const stats = await stat5(expandedPath);
1809
+ if (stats.isDirectory()) {
1810
+ await chmod(expandedPath, 448);
1811
+ } else {
1812
+ await chmod(expandedPath, 384);
1813
+ }
1814
+ } catch {
1669
1815
  }
1670
- return false;
1671
1816
  };
1672
- isSensitiveFile = (path) => {
1673
- const pathToTest = path.startsWith("~/") ? path.slice(2) : path;
1674
- for (const pattern of SENSITIVE_FILE_PATTERNS) {
1675
- if (pattern.test(pathToTest)) {
1676
- return true;
1817
+ fixGPGPermissions = async (path) => {
1818
+ const expandedPath = expandPath(path);
1819
+ if (!path.includes(".gnupg/") && !path.endsWith(".gnupg")) {
1820
+ return;
1821
+ }
1822
+ try {
1823
+ const stats = await stat5(expandedPath);
1824
+ if (stats.isDirectory()) {
1825
+ await chmod(expandedPath, 448);
1826
+ } else {
1827
+ await chmod(expandedPath, 384);
1677
1828
  }
1829
+ } catch {
1678
1830
  }
1679
- return false;
1680
1831
  };
1681
- validateAndPrepareFiles = async (paths, tuckDir, options) => {
1682
- const filesToAdd = [];
1683
- for (const path of paths) {
1684
- const expandedPath = expandPath(path);
1685
- const collapsedPath = collapsePath(expandedPath);
1686
- if (isPrivateKey(collapsedPath)) {
1687
- throw new Error(
1688
- `Cannot track private key: ${path}
1689
- Private keys should NEVER be committed to a repository.
1690
- If you need to backup SSH keys, use a secure password manager.`
1691
- );
1692
- }
1693
- if (!await pathExists(expandedPath)) {
1694
- throw new FileNotFoundError(path);
1832
+ prepareFilesToRestore = async (tuckDir, paths) => {
1833
+ const allFiles = await getAllTrackedFiles(tuckDir);
1834
+ const filesToRestore = [];
1835
+ if (paths && paths.length > 0) {
1836
+ for (const path of paths) {
1837
+ const expandedPath = expandPath(path);
1838
+ const collapsedPath = collapsePath(expandedPath);
1839
+ const tracked = await getTrackedFileBySource(tuckDir, collapsedPath);
1840
+ if (!tracked) {
1841
+ throw new FileNotFoundError(`Not tracked: ${path}`);
1842
+ }
1843
+ filesToRestore.push({
1844
+ id: tracked.id,
1845
+ source: tracked.file.source,
1846
+ destination: join8(tuckDir, tracked.file.destination),
1847
+ category: tracked.file.category,
1848
+ existsAtTarget: await pathExists(expandedPath)
1849
+ });
1695
1850
  }
1696
- if (await isFileTracked(tuckDir, collapsedPath)) {
1697
- throw new FileAlreadyTrackedError(path);
1851
+ } else {
1852
+ for (const [id, file] of Object.entries(allFiles)) {
1853
+ const targetPath = expandPath(file.source);
1854
+ filesToRestore.push({
1855
+ id,
1856
+ source: file.source,
1857
+ destination: join8(tuckDir, file.destination),
1858
+ category: file.category,
1859
+ existsAtTarget: await pathExists(targetPath)
1860
+ });
1698
1861
  }
1699
- const isDir = await isDirectory(expandedPath);
1700
- const fileCount = isDir ? await getDirectoryFileCount(expandedPath) : 1;
1701
- const category = options.category || detectCategory(expandedPath);
1702
- const filename = options.name || sanitizeFilename(expandedPath);
1703
- const destination = getDestinationPath(tuckDir, category, filename);
1704
- const sensitive = isSensitiveFile(collapsedPath);
1705
- filesToAdd.push({
1706
- source: collapsedPath,
1707
- destination,
1708
- category,
1709
- filename,
1710
- isDir,
1711
- fileCount,
1712
- sensitive
1713
- });
1714
1862
  }
1715
- return filesToAdd;
1863
+ return filesToRestore;
1716
1864
  };
1717
- addFiles = async (filesToAdd, tuckDir, options) => {
1865
+ restoreFiles = async (tuckDir, files, options) => {
1718
1866
  const config = await loadConfig(tuckDir);
1719
- const strategy = options.symlink ? "symlink" : config.files.strategy || "copy";
1720
- for (const file of filesToAdd) {
1721
- const expandedSource = expandPath(file.source);
1722
- await withSpinner(`Copying ${file.source}...`, async () => {
1723
- await copyFileOrDir(expandedSource, file.destination, { overwrite: true });
1724
- });
1725
- const checksum = await getFileChecksum(file.destination);
1726
- const info = await getFileInfo(expandedSource);
1727
- const now = (/* @__PURE__ */ new Date()).toISOString();
1728
- const id = generateFileId(file.source);
1729
- await addFileToManifest(tuckDir, id, {
1730
- source: file.source,
1731
- destination: getRelativeDestination(file.category, file.filename),
1732
- category: file.category,
1733
- strategy,
1734
- encrypted: options.encrypt || false,
1735
- template: options.template || false,
1736
- permissions: info.permissions,
1737
- added: now,
1738
- modified: now,
1739
- checksum
1740
- });
1741
- const categoryInfo = CATEGORIES[file.category];
1742
- const icon = categoryInfo?.icon || "-";
1743
- logger.success(`Added ${file.source}`);
1744
- logger.dim(` ${icon} Category: ${file.category}`);
1745
- if (file.isDir) {
1746
- logger.dim(` [dir] Directory with ${file.fileCount} files`);
1867
+ const useSymlink = options.symlink || config.files.strategy === "symlink";
1868
+ const shouldBackup = options.backup ?? config.files.backupOnRestore;
1869
+ const hookOptions = {
1870
+ skipHooks: options.noHooks,
1871
+ trustHooks: options.trustHooks
1872
+ };
1873
+ await runPreRestoreHook(tuckDir, hookOptions);
1874
+ let restoredCount = 0;
1875
+ for (const file of files) {
1876
+ const targetPath = expandPath(file.source);
1877
+ if (!await pathExists(file.destination)) {
1878
+ logger.warning(`Source not found in repository: ${file.source}`);
1879
+ continue;
1880
+ }
1881
+ if (options.dryRun) {
1882
+ if (file.existsAtTarget) {
1883
+ logger.file("modify", `${file.source} (would overwrite)`);
1884
+ } else {
1885
+ logger.file("add", `${file.source} (would create)`);
1886
+ }
1887
+ continue;
1747
1888
  }
1748
- if (file.sensitive) {
1749
- console.log(chalk7.yellow(` [!] Warning: This file may contain sensitive data`));
1750
- console.log(chalk7.dim(` Make sure your repository is private!`));
1889
+ if (shouldBackup && file.existsAtTarget) {
1890
+ await withSpinner(`Backing up ${file.source}...`, async () => {
1891
+ await createBackup(targetPath);
1892
+ });
1751
1893
  }
1894
+ await withSpinner(`Restoring ${file.source}...`, async () => {
1895
+ if (useSymlink) {
1896
+ await createSymlink(file.destination, targetPath, { overwrite: true });
1897
+ } else {
1898
+ await copyFileOrDir(file.destination, targetPath, { overwrite: true });
1899
+ }
1900
+ await fixSSHPermissions(file.source);
1901
+ await fixGPGPermissions(file.source);
1902
+ });
1903
+ restoredCount++;
1752
1904
  }
1905
+ await runPostRestoreHook(tuckDir, hookOptions);
1906
+ return restoredCount;
1753
1907
  };
1754
- runInteractiveAdd = async (tuckDir) => {
1755
- prompts.intro("tuck add");
1756
- const pathsInput = await prompts.text("Enter file paths to track (space-separated):", {
1757
- placeholder: "~/.zshrc ~/.gitconfig",
1758
- validate: (value) => {
1759
- if (!value.trim()) return "At least one path is required";
1760
- return void 0;
1761
- }
1908
+ runInteractiveRestore = async (tuckDir) => {
1909
+ prompts.intro("tuck restore");
1910
+ const files = await prepareFilesToRestore(tuckDir);
1911
+ if (files.length === 0) {
1912
+ prompts.log.warning("No files to restore");
1913
+ prompts.note("Run 'tuck add <path>' to track files first", "Tip");
1914
+ return;
1915
+ }
1916
+ const fileOptions = files.map((file) => {
1917
+ const categoryConfig = CATEGORIES[file.category] || { icon: "\u{1F4C4}" };
1918
+ const status = file.existsAtTarget ? chalk10.yellow("(exists, will backup)") : "";
1919
+ return {
1920
+ value: file.id,
1921
+ label: `${categoryConfig.icon} ${file.source} ${status}`,
1922
+ hint: file.category
1923
+ };
1762
1924
  });
1763
- const paths = pathsInput.split(/\s+/).filter(Boolean);
1764
- let filesToAdd;
1765
- try {
1766
- filesToAdd = await validateAndPrepareFiles(paths, tuckDir, {});
1767
- } catch (error) {
1768
- if (error instanceof Error) {
1769
- prompts.log.error(error.message);
1770
- }
1771
- prompts.cancel();
1925
+ const selectedIds = await prompts.multiselect("Select files to restore:", fileOptions, true);
1926
+ if (selectedIds.length === 0) {
1927
+ prompts.cancel("No files selected");
1772
1928
  return;
1773
1929
  }
1774
- for (const file of filesToAdd) {
1775
- prompts.log.step(`${file.source}`);
1776
- const categoryOptions = Object.entries(CATEGORIES).map(([name, config]) => ({
1777
- value: name,
1778
- label: `${config.icon} ${name}`,
1779
- hint: file.category === name ? "(auto-detected)" : void 0
1780
- }));
1781
- categoryOptions.sort((a, b) => {
1782
- if (a.value === file.category) return -1;
1783
- if (b.value === file.category) return 1;
1784
- return 0;
1785
- });
1786
- const selectedCategory = await prompts.select("Category:", categoryOptions);
1787
- file.category = selectedCategory;
1788
- file.destination = getDestinationPath(tuckDir, file.category, file.filename);
1930
+ const selectedFiles = files.filter((f) => selectedIds.includes(f.id));
1931
+ const existingFiles = selectedFiles.filter((f) => f.existsAtTarget);
1932
+ if (existingFiles.length > 0) {
1933
+ console.log();
1934
+ prompts.log.warning(
1935
+ `${existingFiles.length} file${existingFiles.length > 1 ? "s" : ""} will be backed up:`
1936
+ );
1937
+ existingFiles.forEach((f) => console.log(chalk10.dim(` ${f.source}`)));
1938
+ console.log();
1789
1939
  }
1940
+ const useSymlink = await prompts.select("Restore method:", [
1941
+ { value: false, label: "Copy files", hint: "Recommended" },
1942
+ { value: true, label: "Create symlinks", hint: "Files stay in tuck repo" }
1943
+ ]);
1790
1944
  const confirm2 = await prompts.confirm(
1791
- `Add ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}?`,
1945
+ `Restore ${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}?`,
1792
1946
  true
1793
1947
  );
1794
1948
  if (!confirm2) {
1795
1949
  prompts.cancel("Operation cancelled");
1796
1950
  return;
1797
1951
  }
1798
- await addFiles(filesToAdd, tuckDir, {});
1799
- prompts.outro(`Added ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}`);
1800
- logger.info("Run 'tuck sync' to commit changes");
1952
+ const restoredCount = await restoreFiles(tuckDir, selectedFiles, {
1953
+ symlink: useSymlink,
1954
+ backup: true
1955
+ });
1956
+ console.log();
1957
+ prompts.outro(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
1958
+ };
1959
+ runRestore = async (options) => {
1960
+ const tuckDir = getTuckDir();
1961
+ try {
1962
+ await loadManifest(tuckDir);
1963
+ } catch {
1964
+ throw new NotInitializedError();
1965
+ }
1966
+ if (options.all) {
1967
+ const files = await prepareFilesToRestore(tuckDir, void 0);
1968
+ if (files.length === 0) {
1969
+ logger.warning("No files to restore");
1970
+ return;
1971
+ }
1972
+ const restoredCount = await restoreFiles(tuckDir, files, options);
1973
+ logger.blank();
1974
+ logger.success(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
1975
+ } else {
1976
+ await runInteractiveRestore(tuckDir);
1977
+ }
1978
+ };
1979
+ runRestoreCommand = async (paths, options) => {
1980
+ const tuckDir = getTuckDir();
1981
+ try {
1982
+ await loadManifest(tuckDir);
1983
+ } catch {
1984
+ throw new NotInitializedError();
1985
+ }
1986
+ if (paths.length === 0 && !options.all) {
1987
+ await runInteractiveRestore(tuckDir);
1988
+ return;
1989
+ }
1990
+ const files = await prepareFilesToRestore(tuckDir, options.all ? void 0 : paths);
1991
+ if (files.length === 0) {
1992
+ logger.warning("No files to restore");
1993
+ return;
1994
+ }
1995
+ if (options.dryRun) {
1996
+ logger.heading("Dry run - would restore:");
1997
+ } else {
1998
+ logger.heading("Restoring:");
1999
+ }
2000
+ const restoredCount = await restoreFiles(tuckDir, files, options);
2001
+ logger.blank();
2002
+ if (options.dryRun) {
2003
+ logger.info(`Would restore ${files.length} file${files.length > 1 ? "s" : ""}`);
2004
+ } else {
2005
+ logger.success(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
2006
+ }
2007
+ };
2008
+ restoreCommand = new Command("restore").description("Restore dotfiles to the system").argument("[paths...]", "Paths to restore (or use --all)").option("-a, --all", "Restore all tracked files").option("--symlink", "Create symlinks instead of copies").option("--backup", "Backup existing files before restore").option("--no-backup", "Skip backup of existing files").option("--dry-run", "Show what would be done").option("--no-hooks", "Skip execution of pre/post restore hooks").option("--trust-hooks", "Trust and run hooks without confirmation (use with caution)").action(async (paths, options) => {
2009
+ await runRestoreCommand(paths, options);
2010
+ });
2011
+ }
2012
+ });
2013
+
2014
+ // src/commands/sync.ts
2015
+ var sync_exports = {};
2016
+ __export(sync_exports, {
2017
+ runSync: () => runSync,
2018
+ syncCommand: () => syncCommand
2019
+ });
2020
+ import { Command as Command2 } from "commander";
2021
+ import chalk11 from "chalk";
2022
+ import { join as join9 } from "path";
2023
+ var detectChanges, generateCommitMessage, syncFiles, runInteractiveSync, runSync, runSyncCommand, syncCommand;
2024
+ var init_sync = __esm({
2025
+ "src/commands/sync.ts"() {
2026
+ "use strict";
2027
+ init_ui();
2028
+ init_paths();
2029
+ init_manifest();
2030
+ init_git();
2031
+ init_files();
2032
+ init_hooks();
2033
+ init_errors();
2034
+ detectChanges = async (tuckDir) => {
2035
+ const files = await getAllTrackedFiles(tuckDir);
2036
+ const changes = [];
2037
+ for (const [, file] of Object.entries(files)) {
2038
+ const sourcePath = expandPath(file.source);
2039
+ if (!await pathExists(sourcePath)) {
2040
+ changes.push({
2041
+ path: file.source,
2042
+ status: "deleted",
2043
+ source: file.source,
2044
+ destination: file.destination
2045
+ });
2046
+ continue;
2047
+ }
2048
+ try {
2049
+ const sourceChecksum = await getFileChecksum(sourcePath);
2050
+ if (sourceChecksum !== file.checksum) {
2051
+ changes.push({
2052
+ path: file.source,
2053
+ status: "modified",
2054
+ source: file.source,
2055
+ destination: file.destination
2056
+ });
2057
+ }
2058
+ } catch {
2059
+ changes.push({
2060
+ path: file.source,
2061
+ status: "modified",
2062
+ source: file.source,
2063
+ destination: file.destination
2064
+ });
2065
+ }
2066
+ }
2067
+ return changes;
2068
+ };
2069
+ generateCommitMessage = (result) => {
2070
+ const parts = [];
2071
+ if (result.modified.length > 0) {
2072
+ parts.push(`Update: ${result.modified.join(", ")}`);
2073
+ }
2074
+ if (result.deleted.length > 0) {
2075
+ parts.push(`Remove: ${result.deleted.join(", ")}`);
2076
+ }
2077
+ if (parts.length === 0) {
2078
+ return "Sync dotfiles";
2079
+ }
2080
+ const totalCount = result.modified.length + result.deleted.length;
2081
+ if (parts.length === 1 && totalCount <= 3) {
2082
+ return parts[0];
2083
+ }
2084
+ return `Sync: ${totalCount} file${totalCount > 1 ? "s" : ""} changed`;
2085
+ };
2086
+ syncFiles = async (tuckDir, changes, options) => {
2087
+ const result = {
2088
+ modified: [],
2089
+ deleted: []
2090
+ };
2091
+ const hookOptions = {
2092
+ skipHooks: options.noHooks,
2093
+ trustHooks: options.trustHooks
2094
+ };
2095
+ await runPreSyncHook(tuckDir, hookOptions);
2096
+ for (const change of changes) {
2097
+ const sourcePath = expandPath(change.source);
2098
+ const destPath = join9(tuckDir, change.destination);
2099
+ if (change.status === "modified") {
2100
+ await withSpinner(`Syncing ${change.path}...`, async () => {
2101
+ await copyFileOrDir(sourcePath, destPath, { overwrite: true });
2102
+ const newChecksum = await getFileChecksum(destPath);
2103
+ const files = await getAllTrackedFiles(tuckDir);
2104
+ const fileId = Object.entries(files).find(([, f]) => f.source === change.source)?.[0];
2105
+ if (fileId) {
2106
+ await updateFileInManifest(tuckDir, fileId, {
2107
+ checksum: newChecksum,
2108
+ modified: (/* @__PURE__ */ new Date()).toISOString()
2109
+ });
2110
+ }
2111
+ });
2112
+ result.modified.push(change.path.split("/").pop() || change.path);
2113
+ } else if (change.status === "deleted") {
2114
+ await withSpinner(`Removing ${change.path}...`, async () => {
2115
+ await deleteFileOrDir(destPath);
2116
+ const files = await getAllTrackedFiles(tuckDir);
2117
+ const fileId = Object.entries(files).find(([, f]) => f.source === change.source)?.[0];
2118
+ if (fileId) {
2119
+ await removeFileFromManifest(tuckDir, fileId);
2120
+ }
2121
+ });
2122
+ result.deleted.push(change.path.split("/").pop() || change.path);
2123
+ }
2124
+ }
2125
+ if (!options.noCommit && (result.modified.length > 0 || result.deleted.length > 0)) {
2126
+ await withSpinner("Staging changes...", async () => {
2127
+ await stageAll(tuckDir);
2128
+ });
2129
+ const message = options.message || generateCommitMessage(result);
2130
+ await withSpinner("Committing...", async () => {
2131
+ result.commitHash = await commit(tuckDir, message);
2132
+ });
2133
+ }
2134
+ await runPostSyncHook(tuckDir, hookOptions);
2135
+ return result;
2136
+ };
2137
+ runInteractiveSync = async (tuckDir, options = {}) => {
2138
+ prompts.intro("tuck sync");
2139
+ const spinner2 = prompts.spinner();
2140
+ spinner2.start("Detecting changes...");
2141
+ const changes = await detectChanges(tuckDir);
2142
+ spinner2.stop("Changes detected");
2143
+ if (changes.length === 0) {
2144
+ const gitStatus = await getStatus(tuckDir);
2145
+ if (gitStatus.hasChanges) {
2146
+ prompts.log.info("No dotfile changes, but repository has uncommitted changes");
2147
+ const commitAnyway = await prompts.confirm("Commit repository changes?");
2148
+ if (commitAnyway) {
2149
+ const message2 = await prompts.text("Commit message:", {
2150
+ defaultValue: "Update dotfiles"
2151
+ });
2152
+ await stageAll(tuckDir);
2153
+ const hash = await commit(tuckDir, message2);
2154
+ prompts.log.success(`Committed: ${hash.slice(0, 7)}`);
2155
+ }
2156
+ } else {
2157
+ prompts.log.success("Everything is up to date");
2158
+ }
2159
+ return;
2160
+ }
2161
+ console.log();
2162
+ console.log(chalk11.bold("Changes detected:"));
2163
+ for (const change of changes) {
2164
+ if (change.status === "modified") {
2165
+ console.log(chalk11.yellow(` ~ ${change.path}`));
2166
+ } else if (change.status === "deleted") {
2167
+ console.log(chalk11.red(` - ${change.path}`));
2168
+ }
2169
+ }
2170
+ console.log();
2171
+ const confirm2 = await prompts.confirm("Sync these changes?", true);
2172
+ if (!confirm2) {
2173
+ prompts.cancel("Operation cancelled");
2174
+ return;
2175
+ }
2176
+ const autoMessage = generateCommitMessage({
2177
+ modified: changes.filter((c) => c.status === "modified").map((c) => c.path),
2178
+ deleted: changes.filter((c) => c.status === "deleted").map((c) => c.path)
2179
+ });
2180
+ const message = await prompts.text("Commit message:", {
2181
+ defaultValue: autoMessage
2182
+ });
2183
+ const result = await syncFiles(tuckDir, changes, { message });
2184
+ console.log();
2185
+ if (result.commitHash) {
2186
+ prompts.log.success(`Committed: ${result.commitHash.slice(0, 7)}`);
2187
+ if (options.push !== false && await hasRemote(tuckDir)) {
2188
+ const spinner22 = prompts.spinner();
2189
+ spinner22.start("Pushing to remote...");
2190
+ try {
2191
+ await push(tuckDir);
2192
+ spinner22.stop("Pushed to remote");
2193
+ } catch {
2194
+ spinner22.stop("Push failed (will retry on next sync)");
2195
+ }
2196
+ } else if (options.push === false) {
2197
+ prompts.log.info("Run 'tuck push' when ready to upload");
2198
+ }
2199
+ }
2200
+ prompts.outro("Synced successfully!");
1801
2201
  };
1802
- addFilesFromPaths = async (paths, options = {}) => {
2202
+ runSync = async (options = {}) => {
1803
2203
  const tuckDir = getTuckDir();
1804
2204
  try {
1805
2205
  await loadManifest(tuckDir);
1806
2206
  } catch {
1807
2207
  throw new NotInitializedError();
1808
2208
  }
1809
- const filesToAdd = await validateAndPrepareFiles(paths, tuckDir, options);
1810
- await addFiles(filesToAdd, tuckDir, options);
1811
- return filesToAdd.length;
2209
+ await runInteractiveSync(tuckDir, options);
1812
2210
  };
1813
- runAdd = async (paths, options) => {
2211
+ runSyncCommand = async (messageArg, options) => {
1814
2212
  const tuckDir = getTuckDir();
1815
2213
  try {
1816
2214
  await loadManifest(tuckDir);
1817
2215
  } catch {
1818
2216
  throw new NotInitializedError();
1819
2217
  }
1820
- if (paths.length === 0) {
1821
- await runInteractiveAdd(tuckDir);
2218
+ if (!messageArg && !options.message && !options.noCommit) {
2219
+ await runInteractiveSync(tuckDir, options);
2220
+ return;
2221
+ }
2222
+ const changes = await detectChanges(tuckDir);
2223
+ if (changes.length === 0) {
2224
+ logger.info("No changes detected");
1822
2225
  return;
1823
2226
  }
1824
- const filesToAdd = await validateAndPrepareFiles(paths, tuckDir, options);
1825
- await addFiles(filesToAdd, tuckDir, options);
2227
+ logger.heading("Changes detected:");
2228
+ for (const change of changes) {
2229
+ logger.file(change.status === "modified" ? "modify" : "delete", change.path);
2230
+ }
1826
2231
  logger.blank();
1827
- logger.success(`Added ${filesToAdd.length} ${filesToAdd.length === 1 ? "item" : "items"}`);
1828
- logger.info("Run 'tuck sync' to commit changes");
2232
+ const message = messageArg || options.message;
2233
+ const result = await syncFiles(tuckDir, changes, { ...options, message });
2234
+ logger.blank();
2235
+ logger.success(`Synced ${changes.length} file${changes.length > 1 ? "s" : ""}`);
2236
+ if (result.commitHash) {
2237
+ logger.info(`Commit: ${result.commitHash.slice(0, 7)}`);
2238
+ if (options.push !== false && await hasRemote(tuckDir)) {
2239
+ await withSpinner("Pushing to remote...", async () => {
2240
+ await push(tuckDir);
2241
+ });
2242
+ logger.success("Pushed to remote");
2243
+ } else if (options.push === false) {
2244
+ logger.info("Run 'tuck push' when ready to upload");
2245
+ }
2246
+ }
1829
2247
  };
1830
- addCommand = new Command2("add").description("Track new dotfiles").argument("[paths...]", "Paths to dotfiles to track").option("-c, --category <name>", "Category to organize under").option("-n, --name <name>", "Custom name for the file in manifest").option("--symlink", "Create symlink instead of copy").option("--encrypt", "Encrypt this file (requires GPG setup)").option("--template", "Treat as template with variable substitution").action(async (paths, options) => {
1831
- await runAdd(paths, options);
2248
+ syncCommand = new Command2("sync").description("Sync changes to repository (commits and pushes)").argument("[message]", "Commit message").option("-m, --message <msg>", "Commit message").option("--no-commit", "Stage changes but don't commit").option("--no-push", "Commit but don't push to remote").option("--no-hooks", "Skip execution of pre/post sync hooks").option("--trust-hooks", "Trust and run hooks without confirmation (use with caution)").action(async (messageArg, options) => {
2249
+ await runSyncCommand(messageArg, options);
1832
2250
  });
1833
2251
  }
1834
2252
  });
1835
2253
 
1836
2254
  // src/index.ts
1837
2255
  import { Command as Command15 } from "commander";
1838
- import chalk20 from "chalk";
2256
+ import chalk21 from "chalk";
1839
2257
 
1840
2258
  // src/commands/init.ts
1841
2259
  init_ui();
@@ -1844,10 +2262,10 @@ init_config();
1844
2262
  init_manifest();
1845
2263
  init_git();
1846
2264
  init_github();
1847
- import { Command } from "commander";
1848
- import { join as join6, resolve as resolve2, sep } from "path";
2265
+ import { Command as Command3 } from "commander";
2266
+ import { join as join10, resolve as resolve2, sep as sep2 } from "path";
1849
2267
  import { writeFile as writeFile4 } from "fs/promises";
1850
- import { ensureDir as ensureDir2 } from "fs-extra";
2268
+ import { ensureDir as ensureDir5 } from "fs-extra";
1851
2269
 
1852
2270
  // src/lib/detect.ts
1853
2271
  init_paths();
@@ -2473,39 +2891,195 @@ var formatSnapshotDate = (snapshotId) => {
2473
2891
  init_errors();
2474
2892
  init_constants();
2475
2893
  init_config_schema();
2476
- import { copy as copy2 } from "fs-extra";
2894
+ import { copy as copy4 } from "fs-extra";
2477
2895
  import { tmpdir } from "os";
2478
- import { readFile as readFile4, rm as rm2 } from "fs/promises";
2479
- var GITIGNORE_TEMPLATE = `# OS generated files
2480
- .DS_Store
2481
- .DS_Store?
2482
- ._*
2483
- .Spotlight-V100
2484
- .Trashes
2485
- ehthumbs.db
2486
- Thumbs.db
2487
-
2488
- # Backup files
2489
- *.bak
2490
- *.backup
2491
- *~
2896
+ import { readFile as readFile5, rm as rm3 } from "fs/promises";
2492
2897
 
2493
- # Secret files (add patterns for files you want to exclude)
2494
- # *.secret
2495
- # .env.local
2496
- `;
2497
- var README_TEMPLATE = (machine) => `# Dotfiles
2498
-
2499
- Managed with [tuck](https://github.com/Pranav-Karra-3301/tuck) - Modern Dotfiles Manager
2500
-
2501
- ${machine ? `## Machine: ${machine}
2502
- ` : ""}
2503
-
2504
- ## Quick Start
2505
-
2506
- \`\`\`bash
2507
- # Restore dotfiles to a new machine
2508
- tuck init --from <this-repo-url>
2898
+ // src/lib/fileTracking.ts
2899
+ init_paths();
2900
+ init_manifest();
2901
+ init_files();
2902
+ init_config();
2903
+ init_constants();
2904
+ import chalk8 from "chalk";
2905
+ import ora3 from "ora";
2906
+ import { ensureDir as ensureDir3 } from "fs-extra";
2907
+ import { dirname as dirname5 } from "path";
2908
+ var SENSITIVE_FILE_PATTERNS = [
2909
+ /^\.netrc$/,
2910
+ /^\.aws\/credentials$/,
2911
+ /^\.docker\/config\.json$/,
2912
+ /^\.npmrc$/,
2913
+ /^\.pypirc$/,
2914
+ /^\.kube\/config$/,
2915
+ /^\.ssh\/config$/,
2916
+ /^\.gnupg\//,
2917
+ /credentials/i,
2918
+ /secrets?/i,
2919
+ /tokens?\.json$/i,
2920
+ /\.env$/,
2921
+ /\.env\./
2922
+ ];
2923
+ var isSensitiveFile = (path) => {
2924
+ const pathToTest = path.startsWith("~/") ? path.slice(2) : path;
2925
+ for (const pattern of SENSITIVE_FILE_PATTERNS) {
2926
+ if (pattern.test(pathToTest)) {
2927
+ return true;
2928
+ }
2929
+ }
2930
+ return false;
2931
+ };
2932
+ var trackFilesWithProgress = async (files, tuckDir, options = {}) => {
2933
+ const {
2934
+ showCategory = true,
2935
+ strategy: customStrategy,
2936
+ // TODO: Encryption and templating are planned for a future version
2937
+ // encrypt = false,
2938
+ // template = false,
2939
+ actionVerb = "Tracking",
2940
+ onProgress
2941
+ } = options;
2942
+ let { delayBetween } = options;
2943
+ if (delayBetween === void 0) {
2944
+ delayBetween = files.length >= 50 ? 10 : 30;
2945
+ }
2946
+ const config = await loadConfig(tuckDir);
2947
+ const strategy = customStrategy || config.files.strategy || "copy";
2948
+ const total = files.length;
2949
+ const errors = [];
2950
+ const sensitiveFiles = [];
2951
+ let succeeded = 0;
2952
+ console.log();
2953
+ console.log(chalk8.bold.cyan(`${actionVerb} ${total} ${total === 1 ? "file" : "files"}...`));
2954
+ console.log(chalk8.dim("\u2500".repeat(50)));
2955
+ console.log();
2956
+ for (let i = 0; i < files.length; i++) {
2957
+ const file = files[i];
2958
+ const expandedPath = expandPath(file.path);
2959
+ const indexStr = chalk8.dim(`[${i + 1}/${total}]`);
2960
+ const category = file.category || detectCategory(expandedPath);
2961
+ const filename = sanitizeFilename(expandedPath);
2962
+ const categoryInfo = CATEGORIES[category];
2963
+ const icon = categoryInfo?.icon || "\u25CB";
2964
+ const spinner2 = ora3({
2965
+ text: `${indexStr} ${actionVerb} ${chalk8.cyan(collapsePath(file.path))}`,
2966
+ color: "cyan",
2967
+ spinner: "dots",
2968
+ indent: 2
2969
+ }).start();
2970
+ try {
2971
+ const destination = getDestinationPath(tuckDir, category, filename);
2972
+ await ensureDir3(dirname5(destination));
2973
+ if (strategy === "symlink") {
2974
+ await createSymlink(expandedPath, destination, { overwrite: true });
2975
+ } else {
2976
+ await copyFileOrDir(expandedPath, destination, { overwrite: true });
2977
+ }
2978
+ const checksum = await getFileChecksum(destination);
2979
+ const info = await getFileInfo(expandedPath);
2980
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2981
+ const id = generateFileId(file.path);
2982
+ await addFileToManifest(tuckDir, id, {
2983
+ source: collapsePath(file.path),
2984
+ destination: getRelativeDestination(category, filename),
2985
+ category,
2986
+ strategy,
2987
+ // TODO: Encryption and templating are planned for a future version
2988
+ encrypted: false,
2989
+ template: false,
2990
+ permissions: info.permissions,
2991
+ added: now,
2992
+ modified: now,
2993
+ checksum
2994
+ });
2995
+ spinner2.stop();
2996
+ const categoryStr = showCategory ? chalk8.dim(` ${icon} ${category}`) : "";
2997
+ console.log(` ${chalk8.green("\u2713")} ${indexStr} ${collapsePath(file.path)}${categoryStr}`);
2998
+ if (isSensitiveFile(collapsePath(file.path))) {
2999
+ sensitiveFiles.push(file.path);
3000
+ }
3001
+ succeeded++;
3002
+ if (onProgress) {
3003
+ onProgress(i + 1, total);
3004
+ }
3005
+ if (i < files.length - 1 && delayBetween > 0) {
3006
+ await new Promise((resolve3) => setTimeout(resolve3, delayBetween));
3007
+ }
3008
+ } catch (error) {
3009
+ spinner2.stop();
3010
+ const errorObj = error instanceof Error ? error : new Error(String(error));
3011
+ errors.push({ path: file.path, error: errorObj });
3012
+ console.log(` ${chalk8.red("\u2717")} ${indexStr} ${collapsePath(file.path)} ${chalk8.red("- failed")}`);
3013
+ }
3014
+ }
3015
+ console.log();
3016
+ if (succeeded > 0) {
3017
+ console.log(chalk8.green("\u2713"), chalk8.bold(`Tracked ${succeeded} ${succeeded === 1 ? "file" : "files"} successfully`));
3018
+ }
3019
+ if (errors.length > 0) {
3020
+ console.log();
3021
+ console.log(chalk8.red("\u2717"), chalk8.bold(`Failed to track ${errors.length} ${errors.length === 1 ? "file" : "files"}:`));
3022
+ for (const { path, error } of errors) {
3023
+ console.log(chalk8.dim(` \u2022 ${collapsePath(path)}: ${error.message}`));
3024
+ }
3025
+ }
3026
+ if (sensitiveFiles.length > 0) {
3027
+ console.log();
3028
+ console.log(chalk8.yellow("\u26A0"), chalk8.yellow("Warning: Some files may contain sensitive data:"));
3029
+ for (const path of sensitiveFiles) {
3030
+ console.log(chalk8.dim(` \u2022 ${collapsePath(path)}`));
3031
+ }
3032
+ console.log(chalk8.dim(" Make sure your repository is private!"));
3033
+ }
3034
+ return {
3035
+ succeeded,
3036
+ failed: errors.length,
3037
+ errors,
3038
+ sensitiveFiles
3039
+ };
3040
+ };
3041
+
3042
+ // src/commands/init.ts
3043
+ var GITIGNORE_TEMPLATE = `# OS generated files
3044
+ .DS_Store
3045
+ .DS_Store?
3046
+ ._*
3047
+ .Spotlight-V100
3048
+ .Trashes
3049
+ ehthumbs.db
3050
+ Thumbs.db
3051
+
3052
+ # Backup files
3053
+ *.bak
3054
+ *.backup
3055
+ *~
3056
+
3057
+ # Secret files (add patterns for files you want to exclude)
3058
+ # *.secret
3059
+ # .env.local
3060
+ `;
3061
+ var trackFilesWithProgressInit = async (selectedPaths, tuckDir) => {
3062
+ const filesToTrack = selectedPaths.map((path) => ({
3063
+ path
3064
+ }));
3065
+ const result = await trackFilesWithProgress(filesToTrack, tuckDir, {
3066
+ showCategory: true,
3067
+ actionVerb: "Tracking"
3068
+ });
3069
+ return result.succeeded;
3070
+ };
3071
+ var README_TEMPLATE = (machine) => `# Dotfiles
3072
+
3073
+ Managed with [tuck](https://github.com/Pranav-Karra-3301/tuck) - Modern Dotfiles Manager
3074
+
3075
+ ${machine ? `## Machine: ${machine}
3076
+ ` : ""}
3077
+
3078
+ ## Quick Start
3079
+
3080
+ \`\`\`bash
3081
+ # Restore dotfiles to a new machine
3082
+ tuck init --from <this-repo-url>
2509
3083
 
2510
3084
  # Or clone and restore manually
2511
3085
  git clone <this-repo-url> ~/.tuck
@@ -2539,18 +3113,18 @@ tuck restore --all
2539
3113
  \`\`\`
2540
3114
  `;
2541
3115
  var createDirectoryStructure = async (tuckDir) => {
2542
- await ensureDir2(tuckDir);
2543
- await ensureDir2(getFilesDir(tuckDir));
3116
+ await ensureDir5(tuckDir);
3117
+ await ensureDir5(getFilesDir(tuckDir));
2544
3118
  for (const category of Object.keys(CATEGORIES)) {
2545
- await ensureDir2(getCategoryDir(tuckDir, category));
3119
+ await ensureDir5(getCategoryDir(tuckDir, category));
2546
3120
  }
2547
3121
  };
2548
3122
  var createDefaultFiles = async (tuckDir, machine) => {
2549
- const gitignorePath = join6(tuckDir, ".gitignore");
3123
+ const gitignorePath = join10(tuckDir, ".gitignore");
2550
3124
  if (!await pathExists(gitignorePath)) {
2551
3125
  await writeFile4(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
2552
3126
  }
2553
- const readmePath = join6(tuckDir, "README.md");
3127
+ const readmePath = join10(tuckDir, "README.md");
2554
3128
  if (!await pathExists(readmePath)) {
2555
3129
  await writeFile4(readmePath, README_TEMPLATE(machine), "utf-8");
2556
3130
  }
@@ -2670,10 +3244,10 @@ tuck apply ${user.login}`,
2670
3244
  return { remoteUrl, pushed: false };
2671
3245
  };
2672
3246
  var analyzeRepository = async (repoDir) => {
2673
- const manifestPath = join6(repoDir, ".tuckmanifest.json");
3247
+ const manifestPath = join10(repoDir, ".tuckmanifest.json");
2674
3248
  if (await pathExists(manifestPath)) {
2675
3249
  try {
2676
- const content = await readFile4(manifestPath, "utf-8");
3250
+ const content = await readFile5(manifestPath, "utf-8");
2677
3251
  const manifest = JSON.parse(content);
2678
3252
  if (manifest.files && Object.keys(manifest.files).length > 0) {
2679
3253
  return { type: "valid-tuck", manifest };
@@ -2683,7 +3257,7 @@ var analyzeRepository = async (repoDir) => {
2683
3257
  return { type: "messed-up", reason: "Manifest file is corrupted or invalid" };
2684
3258
  }
2685
3259
  }
2686
- const filesDir = join6(repoDir, "files");
3260
+ const filesDir = join10(repoDir, "files");
2687
3261
  const hasFilesDir = await pathExists(filesDir);
2688
3262
  const commonPatterns = [
2689
3263
  ".zshrc",
@@ -2704,7 +3278,7 @@ var analyzeRepository = async (repoDir) => {
2704
3278
  try {
2705
3279
  const categories = await readdir5(filesDir);
2706
3280
  for (const category of categories) {
2707
- const categoryPath = join6(filesDir, category);
3281
+ const categoryPath = join10(filesDir, category);
2708
3282
  const categoryStats = await import("fs/promises").then((fs) => fs.stat(categoryPath).catch(() => null));
2709
3283
  if (categoryStats?.isDirectory()) {
2710
3284
  const files = await readdir5(categoryPath);
@@ -2745,9 +3319,9 @@ var analyzeRepository = async (repoDir) => {
2745
3319
  return { type: "messed-up", reason: "Repository does not contain recognizable dotfiles" };
2746
3320
  };
2747
3321
  var validateDestinationPath = (tuckDir, destination) => {
2748
- const fullPath = resolve2(join6(tuckDir, destination));
3322
+ const fullPath = resolve2(join10(tuckDir, destination));
2749
3323
  const normalizedTuckDir = resolve2(tuckDir);
2750
- return fullPath.startsWith(normalizedTuckDir + sep) || fullPath === normalizedTuckDir;
3324
+ return fullPath.startsWith(normalizedTuckDir + sep2) || fullPath === normalizedTuckDir;
2751
3325
  };
2752
3326
  var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2753
3327
  const { getPreferredRemoteProtocol: getPreferredRemoteProtocol2 } = await Promise.resolve().then(() => (init_github(), github_exports));
@@ -2757,7 +3331,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2757
3331
  prompts.log.step("Importing tuck repository...");
2758
3332
  const spinner2 = prompts.spinner();
2759
3333
  spinner2.start("Copying repository...");
2760
- await copy2(repoDir, tuckDir, { overwrite: true });
3334
+ await copy4(repoDir, tuckDir, { overwrite: true });
2761
3335
  spinner2.stop("Repository copied");
2762
3336
  const fileCount = Object.keys(analysis.manifest.files).length;
2763
3337
  let appliedCount = 0;
@@ -2796,12 +3370,12 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2796
3370
  const applySpinner = prompts.spinner();
2797
3371
  applySpinner.start("Applying dotfiles...");
2798
3372
  for (const file of validFiles) {
2799
- const repoFilePath = join6(tuckDir, file.destination);
3373
+ const repoFilePath = join10(tuckDir, file.destination);
2800
3374
  const destPath = expandPath(file.source);
2801
3375
  if (await pathExists(repoFilePath)) {
2802
- const destDir = join6(destPath, "..");
2803
- await ensureDir2(destDir);
2804
- await copy2(repoFilePath, destPath, { overwrite: true });
3376
+ const destDir = join10(destPath, "..");
3377
+ await ensureDir5(destDir);
3378
+ await copy4(repoFilePath, destPath, { overwrite: true });
2805
3379
  appliedCount++;
2806
3380
  }
2807
3381
  }
@@ -2814,7 +3388,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2814
3388
  prompts.log.info("Importing repository and setting up tuck...");
2815
3389
  const copySpinner = prompts.spinner();
2816
3390
  copySpinner.start("Copying repository contents...");
2817
- await copy2(repoDir, tuckDir, { overwrite: true });
3391
+ await copy4(repoDir, tuckDir, { overwrite: true });
2818
3392
  copySpinner.stop("Repository contents copied");
2819
3393
  await setDefaultBranch(tuckDir, "main");
2820
3394
  const hostname2 = (await import("os")).hostname();
@@ -2869,7 +3443,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2869
3443
  const entries = await readdir4(dir);
2870
3444
  for (const entry of entries) {
2871
3445
  if (entry === ".git" || entry === ".tuckmanifest.json" || entry === ".tuckrc.json") continue;
2872
- const fullPath = join6(dir, entry);
3446
+ const fullPath = join10(dir, entry);
2873
3447
  const stats = await stat7(fullPath).catch(() => null);
2874
3448
  if (stats?.isDirectory()) {
2875
3449
  count += await countFiles(fullPath);
@@ -2976,7 +3550,7 @@ var runInteractiveInit = async () => {
2976
3550
  true
2977
3551
  );
2978
3552
  if (importRepo) {
2979
- const tempDir = join6(tmpdir(), `tuck-import-${Date.now()}`);
3553
+ const tempDir = join10(tmpdir(), `tuck-import-${Date.now()}`);
2980
3554
  const cloneSpinner = prompts.spinner();
2981
3555
  cloneSpinner.start("Cloning repository...");
2982
3556
  let phase = "cloning";
@@ -3037,7 +3611,7 @@ var runInteractiveInit = async () => {
3037
3611
  } finally {
3038
3612
  if (await pathExists(tempDir)) {
3039
3613
  try {
3040
- await rm2(tempDir, { recursive: true, force: true });
3614
+ await rm3(tempDir, { recursive: true, force: true });
3041
3615
  } catch (cleanupError) {
3042
3616
  prompts.log.warning(
3043
3617
  `Failed to clean up temporary directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -3072,7 +3646,9 @@ var runInteractiveInit = async () => {
3072
3646
  prompts.log.success("Repository cloned successfully!");
3073
3647
  const shouldRestore = await prompts.confirm("Would you like to restore dotfiles now?", true);
3074
3648
  if (shouldRestore) {
3075
- prompts.log.info("Run `tuck restore --all` to restore all dotfiles");
3649
+ console.log();
3650
+ const { runRestore: runRestore2 } = await Promise.resolve().then(() => (init_restore(), restore_exports));
3651
+ await runRestore2({ all: true });
3076
3652
  }
3077
3653
  } else {
3078
3654
  await initFromScratch(tuckDir, {});
@@ -3105,19 +3681,28 @@ var runInteractiveInit = async () => {
3105
3681
  console.log();
3106
3682
  const trackNow = await prompts.confirm("Would you like to track some of these now?", true);
3107
3683
  if (trackNow) {
3108
- const options = nonSensitiveFiles.slice(0, 25).map((f) => ({
3684
+ const options = nonSensitiveFiles.map((f) => ({
3109
3685
  value: f.path,
3110
- label: `${collapsePath(f.path)} (${f.category})`
3686
+ label: `${collapsePath(f.path)}`,
3687
+ hint: f.category
3111
3688
  }));
3112
3689
  const selectedFiles = await prompts.multiselect(
3113
3690
  "Select files to track:",
3114
3691
  options
3115
3692
  );
3116
3693
  if (selectedFiles.length > 0) {
3117
- prompts.log.step(
3118
- `Run the following to track these files:
3119
- tuck add ${selectedFiles.join(" ")}`
3120
- );
3694
+ const trackedCount = await trackFilesWithProgressInit(selectedFiles, tuckDir);
3695
+ if (trackedCount > 0) {
3696
+ console.log();
3697
+ const shouldSync = await prompts.confirm("Would you like to sync these changes now?", true);
3698
+ if (shouldSync) {
3699
+ console.log();
3700
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3701
+ await runSync2({});
3702
+ }
3703
+ } else {
3704
+ prompts.outro("No files were tracked");
3705
+ }
3121
3706
  }
3122
3707
  } else {
3123
3708
  prompts.log.info("Run 'tuck scan' later to interactively add files");
@@ -3166,7 +3751,7 @@ var runInit = async (options) => {
3166
3751
  `Push remote: tuck push`
3167
3752
  ]);
3168
3753
  };
3169
- var initCommand = new Command("init").description("Initialize tuck repository").option("-d, --dir <path>", "Directory for tuck repository", "~/.tuck").option("-r, --remote <url>", "Git remote URL to set up").option("--bare", "Initialize without any default files").option("--from <url>", "Clone from existing tuck repository").action(async (options) => {
3754
+ var initCommand = new Command3("init").description("Initialize tuck repository").option("-d, --dir <path>", "Directory for tuck repository", "~/.tuck").option("-r, --remote <url>", "Git remote URL to set up").option("--bare", "Initialize without any default files").option("--from <url>", "Clone from existing tuck repository").action(async (options) => {
3170
3755
  if (!options.remote && !options.bare && !options.from && options.dir === "~/.tuck") {
3171
3756
  await runInteractiveInit();
3172
3757
  } else {
@@ -3174,8 +3759,188 @@ var initCommand = new Command("init").description("Initialize tuck repository").
3174
3759
  }
3175
3760
  });
3176
3761
 
3177
- // src/commands/index.ts
3178
- init_add();
3762
+ // src/commands/add.ts
3763
+ init_ui();
3764
+ init_paths();
3765
+ init_manifest();
3766
+ import { Command as Command4 } from "commander";
3767
+ import { basename as basename3 } from "path";
3768
+ init_errors();
3769
+ init_constants();
3770
+ init_files();
3771
+ var PRIVATE_KEY_PATTERNS = [
3772
+ /^id_rsa$/,
3773
+ /^id_dsa$/,
3774
+ /^id_ecdsa$/,
3775
+ /^id_ed25519$/,
3776
+ /^id_.*$/,
3777
+ // Any id_ file without .pub
3778
+ /\.pem$/,
3779
+ /\.key$/,
3780
+ /^.*_key$/
3781
+ // aws_key, github_key, etc.
3782
+ ];
3783
+ var SENSITIVE_FILE_PATTERNS2 = [
3784
+ /^\.netrc$/,
3785
+ /^\.aws\/credentials$/,
3786
+ /^\.docker\/config\.json$/,
3787
+ /^\.npmrc$/,
3788
+ // May contain tokens
3789
+ /^\.pypirc$/,
3790
+ /^\.kube\/config$/,
3791
+ /^\.ssh\/config$/,
3792
+ /^\.gnupg\//,
3793
+ /credentials/i,
3794
+ /secrets?/i,
3795
+ /tokens?\.json$/i,
3796
+ /\.env$/,
3797
+ /\.env\./
3798
+ ];
3799
+ var isPrivateKey = (path) => {
3800
+ const name = basename3(path);
3801
+ if (path.includes(".ssh/") && !name.endsWith(".pub")) {
3802
+ for (const pattern of PRIVATE_KEY_PATTERNS) {
3803
+ if (pattern.test(name)) {
3804
+ return true;
3805
+ }
3806
+ }
3807
+ }
3808
+ if (name.endsWith(".pem") || name.endsWith(".key")) {
3809
+ return true;
3810
+ }
3811
+ return false;
3812
+ };
3813
+ var isSensitiveFile2 = (path) => {
3814
+ const pathToTest = path.startsWith("~/") ? path.slice(2) : path;
3815
+ for (const pattern of SENSITIVE_FILE_PATTERNS2) {
3816
+ if (pattern.test(pathToTest)) {
3817
+ return true;
3818
+ }
3819
+ }
3820
+ return false;
3821
+ };
3822
+ var validateAndPrepareFiles = async (paths, tuckDir, options) => {
3823
+ const filesToAdd = [];
3824
+ for (const path of paths) {
3825
+ const expandedPath = expandPath(path);
3826
+ const collapsedPath = collapsePath(expandedPath);
3827
+ if (isPrivateKey(collapsedPath)) {
3828
+ throw new Error(
3829
+ `Cannot track private key: ${path}
3830
+ Private keys should NEVER be committed to a repository.
3831
+ If you need to backup SSH keys, use a secure password manager.`
3832
+ );
3833
+ }
3834
+ if (!await pathExists(expandedPath)) {
3835
+ throw new FileNotFoundError(path);
3836
+ }
3837
+ if (await isFileTracked(tuckDir, collapsedPath)) {
3838
+ throw new FileAlreadyTrackedError(path);
3839
+ }
3840
+ const isDir = await isDirectory(expandedPath);
3841
+ const fileCount = isDir ? await getDirectoryFileCount(expandedPath) : 1;
3842
+ const category = options.category || detectCategory(expandedPath);
3843
+ const filename = options.name || sanitizeFilename(expandedPath);
3844
+ const destination = getDestinationPath(tuckDir, category, filename);
3845
+ const sensitive = isSensitiveFile2(collapsedPath);
3846
+ filesToAdd.push({
3847
+ source: collapsedPath,
3848
+ destination,
3849
+ category,
3850
+ filename,
3851
+ isDir,
3852
+ fileCount,
3853
+ sensitive
3854
+ });
3855
+ }
3856
+ return filesToAdd;
3857
+ };
3858
+ var addFiles = async (filesToAdd, tuckDir, options) => {
3859
+ const filesToTrack = filesToAdd.map((f) => ({
3860
+ path: f.source,
3861
+ category: f.category
3862
+ }));
3863
+ await trackFilesWithProgress(filesToTrack, tuckDir, {
3864
+ showCategory: true,
3865
+ strategy: options.symlink ? "symlink" : void 0,
3866
+ actionVerb: "Tracking"
3867
+ });
3868
+ };
3869
+ var runInteractiveAdd = async (tuckDir) => {
3870
+ prompts.intro("tuck add");
3871
+ const pathsInput = await prompts.text("Enter file paths to track (space-separated):", {
3872
+ placeholder: "~/.zshrc ~/.gitconfig",
3873
+ validate: (value) => {
3874
+ if (!value.trim()) return "At least one path is required";
3875
+ return void 0;
3876
+ }
3877
+ });
3878
+ const paths = pathsInput.split(/\s+/).filter(Boolean);
3879
+ let filesToAdd;
3880
+ try {
3881
+ filesToAdd = await validateAndPrepareFiles(paths, tuckDir, {});
3882
+ } catch (error) {
3883
+ if (error instanceof Error) {
3884
+ prompts.log.error(error.message);
3885
+ }
3886
+ prompts.cancel();
3887
+ return;
3888
+ }
3889
+ for (const file of filesToAdd) {
3890
+ prompts.log.step(`${file.source}`);
3891
+ const categoryOptions = Object.entries(CATEGORIES).map(([name, config]) => ({
3892
+ value: name,
3893
+ label: `${config.icon} ${name}`,
3894
+ hint: file.category === name ? "(auto-detected)" : void 0
3895
+ }));
3896
+ categoryOptions.sort((a, b) => {
3897
+ if (a.value === file.category) return -1;
3898
+ if (b.value === file.category) return 1;
3899
+ return 0;
3900
+ });
3901
+ const selectedCategory = await prompts.select("Category:", categoryOptions);
3902
+ file.category = selectedCategory;
3903
+ file.destination = getDestinationPath(tuckDir, file.category, file.filename);
3904
+ }
3905
+ const confirm2 = await prompts.confirm(
3906
+ `Add ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}?`,
3907
+ true
3908
+ );
3909
+ if (!confirm2) {
3910
+ prompts.cancel("Operation cancelled");
3911
+ return;
3912
+ }
3913
+ await addFiles(filesToAdd, tuckDir, {});
3914
+ prompts.outro(`Added ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}`);
3915
+ logger.info("Run 'tuck sync' to commit changes");
3916
+ };
3917
+ var runAdd = async (paths, options) => {
3918
+ const tuckDir = getTuckDir();
3919
+ try {
3920
+ await loadManifest(tuckDir);
3921
+ } catch {
3922
+ throw new NotInitializedError();
3923
+ }
3924
+ if (paths.length === 0) {
3925
+ await runInteractiveAdd(tuckDir);
3926
+ return;
3927
+ }
3928
+ const filesToAdd = await validateAndPrepareFiles(paths, tuckDir, options);
3929
+ await addFiles(filesToAdd, tuckDir, options);
3930
+ console.log();
3931
+ const shouldSync = await prompts.confirm("Would you like to sync these changes now?", true);
3932
+ if (shouldSync) {
3933
+ console.log();
3934
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3935
+ await runSync2({});
3936
+ } else {
3937
+ console.log();
3938
+ logger.info("Run 'tuck sync' when you're ready to commit changes");
3939
+ }
3940
+ };
3941
+ var addCommand = new Command4("add").description("Track new dotfiles").argument("[paths...]", "Paths to dotfiles to track").option("-c, --category <name>", "Category to organize under").option("-n, --name <name>", "Custom name for the file in manifest").option("--symlink", "Create symlink instead of copy").action(async (paths, options) => {
3942
+ await runAdd(paths, options);
3943
+ });
3179
3944
 
3180
3945
  // src/commands/remove.ts
3181
3946
  init_ui();
@@ -3183,8 +3948,8 @@ init_paths();
3183
3948
  init_manifest();
3184
3949
  init_files();
3185
3950
  init_errors();
3186
- import { Command as Command3 } from "commander";
3187
- import { join as join8 } from "path";
3951
+ import { Command as Command5 } from "commander";
3952
+ import { join as join11 } from "path";
3188
3953
  var validateAndPrepareFiles2 = async (paths, tuckDir) => {
3189
3954
  const filesToRemove = [];
3190
3955
  for (const path of paths) {
@@ -3197,7 +3962,7 @@ var validateAndPrepareFiles2 = async (paths, tuckDir) => {
3197
3962
  filesToRemove.push({
3198
3963
  id: tracked.id,
3199
3964
  source: tracked.file.source,
3200
- destination: join8(tuckDir, tracked.file.destination)
3965
+ destination: join11(tuckDir, tracked.file.destination)
3201
3966
  });
3202
3967
  }
3203
3968
  return filesToRemove;
@@ -3254,7 +4019,7 @@ var runInteractiveRemove = async (tuckDir) => {
3254
4019
  return {
3255
4020
  id,
3256
4021
  source: file.source,
3257
- destination: join8(tuckDir, file.destination)
4022
+ destination: join11(tuckDir, file.destination)
3258
4023
  };
3259
4024
  });
3260
4025
  await removeFiles(filesToRemove, tuckDir, { delete: shouldDelete });
@@ -3278,316 +4043,12 @@ var runRemove = async (paths, options) => {
3278
4043
  logger.success(`Removed ${filesToRemove.length} ${filesToRemove.length === 1 ? "item" : "items"} from tracking`);
3279
4044
  logger.info("Run 'tuck sync' to commit changes");
3280
4045
  };
3281
- var removeCommand = new Command3("remove").description("Stop tracking dotfiles").argument("[paths...]", "Paths to dotfiles to untrack").option("--delete", "Also delete from tuck repository").option("--keep-original", "Don't restore symlinks to regular files").action(async (paths, options) => {
4046
+ var removeCommand = new Command5("remove").description("Stop tracking dotfiles").argument("[paths...]", "Paths to dotfiles to untrack").option("--delete", "Also delete from tuck repository").option("--keep-original", "Don't restore symlinks to regular files").action(async (paths, options) => {
3282
4047
  await runRemove(paths, options);
3283
4048
  });
3284
4049
 
3285
- // src/commands/sync.ts
3286
- init_ui();
3287
- init_paths();
3288
- init_manifest();
3289
- init_git();
3290
- init_files();
3291
- import { Command as Command4 } from "commander";
3292
- import chalk9 from "chalk";
3293
- import { join as join9 } from "path";
3294
-
3295
- // src/lib/hooks.ts
3296
- init_config();
3297
- init_logger();
3298
- init_prompts();
3299
- import { exec } from "child_process";
3300
- import { promisify as promisify2 } from "util";
3301
- import chalk8 from "chalk";
3302
- var execAsync = promisify2(exec);
3303
- var runHook = async (hookType, tuckDir, options) => {
3304
- if (options?.skipHooks) {
3305
- return { success: true, skipped: true };
3306
- }
3307
- const config = await loadConfig(tuckDir);
3308
- const command = config.hooks[hookType];
3309
- if (!command) {
3310
- return { success: true };
3311
- }
3312
- if (!options?.trustHooks) {
3313
- console.log();
3314
- console.log(chalk8.yellow.bold("WARNING: Hook Execution"));
3315
- console.log(chalk8.dim("\u2500".repeat(50)));
3316
- console.log(chalk8.white(`Hook type: ${chalk8.cyan(hookType)}`));
3317
- console.log(chalk8.white("Command:"));
3318
- console.log(chalk8.red(` ${command}`));
3319
- console.log(chalk8.dim("\u2500".repeat(50)));
3320
- console.log(
3321
- chalk8.yellow(
3322
- "SECURITY: Hooks can execute arbitrary commands on your system."
3323
- )
3324
- );
3325
- console.log(
3326
- chalk8.yellow(
3327
- "Only proceed if you trust the source of this configuration."
3328
- )
3329
- );
3330
- console.log();
3331
- const confirmed = await prompts.confirm(
3332
- "Execute this hook?",
3333
- false
3334
- // Default to NO for safety
3335
- );
3336
- if (!confirmed) {
3337
- logger.warning(`Hook ${hookType} skipped by user`);
3338
- return { success: true, skipped: true };
3339
- }
3340
- }
3341
- if (!options?.silent) {
3342
- logger.dim(`Running ${hookType} hook...`);
3343
- }
3344
- try {
3345
- const { stdout, stderr } = await execAsync(command, {
3346
- cwd: tuckDir,
3347
- timeout: 3e4,
3348
- // 30 second timeout
3349
- env: {
3350
- ...process.env,
3351
- TUCK_DIR: tuckDir,
3352
- TUCK_HOOK: hookType
3353
- }
3354
- });
3355
- if (stdout && !options?.silent) {
3356
- logger.dim(stdout.trim());
3357
- }
3358
- if (stderr && !options?.silent) {
3359
- logger.warning(stderr.trim());
3360
- }
3361
- return { success: true, output: stdout };
3362
- } catch (error) {
3363
- const errorMessage = error instanceof Error ? error.message : String(error);
3364
- if (!options?.silent) {
3365
- logger.error(`Hook ${hookType} failed: ${errorMessage}`);
3366
- }
3367
- return { success: false, error: errorMessage };
3368
- }
3369
- };
3370
- var runPreSyncHook = async (tuckDir, options) => {
3371
- return runHook("preSync", tuckDir, options);
3372
- };
3373
- var runPostSyncHook = async (tuckDir, options) => {
3374
- return runHook("postSync", tuckDir, options);
3375
- };
3376
- var runPreRestoreHook = async (tuckDir, options) => {
3377
- return runHook("preRestore", tuckDir, options);
3378
- };
3379
- var runPostRestoreHook = async (tuckDir, options) => {
3380
- return runHook("postRestore", tuckDir, options);
3381
- };
3382
-
3383
- // src/commands/sync.ts
3384
- init_errors();
3385
- var detectChanges = async (tuckDir) => {
3386
- const files = await getAllTrackedFiles(tuckDir);
3387
- const changes = [];
3388
- for (const [, file] of Object.entries(files)) {
3389
- const sourcePath = expandPath(file.source);
3390
- if (!await pathExists(sourcePath)) {
3391
- changes.push({
3392
- path: file.source,
3393
- status: "deleted",
3394
- source: file.source,
3395
- destination: file.destination
3396
- });
3397
- continue;
3398
- }
3399
- try {
3400
- const sourceChecksum = await getFileChecksum(sourcePath);
3401
- if (sourceChecksum !== file.checksum) {
3402
- changes.push({
3403
- path: file.source,
3404
- status: "modified",
3405
- source: file.source,
3406
- destination: file.destination
3407
- });
3408
- }
3409
- } catch {
3410
- changes.push({
3411
- path: file.source,
3412
- status: "modified",
3413
- source: file.source,
3414
- destination: file.destination
3415
- });
3416
- }
3417
- }
3418
- return changes;
3419
- };
3420
- var generateCommitMessage = (result) => {
3421
- const parts = [];
3422
- if (result.added.length > 0) {
3423
- parts.push(`Add: ${result.added.join(", ")}`);
3424
- }
3425
- if (result.modified.length > 0) {
3426
- parts.push(`Update: ${result.modified.join(", ")}`);
3427
- }
3428
- if (result.deleted.length > 0) {
3429
- parts.push(`Remove: ${result.deleted.join(", ")}`);
3430
- }
3431
- if (parts.length === 0) {
3432
- return "Sync dotfiles";
3433
- }
3434
- const totalCount = result.added.length + result.modified.length + result.deleted.length;
3435
- if (parts.length === 1 && totalCount <= 3) {
3436
- return parts[0];
3437
- }
3438
- return `Sync: ${totalCount} file${totalCount > 1 ? "s" : ""} changed`;
3439
- };
3440
- var syncFiles = async (tuckDir, changes, options) => {
3441
- const result = {
3442
- modified: [],
3443
- added: [],
3444
- deleted: []
3445
- };
3446
- const hookOptions = {
3447
- skipHooks: options.noHooks,
3448
- trustHooks: options.trustHooks
3449
- };
3450
- await runPreSyncHook(tuckDir, hookOptions);
3451
- for (const change of changes) {
3452
- const sourcePath = expandPath(change.source);
3453
- const destPath = join9(tuckDir, change.destination);
3454
- if (change.status === "modified") {
3455
- await withSpinner(`Syncing ${change.path}...`, async () => {
3456
- await copyFileOrDir(sourcePath, destPath, { overwrite: true });
3457
- const newChecksum = await getFileChecksum(destPath);
3458
- const files = await getAllTrackedFiles(tuckDir);
3459
- const fileId = Object.entries(files).find(([, f]) => f.source === change.source)?.[0];
3460
- if (fileId) {
3461
- await updateFileInManifest(tuckDir, fileId, {
3462
- checksum: newChecksum,
3463
- modified: (/* @__PURE__ */ new Date()).toISOString()
3464
- });
3465
- }
3466
- });
3467
- result.modified.push(change.path.split("/").pop() || change.path);
3468
- } else if (change.status === "deleted") {
3469
- logger.warning(`Source file deleted: ${change.path}`);
3470
- result.deleted.push(change.path.split("/").pop() || change.path);
3471
- }
3472
- }
3473
- if (!options.noCommit && (result.modified.length > 0 || result.deleted.length > 0)) {
3474
- await withSpinner("Staging changes...", async () => {
3475
- await stageAll(tuckDir);
3476
- });
3477
- const message = options.message || generateCommitMessage(result);
3478
- await withSpinner("Committing...", async () => {
3479
- result.commitHash = await commit(tuckDir, message);
3480
- });
3481
- }
3482
- await runPostSyncHook(tuckDir, hookOptions);
3483
- return result;
3484
- };
3485
- var runInteractiveSync = async (tuckDir, options = {}) => {
3486
- prompts.intro("tuck sync");
3487
- const spinner2 = prompts.spinner();
3488
- spinner2.start("Detecting changes...");
3489
- const changes = await detectChanges(tuckDir);
3490
- spinner2.stop("Changes detected");
3491
- if (changes.length === 0) {
3492
- const gitStatus = await getStatus(tuckDir);
3493
- if (gitStatus.hasChanges) {
3494
- prompts.log.info("No dotfile changes, but repository has uncommitted changes");
3495
- const commitAnyway = await prompts.confirm("Commit repository changes?");
3496
- if (commitAnyway) {
3497
- const message2 = await prompts.text("Commit message:", {
3498
- defaultValue: "Update dotfiles"
3499
- });
3500
- await stageAll(tuckDir);
3501
- const hash = await commit(tuckDir, message2);
3502
- prompts.log.success(`Committed: ${hash.slice(0, 7)}`);
3503
- }
3504
- } else {
3505
- prompts.log.success("Everything is up to date");
3506
- }
3507
- return;
3508
- }
3509
- console.log();
3510
- console.log(chalk9.bold("Changes detected:"));
3511
- for (const change of changes) {
3512
- if (change.status === "modified") {
3513
- console.log(chalk9.yellow(` ~ ${change.path}`));
3514
- } else if (change.status === "deleted") {
3515
- console.log(chalk9.red(` - ${change.path}`));
3516
- }
3517
- }
3518
- console.log();
3519
- const confirm2 = await prompts.confirm("Sync these changes?", true);
3520
- if (!confirm2) {
3521
- prompts.cancel("Operation cancelled");
3522
- return;
3523
- }
3524
- const autoMessage = generateCommitMessage({
3525
- modified: changes.filter((c) => c.status === "modified").map((c) => c.path),
3526
- added: [],
3527
- deleted: changes.filter((c) => c.status === "deleted").map((c) => c.path)
3528
- });
3529
- const message = await prompts.text("Commit message:", {
3530
- defaultValue: autoMessage
3531
- });
3532
- const result = await syncFiles(tuckDir, changes, { message });
3533
- console.log();
3534
- if (result.commitHash) {
3535
- prompts.log.success(`Committed: ${result.commitHash.slice(0, 7)}`);
3536
- if (options.push !== false && await hasRemote(tuckDir)) {
3537
- const spinner22 = prompts.spinner();
3538
- spinner22.start("Pushing to remote...");
3539
- try {
3540
- await push(tuckDir);
3541
- spinner22.stop("Pushed to remote");
3542
- } catch {
3543
- spinner22.stop("Push failed (will retry on next sync)");
3544
- }
3545
- } else if (options.push === false) {
3546
- prompts.log.info("Run 'tuck push' when ready to upload");
3547
- }
3548
- }
3549
- prompts.outro("Synced successfully!");
3550
- };
3551
- var runSync = async (messageArg, options) => {
3552
- const tuckDir = getTuckDir();
3553
- try {
3554
- await loadManifest(tuckDir);
3555
- } catch {
3556
- throw new NotInitializedError();
3557
- }
3558
- if (!messageArg && !options.message && !options.all && !options.noCommit && !options.amend) {
3559
- await runInteractiveSync(tuckDir, options);
3560
- return;
3561
- }
3562
- const changes = await detectChanges(tuckDir);
3563
- if (changes.length === 0) {
3564
- logger.info("No changes detected");
3565
- return;
3566
- }
3567
- logger.heading("Changes detected:");
3568
- for (const change of changes) {
3569
- logger.file(change.status === "modified" ? "modify" : "delete", change.path);
3570
- }
3571
- logger.blank();
3572
- const message = messageArg || options.message;
3573
- const result = await syncFiles(tuckDir, changes, { ...options, message });
3574
- logger.blank();
3575
- logger.success(`Synced ${changes.length} file${changes.length > 1 ? "s" : ""}`);
3576
- if (result.commitHash) {
3577
- logger.info(`Commit: ${result.commitHash.slice(0, 7)}`);
3578
- if (options.push !== false && await hasRemote(tuckDir)) {
3579
- await withSpinner("Pushing to remote...", async () => {
3580
- await push(tuckDir);
3581
- });
3582
- logger.success("Pushed to remote");
3583
- } else if (options.push === false) {
3584
- logger.info("Run 'tuck push' when ready to upload");
3585
- }
3586
- }
3587
- };
3588
- var syncCommand = new Command4("sync").description("Sync changes to repository (commits and pushes)").argument("[message]", "Commit message").option("-m, --message <msg>", "Commit message").option("-a, --all", "Sync all tracked files, not just changed").option("--no-commit", "Stage changes but don't commit").option("--no-push", "Commit but don't push to remote").option("--amend", "Amend previous commit").option("--no-hooks", "Skip execution of pre/post sync hooks").option("--trust-hooks", "Trust and run hooks without confirmation (use with caution)").action(async (messageArg, options) => {
3589
- await runSync(messageArg, options);
3590
- });
4050
+ // src/commands/index.ts
4051
+ init_sync();
3591
4052
 
3592
4053
  // src/commands/push.ts
3593
4054
  init_ui();
@@ -3595,8 +4056,8 @@ init_paths();
3595
4056
  init_manifest();
3596
4057
  init_git();
3597
4058
  init_errors();
3598
- import { Command as Command5 } from "commander";
3599
- import chalk10 from "chalk";
4059
+ import { Command as Command6 } from "commander";
4060
+ import chalk12 from "chalk";
3600
4061
  var runInteractivePush = async (tuckDir) => {
3601
4062
  prompts.intro("tuck push");
3602
4063
  const hasRemoteRepo = await hasRemote(tuckDir);
@@ -3625,13 +4086,13 @@ var runInteractivePush = async (tuckDir) => {
3625
4086
  return;
3626
4087
  }
3627
4088
  console.log();
3628
- console.log(chalk10.dim("Remote:"), remoteUrl);
3629
- console.log(chalk10.dim("Branch:"), branch);
4089
+ console.log(chalk12.dim("Remote:"), remoteUrl);
4090
+ console.log(chalk12.dim("Branch:"), branch);
3630
4091
  if (status.ahead > 0) {
3631
- console.log(chalk10.dim("Commits:"), chalk10.green(`\u2191 ${status.ahead} to push`));
4092
+ console.log(chalk12.dim("Commits:"), chalk12.green(`\u2191 ${status.ahead} to push`));
3632
4093
  }
3633
4094
  if (status.behind > 0) {
3634
- console.log(chalk10.dim("Warning:"), chalk10.yellow(`\u2193 ${status.behind} commits behind remote`));
4095
+ console.log(chalk12.dim("Warning:"), chalk12.yellow(`\u2193 ${status.behind} commits behind remote`));
3635
4096
  const pullFirst = await prompts.confirm("Pull changes first?", true);
3636
4097
  if (pullFirst) {
3637
4098
  prompts.log.info("Run 'tuck pull' first, then push");
@@ -3677,7 +4138,7 @@ var runInteractivePush = async (tuckDir) => {
3677
4138
  viewUrl = remoteUrl.replace("git@github.com:", "https://github.com/").replace(".git", "");
3678
4139
  }
3679
4140
  console.log();
3680
- console.log(chalk10.dim("View at:"), chalk10.cyan(viewUrl));
4141
+ console.log(chalk12.dim("View at:"), chalk12.cyan(viewUrl));
3681
4142
  }
3682
4143
  prompts.outro("");
3683
4144
  };
@@ -3719,7 +4180,7 @@ var runPush = async (options) => {
3719
4180
  }
3720
4181
  }
3721
4182
  };
3722
- var pushCommand = new Command5("push").description("Push changes to remote repository").option("-f, --force", "Force push").option("--set-upstream <name>", "Set upstream branch").action(async (options) => {
4183
+ var pushCommand = new Command6("push").description("Push changes to remote repository").option("-f, --force", "Force push").option("--set-upstream <name>", "Set upstream branch").action(async (options) => {
3723
4184
  await runPush(options);
3724
4185
  });
3725
4186
 
@@ -3729,8 +4190,8 @@ init_paths();
3729
4190
  init_manifest();
3730
4191
  init_git();
3731
4192
  init_errors();
3732
- import { Command as Command6 } from "commander";
3733
- import chalk11 from "chalk";
4193
+ import { Command as Command7 } from "commander";
4194
+ import chalk13 from "chalk";
3734
4195
  var runInteractivePull = async (tuckDir) => {
3735
4196
  prompts.intro("tuck pull");
3736
4197
  const hasRemoteRepo = await hasRemote(tuckDir);
@@ -3746,23 +4207,23 @@ var runInteractivePull = async (tuckDir) => {
3746
4207
  const branch = await getCurrentBranch(tuckDir);
3747
4208
  const remoteUrl = await getRemoteUrl(tuckDir);
3748
4209
  console.log();
3749
- console.log(chalk11.dim("Remote:"), remoteUrl);
3750
- console.log(chalk11.dim("Branch:"), branch);
4210
+ console.log(chalk13.dim("Remote:"), remoteUrl);
4211
+ console.log(chalk13.dim("Branch:"), branch);
3751
4212
  if (status.behind === 0) {
3752
4213
  prompts.log.success("Already up to date");
3753
4214
  return;
3754
4215
  }
3755
- console.log(chalk11.dim("Commits:"), chalk11.yellow(`\u2193 ${status.behind} to pull`));
4216
+ console.log(chalk13.dim("Commits:"), chalk13.yellow(`\u2193 ${status.behind} to pull`));
3756
4217
  if (status.ahead > 0) {
3757
4218
  console.log(
3758
- chalk11.dim("Note:"),
3759
- chalk11.yellow(`You also have ${status.ahead} local commit${status.ahead > 1 ? "s" : ""} to push`)
4219
+ chalk13.dim("Note:"),
4220
+ chalk13.yellow(`You also have ${status.ahead} local commit${status.ahead > 1 ? "s" : ""} to push`)
3760
4221
  );
3761
4222
  }
3762
4223
  if (status.modified.length > 0 || status.staged.length > 0) {
3763
4224
  console.log();
3764
4225
  prompts.log.warning("You have uncommitted changes");
3765
- console.log(chalk11.dim("Modified:"), status.modified.join(", "));
4226
+ console.log(chalk13.dim("Modified:"), status.modified.join(", "));
3766
4227
  const continueAnyway = await prompts.confirm("Pull anyway? (may cause merge conflicts)");
3767
4228
  if (!continueAnyway) {
3768
4229
  prompts.cancel("Commit or stash your changes first with 'tuck sync'");
@@ -3807,249 +4268,12 @@ var runPull = async (options) => {
3807
4268
  logger.info("Run 'tuck restore --all' to restore dotfiles");
3808
4269
  }
3809
4270
  };
3810
- var pullCommand = new Command6("pull").description("Pull changes from remote").option("--rebase", "Pull with rebase").option("--restore", "Also restore files to system after pull").action(async (options) => {
4271
+ var pullCommand = new Command7("pull").description("Pull changes from remote").option("--rebase", "Pull with rebase").option("--restore", "Also restore files to system after pull").action(async (options) => {
3811
4272
  await runPull(options);
3812
4273
  });
3813
4274
 
3814
- // src/commands/restore.ts
3815
- init_ui();
3816
- init_paths();
3817
- init_manifest();
3818
- init_config();
3819
- init_files();
3820
- import { Command as Command7 } from "commander";
3821
- import chalk12 from "chalk";
3822
- import { join as join11 } from "path";
3823
- import { chmod, stat as stat5 } from "fs/promises";
3824
-
3825
- // src/lib/backup.ts
3826
- import { join as join10 } from "path";
3827
- init_constants();
3828
- init_paths();
3829
- import { copy as copy4, ensureDir as ensureDir4, pathExists as pathExists3 } from "fs-extra";
3830
- var getBackupDir = () => {
3831
- return expandPath(BACKUP_DIR);
3832
- };
3833
- var formatDateForBackup = (date) => {
3834
- return date.toISOString().slice(0, 10);
3835
- };
3836
- var getTimestampedBackupDir = (date) => {
3837
- const backupRoot = getBackupDir();
3838
- const timestamp = formatDateForBackup(date);
3839
- return join10(backupRoot, timestamp);
3840
- };
3841
- var createBackup = async (sourcePath, customBackupDir) => {
3842
- const expandedSource = expandPath(sourcePath);
3843
- const date = /* @__PURE__ */ new Date();
3844
- if (!await pathExists(expandedSource)) {
3845
- throw new Error(`Source path does not exist: ${sourcePath}`);
3846
- }
3847
- const backupRoot = customBackupDir ? expandPath(customBackupDir) : getTimestampedBackupDir(date);
3848
- await ensureDir4(backupRoot);
3849
- const collapsed = collapsePath(expandedSource);
3850
- const backupName = collapsed.replace(/^~\//, "").replace(/\//g, "_").replace(/^\./, "dot-");
3851
- const timestamp = date.toISOString().replace(/[:.]/g, "-").slice(11, 19);
3852
- const backupPath = join10(backupRoot, `${backupName}_${timestamp}`);
3853
- await copy4(expandedSource, backupPath, { overwrite: true });
3854
- return {
3855
- originalPath: expandedSource,
3856
- backupPath,
3857
- date
3858
- };
3859
- };
3860
-
3861
- // src/commands/restore.ts
3862
- init_errors();
3863
- init_constants();
3864
- var fixSSHPermissions = async (path) => {
3865
- const expandedPath = expandPath(path);
3866
- if (!path.includes(".ssh/") && !path.endsWith(".ssh")) {
3867
- return;
3868
- }
3869
- try {
3870
- const stats = await stat5(expandedPath);
3871
- if (stats.isDirectory()) {
3872
- await chmod(expandedPath, 448);
3873
- } else {
3874
- await chmod(expandedPath, 384);
3875
- }
3876
- } catch {
3877
- }
3878
- };
3879
- var fixGPGPermissions = async (path) => {
3880
- const expandedPath = expandPath(path);
3881
- if (!path.includes(".gnupg/") && !path.endsWith(".gnupg")) {
3882
- return;
3883
- }
3884
- try {
3885
- const stats = await stat5(expandedPath);
3886
- if (stats.isDirectory()) {
3887
- await chmod(expandedPath, 448);
3888
- } else {
3889
- await chmod(expandedPath, 384);
3890
- }
3891
- } catch {
3892
- }
3893
- };
3894
- var prepareFilesToRestore = async (tuckDir, paths) => {
3895
- const allFiles = await getAllTrackedFiles(tuckDir);
3896
- const filesToRestore = [];
3897
- if (paths && paths.length > 0) {
3898
- for (const path of paths) {
3899
- const expandedPath = expandPath(path);
3900
- const collapsedPath = collapsePath(expandedPath);
3901
- const tracked = await getTrackedFileBySource(tuckDir, collapsedPath);
3902
- if (!tracked) {
3903
- throw new FileNotFoundError(`Not tracked: ${path}`);
3904
- }
3905
- filesToRestore.push({
3906
- id: tracked.id,
3907
- source: tracked.file.source,
3908
- destination: join11(tuckDir, tracked.file.destination),
3909
- category: tracked.file.category,
3910
- existsAtTarget: await pathExists(expandedPath)
3911
- });
3912
- }
3913
- } else {
3914
- for (const [id, file] of Object.entries(allFiles)) {
3915
- const targetPath = expandPath(file.source);
3916
- filesToRestore.push({
3917
- id,
3918
- source: file.source,
3919
- destination: join11(tuckDir, file.destination),
3920
- category: file.category,
3921
- existsAtTarget: await pathExists(targetPath)
3922
- });
3923
- }
3924
- }
3925
- return filesToRestore;
3926
- };
3927
- var restoreFiles = async (tuckDir, files, options) => {
3928
- const config = await loadConfig(tuckDir);
3929
- const useSymlink = options.symlink || config.files.strategy === "symlink";
3930
- const shouldBackup = options.backup ?? config.files.backupOnRestore;
3931
- const hookOptions = {
3932
- skipHooks: options.noHooks,
3933
- trustHooks: options.trustHooks
3934
- };
3935
- await runPreRestoreHook(tuckDir, hookOptions);
3936
- let restoredCount = 0;
3937
- for (const file of files) {
3938
- const targetPath = expandPath(file.source);
3939
- if (!await pathExists(file.destination)) {
3940
- logger.warning(`Source not found in repository: ${file.source}`);
3941
- continue;
3942
- }
3943
- if (options.dryRun) {
3944
- if (file.existsAtTarget) {
3945
- logger.file("modify", `${file.source} (would overwrite)`);
3946
- } else {
3947
- logger.file("add", `${file.source} (would create)`);
3948
- }
3949
- continue;
3950
- }
3951
- if (shouldBackup && file.existsAtTarget) {
3952
- await withSpinner(`Backing up ${file.source}...`, async () => {
3953
- await createBackup(targetPath);
3954
- });
3955
- }
3956
- await withSpinner(`Restoring ${file.source}...`, async () => {
3957
- if (useSymlink) {
3958
- await createSymlink(file.destination, targetPath, { overwrite: true });
3959
- } else {
3960
- await copyFileOrDir(file.destination, targetPath, { overwrite: true });
3961
- }
3962
- await fixSSHPermissions(file.source);
3963
- await fixGPGPermissions(file.source);
3964
- });
3965
- restoredCount++;
3966
- }
3967
- await runPostRestoreHook(tuckDir, hookOptions);
3968
- return restoredCount;
3969
- };
3970
- var runInteractiveRestore = async (tuckDir) => {
3971
- prompts.intro("tuck restore");
3972
- const files = await prepareFilesToRestore(tuckDir);
3973
- if (files.length === 0) {
3974
- prompts.log.warning("No files to restore");
3975
- prompts.note("Run 'tuck add <path>' to track files first", "Tip");
3976
- return;
3977
- }
3978
- const fileOptions = files.map((file) => {
3979
- const categoryConfig = CATEGORIES[file.category] || { icon: "\u{1F4C4}" };
3980
- const status = file.existsAtTarget ? chalk12.yellow("(exists, will backup)") : "";
3981
- return {
3982
- value: file.id,
3983
- label: `${categoryConfig.icon} ${file.source} ${status}`,
3984
- hint: file.category
3985
- };
3986
- });
3987
- const selectedIds = await prompts.multiselect("Select files to restore:", fileOptions, true);
3988
- if (selectedIds.length === 0) {
3989
- prompts.cancel("No files selected");
3990
- return;
3991
- }
3992
- const selectedFiles = files.filter((f) => selectedIds.includes(f.id));
3993
- const existingFiles = selectedFiles.filter((f) => f.existsAtTarget);
3994
- if (existingFiles.length > 0) {
3995
- console.log();
3996
- prompts.log.warning(
3997
- `${existingFiles.length} file${existingFiles.length > 1 ? "s" : ""} will be backed up:`
3998
- );
3999
- existingFiles.forEach((f) => console.log(chalk12.dim(` ${f.source}`)));
4000
- console.log();
4001
- }
4002
- const useSymlink = await prompts.select("Restore method:", [
4003
- { value: false, label: "Copy files", hint: "Recommended" },
4004
- { value: true, label: "Create symlinks", hint: "Files stay in tuck repo" }
4005
- ]);
4006
- const confirm2 = await prompts.confirm(
4007
- `Restore ${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}?`,
4008
- true
4009
- );
4010
- if (!confirm2) {
4011
- prompts.cancel("Operation cancelled");
4012
- return;
4013
- }
4014
- const restoredCount = await restoreFiles(tuckDir, selectedFiles, {
4015
- symlink: useSymlink,
4016
- backup: true
4017
- });
4018
- console.log();
4019
- prompts.outro(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
4020
- };
4021
- var runRestore = async (paths, options) => {
4022
- const tuckDir = getTuckDir();
4023
- try {
4024
- await loadManifest(tuckDir);
4025
- } catch {
4026
- throw new NotInitializedError();
4027
- }
4028
- if (paths.length === 0 && !options.all) {
4029
- await runInteractiveRestore(tuckDir);
4030
- return;
4031
- }
4032
- const files = await prepareFilesToRestore(tuckDir, options.all ? void 0 : paths);
4033
- if (files.length === 0) {
4034
- logger.warning("No files to restore");
4035
- return;
4036
- }
4037
- if (options.dryRun) {
4038
- logger.heading("Dry run - would restore:");
4039
- } else {
4040
- logger.heading("Restoring:");
4041
- }
4042
- const restoredCount = await restoreFiles(tuckDir, files, options);
4043
- logger.blank();
4044
- if (options.dryRun) {
4045
- logger.info(`Would restore ${files.length} file${files.length > 1 ? "s" : ""}`);
4046
- } else {
4047
- logger.success(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
4048
- }
4049
- };
4050
- var restoreCommand = new Command7("restore").description("Restore dotfiles to the system").argument("[paths...]", "Paths to restore (or use --all)").option("-a, --all", "Restore all tracked files").option("--symlink", "Create symlinks instead of copies").option("--backup", "Backup existing files before restore").option("--no-backup", "Skip backup of existing files").option("--dry-run", "Show what would be done").option("--no-hooks", "Skip execution of pre/post restore hooks").option("--trust-hooks", "Trust and run hooks without confirmation (use with caution)").action(async (paths, options) => {
4051
- await runRestore(paths, options);
4052
- });
4275
+ // src/commands/index.ts
4276
+ init_restore();
4053
4277
 
4054
4278
  // src/commands/status.ts
4055
4279
  init_ui();
@@ -4060,7 +4284,7 @@ init_files();
4060
4284
  init_errors();
4061
4285
  init_constants();
4062
4286
  import { Command as Command8 } from "commander";
4063
- import chalk13 from "chalk";
4287
+ import chalk14 from "chalk";
4064
4288
  import boxen2 from "boxen";
4065
4289
  var detectFileChanges = async (tuckDir) => {
4066
4290
  const files = await getAllTrackedFiles(tuckDir);
@@ -4139,16 +4363,16 @@ var getFullStatus = async (tuckDir) => {
4139
4363
  };
4140
4364
  var printStatus = (status) => {
4141
4365
  const headerLines = [
4142
- `${chalk13.bold.cyan("tuck")} ${chalk13.dim(`v${VERSION}`)}`,
4366
+ `${chalk14.bold.cyan("tuck")} ${chalk14.dim(`v${VERSION}`)}`,
4143
4367
  "",
4144
- `${chalk13.dim("Repository:")} ${collapsePath(status.tuckDir)}`,
4145
- `${chalk13.dim("Branch:")} ${chalk13.cyan(status.branch)}`
4368
+ `${chalk14.dim("Repository:")} ${collapsePath(status.tuckDir)}`,
4369
+ `${chalk14.dim("Branch:")} ${chalk14.cyan(status.branch)}`
4146
4370
  ];
4147
4371
  if (status.remote) {
4148
4372
  const shortRemote = status.remote.length > 40 ? status.remote.replace(/^https?:\/\//, "").replace(/\.git$/, "") : status.remote;
4149
- headerLines.push(`${chalk13.dim("Remote:")} ${shortRemote}`);
4373
+ headerLines.push(`${chalk14.dim("Remote:")} ${shortRemote}`);
4150
4374
  } else {
4151
- headerLines.push(`${chalk13.dim("Remote:")} ${chalk13.yellow("not configured")}`);
4375
+ headerLines.push(`${chalk14.dim("Remote:")} ${chalk14.yellow("not configured")}`);
4152
4376
  }
4153
4377
  console.log(boxen2(headerLines.join("\n"), {
4154
4378
  padding: { top: 0, bottom: 0, left: 1, right: 1 },
@@ -4159,22 +4383,22 @@ var printStatus = (status) => {
4159
4383
  let remoteInfo = "";
4160
4384
  switch (status.remoteStatus) {
4161
4385
  case "up-to-date":
4162
- remoteInfo = chalk13.green("\u2713 Up to date with remote");
4386
+ remoteInfo = chalk14.green("\u2713 Up to date with remote");
4163
4387
  break;
4164
4388
  case "ahead":
4165
- remoteInfo = chalk13.yellow(`\u2191 ${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead of remote`);
4389
+ remoteInfo = chalk14.yellow(`\u2191 ${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead of remote`);
4166
4390
  break;
4167
4391
  case "behind":
4168
- remoteInfo = chalk13.yellow(`\u2193 ${status.behind} commit${status.behind > 1 ? "s" : ""} behind remote`);
4392
+ remoteInfo = chalk14.yellow(`\u2193 ${status.behind} commit${status.behind > 1 ? "s" : ""} behind remote`);
4169
4393
  break;
4170
4394
  case "diverged":
4171
- remoteInfo = chalk13.red(`\u26A0 Diverged (${status.ahead} ahead, ${status.behind} behind)`);
4395
+ remoteInfo = chalk14.red(`\u26A0 Diverged (${status.ahead} ahead, ${status.behind} behind)`);
4172
4396
  break;
4173
4397
  }
4174
4398
  console.log("\n" + remoteInfo);
4175
4399
  }
4176
4400
  console.log();
4177
- console.log(chalk13.bold(`Tracked Files: ${status.trackedCount}`));
4401
+ console.log(chalk14.bold(`Tracked Files: ${status.trackedCount}`));
4178
4402
  const categoryOrder = ["shell", "git", "editors", "terminal", "ssh", "misc"];
4179
4403
  const sortedCategories = Object.keys(status.categoryCounts).sort((a, b) => {
4180
4404
  const aIdx = categoryOrder.indexOf(a);
@@ -4187,32 +4411,32 @@ var printStatus = (status) => {
4187
4411
  if (sortedCategories.length > 0) {
4188
4412
  for (const category of sortedCategories) {
4189
4413
  const count = status.categoryCounts[category];
4190
- console.log(chalk13.dim(` ${category}: ${count} file${count > 1 ? "s" : ""}`));
4414
+ console.log(chalk14.dim(` ${category}: ${count} file${count > 1 ? "s" : ""}`));
4191
4415
  }
4192
4416
  }
4193
4417
  if (status.changes.length > 0) {
4194
4418
  console.log();
4195
- console.log(chalk13.bold("Changes detected:"));
4419
+ console.log(chalk14.bold("Changes detected:"));
4196
4420
  for (const change of status.changes) {
4197
4421
  const statusText = formatStatus(change.status);
4198
- console.log(` ${statusText}: ${chalk13.cyan(change.path)}`);
4422
+ console.log(` ${statusText}: ${chalk14.cyan(change.path)}`);
4199
4423
  }
4200
4424
  }
4201
4425
  const hasGitChanges = status.gitChanges.staged.length > 0 || status.gitChanges.modified.length > 0 || status.gitChanges.untracked.length > 0;
4202
4426
  if (hasGitChanges) {
4203
4427
  console.log();
4204
- console.log(chalk13.bold("Repository changes:"));
4428
+ console.log(chalk14.bold("Repository changes:"));
4205
4429
  if (status.gitChanges.staged.length > 0) {
4206
- console.log(chalk13.green(" Staged:"));
4207
- status.gitChanges.staged.forEach((f) => console.log(chalk13.green(` + ${f}`)));
4430
+ console.log(chalk14.green(" Staged:"));
4431
+ status.gitChanges.staged.forEach((f) => console.log(chalk14.green(` + ${f}`)));
4208
4432
  }
4209
4433
  if (status.gitChanges.modified.length > 0) {
4210
- console.log(chalk13.yellow(" Modified:"));
4211
- status.gitChanges.modified.forEach((f) => console.log(chalk13.yellow(` ~ ${f}`)));
4434
+ console.log(chalk14.yellow(" Modified:"));
4435
+ status.gitChanges.modified.forEach((f) => console.log(chalk14.yellow(` ~ ${f}`)));
4212
4436
  }
4213
4437
  if (status.gitChanges.untracked.length > 0) {
4214
- console.log(chalk13.dim(" Untracked:"));
4215
- status.gitChanges.untracked.forEach((f) => console.log(chalk13.dim(` ? ${f}`)));
4438
+ console.log(chalk14.dim(" Untracked:"));
4439
+ status.gitChanges.untracked.forEach((f) => console.log(chalk14.dim(` ? ${f}`)));
4216
4440
  }
4217
4441
  }
4218
4442
  console.log();
@@ -4277,7 +4501,7 @@ init_manifest();
4277
4501
  init_errors();
4278
4502
  init_constants();
4279
4503
  import { Command as Command9 } from "commander";
4280
- import chalk14 from "chalk";
4504
+ import chalk15 from "chalk";
4281
4505
  var groupByCategory = async (tuckDir) => {
4282
4506
  const files = await getAllTrackedFiles(tuckDir);
4283
4507
  const groups = /* @__PURE__ */ new Map();
@@ -4316,15 +4540,15 @@ var printList = (groups) => {
4316
4540
  totalFiles += fileCount;
4317
4541
  console.log();
4318
4542
  console.log(
4319
- chalk14.bold(`${group2.icon} ${group2.name}`) + chalk14.dim(` (${formatCount(fileCount, "file")})`)
4543
+ chalk15.bold(`${group2.icon} ${group2.name}`) + chalk15.dim(` (${formatCount(fileCount, "file")})`)
4320
4544
  );
4321
4545
  group2.files.forEach((file, index) => {
4322
4546
  const isLast = index === group2.files.length - 1;
4323
4547
  const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4324
4548
  const name = file.source.split("/").pop() || file.source;
4325
- const arrow = chalk14.dim(" \u2192 ");
4326
- const dest = chalk14.dim(file.source);
4327
- console.log(chalk14.dim(prefix) + chalk14.cyan(name) + arrow + dest);
4549
+ const arrow = chalk15.dim(" \u2192 ");
4550
+ const dest = chalk15.dim(file.source);
4551
+ console.log(chalk15.dim(prefix) + chalk15.cyan(name) + arrow + dest);
4328
4552
  });
4329
4553
  }
4330
4554
  console.log();
@@ -4385,7 +4609,7 @@ init_git();
4385
4609
  init_files();
4386
4610
  init_errors();
4387
4611
  import { Command as Command10 } from "commander";
4388
- import chalk15 from "chalk";
4612
+ import chalk16 from "chalk";
4389
4613
  import { join as join12 } from "path";
4390
4614
  import { readFile as readFile6 } from "fs/promises";
4391
4615
  var getFileDiff = async (tuckDir, source) => {
@@ -4423,19 +4647,19 @@ var getFileDiff = async (tuckDir, source) => {
4423
4647
  };
4424
4648
  var formatUnifiedDiff = (source, systemContent, repoContent) => {
4425
4649
  const lines = [];
4426
- lines.push(chalk15.bold(`--- a/${source} (system)`));
4427
- lines.push(chalk15.bold(`+++ b/${source} (repository)`));
4650
+ lines.push(chalk16.bold(`--- a/${source} (system)`));
4651
+ lines.push(chalk16.bold(`+++ b/${source} (repository)`));
4428
4652
  if (!systemContent && repoContent) {
4429
- lines.push(chalk15.red("File missing on system"));
4430
- lines.push(chalk15.dim("Repository content:"));
4653
+ lines.push(chalk16.red("File missing on system"));
4654
+ lines.push(chalk16.dim("Repository content:"));
4431
4655
  repoContent.split("\n").forEach((line) => {
4432
- lines.push(chalk15.green(`+ ${line}`));
4656
+ lines.push(chalk16.green(`+ ${line}`));
4433
4657
  });
4434
4658
  } else if (systemContent && !repoContent) {
4435
- lines.push(chalk15.yellow("File not yet synced to repository"));
4436
- lines.push(chalk15.dim("System content:"));
4659
+ lines.push(chalk16.yellow("File not yet synced to repository"));
4660
+ lines.push(chalk16.dim("System content:"));
4437
4661
  systemContent.split("\n").forEach((line) => {
4438
- lines.push(chalk15.red(`- ${line}`));
4662
+ lines.push(chalk16.red(`- ${line}`));
4439
4663
  });
4440
4664
  } else if (systemContent && repoContent) {
4441
4665
  const systemLines = systemContent.split("\n");
@@ -4450,16 +4674,16 @@ var formatUnifiedDiff = (source, systemContent, repoContent) => {
4450
4674
  if (!inDiff) {
4451
4675
  inDiff = true;
4452
4676
  diffStart = i;
4453
- lines.push(chalk15.cyan(`@@ -${i + 1} +${i + 1} @@`));
4677
+ lines.push(chalk16.cyan(`@@ -${i + 1} +${i + 1} @@`));
4454
4678
  }
4455
4679
  if (sysLine !== void 0) {
4456
- lines.push(chalk15.red(`- ${sysLine}`));
4680
+ lines.push(chalk16.red(`- ${sysLine}`));
4457
4681
  }
4458
4682
  if (repoLine !== void 0) {
4459
- lines.push(chalk15.green(`+ ${repoLine}`));
4683
+ lines.push(chalk16.green(`+ ${repoLine}`));
4460
4684
  }
4461
4685
  } else if (inDiff) {
4462
- lines.push(chalk15.dim(` ${sysLine || ""}`));
4686
+ lines.push(chalk16.dim(` ${sysLine || ""}`));
4463
4687
  if (i - diffStart > 3) {
4464
4688
  inDiff = false;
4465
4689
  }
@@ -4500,10 +4724,10 @@ var runDiff = async (paths, options) => {
4500
4724
  if (options.stat) {
4501
4725
  prompts.intro("tuck diff");
4502
4726
  console.log();
4503
- console.log(chalk15.bold(`${changedFiles.length} file${changedFiles.length > 1 ? "s" : ""} changed:`));
4727
+ console.log(chalk16.bold(`${changedFiles.length} file${changedFiles.length > 1 ? "s" : ""} changed:`));
4504
4728
  console.log();
4505
4729
  for (const diff of changedFiles) {
4506
- console.log(chalk15.yellow(` ~ ${diff.source}`));
4730
+ console.log(chalk16.yellow(` ~ ${diff.source}`));
4507
4731
  }
4508
4732
  console.log();
4509
4733
  return;
@@ -4524,7 +4748,7 @@ var runDiff = async (paths, options) => {
4524
4748
  continue;
4525
4749
  }
4526
4750
  if (options.stat) {
4527
- console.log(chalk15.yellow(`~ ${path}`));
4751
+ console.log(chalk16.yellow(`~ ${path}`));
4528
4752
  } else {
4529
4753
  console.log(formatUnifiedDiff(path, diff.systemContent, diff.repoContent));
4530
4754
  console.log();
@@ -4542,7 +4766,7 @@ init_config();
4542
4766
  init_manifest();
4543
4767
  init_errors();
4544
4768
  import { Command as Command11 } from "commander";
4545
- import chalk16 from "chalk";
4769
+ import chalk17 from "chalk";
4546
4770
  import { spawn } from "child_process";
4547
4771
  var printConfig = (config) => {
4548
4772
  console.log(JSON.stringify(config, null, 2));
@@ -4608,7 +4832,7 @@ var runConfigList = async () => {
4608
4832
  const config = await loadConfig(tuckDir);
4609
4833
  prompts.intro("tuck config");
4610
4834
  console.log();
4611
- console.log(chalk16.dim("Configuration file:"), collapsePath(getConfigPath(tuckDir)));
4835
+ console.log(chalk17.dim("Configuration file:"), collapsePath(getConfigPath(tuckDir)));
4612
4836
  console.log();
4613
4837
  printConfig(config);
4614
4838
  };
@@ -4704,9 +4928,9 @@ init_github();
4704
4928
  import { Command as Command12 } from "commander";
4705
4929
  import { join as join13 } from "path";
4706
4930
  import { readFile as readFile8, rm as rm4, chmod as chmod2, stat as stat6 } from "fs/promises";
4707
- import { ensureDir as ensureDir5, pathExists as fsPathExists } from "fs-extra";
4931
+ import { ensureDir as ensureDir6, pathExists as fsPathExists } from "fs-extra";
4708
4932
  import { tmpdir as tmpdir2 } from "os";
4709
- import chalk17 from "chalk";
4933
+ import chalk18 from "chalk";
4710
4934
 
4711
4935
  // src/lib/merge.ts
4712
4936
  init_paths();
@@ -4972,7 +5196,7 @@ var resolveSource = async (source) => {
4972
5196
  };
4973
5197
  var cloneSource = async (repoId, isUrl) => {
4974
5198
  const tempDir = join13(tmpdir2(), `tuck-apply-${Date.now()}`);
4975
- await ensureDir5(tempDir);
5199
+ await ensureDir6(tempDir);
4976
5200
  if (isUrl) {
4977
5201
  await cloneRepo(repoId, tempDir);
4978
5202
  } else {
@@ -5028,9 +5252,9 @@ var applyWithMerge = async (files, dryRun) => {
5028
5252
  logger.file("merge", `${collapsePath(file.destination)} (${mergeResult.preservedBlocks} blocks preserved)`);
5029
5253
  } else {
5030
5254
  const { writeFile: writeFile5 } = await import("fs/promises");
5031
- const { ensureDir: ensureDir6 } = await import("fs-extra");
5032
- const { dirname: dirname5 } = await import("path");
5033
- await ensureDir6(dirname5(file.destination));
5255
+ const { ensureDir: ensureDir7 } = await import("fs-extra");
5256
+ const { dirname: dirname6 } = await import("path");
5257
+ await ensureDir7(dirname6(file.destination));
5034
5258
  await writeFile5(file.destination, mergeResult.content, "utf-8");
5035
5259
  logger.file("merge", collapsePath(file.destination));
5036
5260
  }
@@ -5126,11 +5350,11 @@ var runInteractiveApply = async (source, options) => {
5126
5350
  }
5127
5351
  for (const [category, categoryFiles] of Object.entries(byCategory)) {
5128
5352
  const categoryConfig = CATEGORIES[category] || { icon: "\u{1F4C4}" };
5129
- console.log(chalk17.bold(` ${categoryConfig.icon} ${category}`));
5353
+ console.log(chalk18.bold(` ${categoryConfig.icon} ${category}`));
5130
5354
  for (const file of categoryFiles) {
5131
5355
  const exists = await pathExists(file.destination);
5132
- const status = exists ? chalk17.yellow("(will update)") : chalk17.green("(new)");
5133
- console.log(chalk17.dim(` ${collapsePath(file.destination)} ${status}`));
5356
+ const status = exists ? chalk18.yellow("(will update)") : chalk18.green("(new)");
5357
+ console.log(chalk18.dim(` ${collapsePath(file.destination)} ${status}`));
5134
5358
  }
5135
5359
  }
5136
5360
  console.log();
@@ -5290,7 +5514,7 @@ var applyCommand = new Command12("apply").description("Apply dotfiles from a rep
5290
5514
  init_ui();
5291
5515
  init_paths();
5292
5516
  import { Command as Command13 } from "commander";
5293
- import chalk18 from "chalk";
5517
+ import chalk19 from "chalk";
5294
5518
  var showSnapshotList = async () => {
5295
5519
  const snapshots = await listSnapshots();
5296
5520
  if (snapshots.length === 0) {
@@ -5303,11 +5527,11 @@ var showSnapshotList = async () => {
5303
5527
  for (const snapshot of snapshots) {
5304
5528
  const date = formatSnapshotDate(snapshot.id);
5305
5529
  const fileCount = snapshot.files.filter((f) => f.existed).length;
5306
- console.log(chalk18.cyan(` ${snapshot.id}`));
5307
- console.log(chalk18.dim(` Date: ${date}`));
5308
- console.log(chalk18.dim(` Reason: ${snapshot.reason}`));
5309
- console.log(chalk18.dim(` Files: ${fileCount} file(s) backed up`));
5310
- console.log(chalk18.dim(` Machine: ${snapshot.machine}`));
5530
+ console.log(chalk19.cyan(` ${snapshot.id}`));
5531
+ console.log(chalk19.dim(` Date: ${date}`));
5532
+ console.log(chalk19.dim(` Reason: ${snapshot.reason}`));
5533
+ console.log(chalk19.dim(` Files: ${fileCount} file(s) backed up`));
5534
+ console.log(chalk19.dim(` Machine: ${snapshot.machine}`));
5311
5535
  console.log();
5312
5536
  }
5313
5537
  const totalSize = await getSnapshotsSize();
@@ -5318,18 +5542,18 @@ var showSnapshotList = async () => {
5318
5542
  };
5319
5543
  var showSnapshotDetails = (snapshot) => {
5320
5544
  console.log();
5321
- console.log(chalk18.bold("Snapshot Details:"));
5322
- console.log(chalk18.dim(` ID: ${snapshot.id}`));
5323
- console.log(chalk18.dim(` Date: ${formatSnapshotDate(snapshot.id)}`));
5324
- console.log(chalk18.dim(` Reason: ${snapshot.reason}`));
5325
- console.log(chalk18.dim(` Machine: ${snapshot.machine}`));
5545
+ console.log(chalk19.bold("Snapshot Details:"));
5546
+ console.log(chalk19.dim(` ID: ${snapshot.id}`));
5547
+ console.log(chalk19.dim(` Date: ${formatSnapshotDate(snapshot.id)}`));
5548
+ console.log(chalk19.dim(` Reason: ${snapshot.reason}`));
5549
+ console.log(chalk19.dim(` Machine: ${snapshot.machine}`));
5326
5550
  console.log();
5327
- console.log(chalk18.bold("Files in snapshot:"));
5551
+ console.log(chalk19.bold("Files in snapshot:"));
5328
5552
  for (const file of snapshot.files) {
5329
5553
  if (file.existed) {
5330
- console.log(chalk18.dim(` ok ${collapsePath(file.originalPath)}`));
5554
+ console.log(chalk19.dim(` ok ${collapsePath(file.originalPath)}`));
5331
5555
  } else {
5332
- console.log(chalk18.dim(` - ${collapsePath(file.originalPath)} (did not exist)`));
5556
+ console.log(chalk19.dim(` - ${collapsePath(file.originalPath)} (did not exist)`));
5333
5557
  }
5334
5558
  }
5335
5559
  console.log();
@@ -5437,11 +5661,11 @@ var runInteractiveUndo = async () => {
5437
5661
  prompts.log.info("Files in this snapshot:");
5438
5662
  for (const file of snapshot.files.slice(0, 10)) {
5439
5663
  if (file.existed) {
5440
- console.log(chalk18.dim(` ${collapsePath(file.originalPath)}`));
5664
+ console.log(chalk19.dim(` ${collapsePath(file.originalPath)}`));
5441
5665
  }
5442
5666
  }
5443
5667
  if (snapshot.files.length > 10) {
5444
- console.log(chalk18.dim(` ... and ${snapshot.files.length - 10} more`));
5668
+ console.log(chalk19.dim(` ... and ${snapshot.files.length - 10} more`));
5445
5669
  }
5446
5670
  console.log();
5447
5671
  const confirmed = await prompts.confirm("Restore these files?", true);
@@ -5492,7 +5716,7 @@ init_ui();
5492
5716
  init_paths();
5493
5717
  init_manifest();
5494
5718
  import { Command as Command14 } from "commander";
5495
- import chalk19 from "chalk";
5719
+ import chalk20 from "chalk";
5496
5720
  init_errors();
5497
5721
  var groupSelectableByCategory = (files) => {
5498
5722
  const grouped = {};
@@ -5517,18 +5741,18 @@ var displayGroupedFiles = (files, showAll) => {
5517
5741
  const trackedFiles = categoryFiles.filter((f) => f.alreadyTracked);
5518
5742
  console.log();
5519
5743
  console.log(
5520
- chalk19.bold(`${config.icon} ${config.name}`) + chalk19.dim(` (${newFiles.length} new, ${trackedFiles.length} tracked)`)
5744
+ chalk20.bold(`${config.icon} ${config.name}`) + chalk20.dim(` (${newFiles.length} new, ${trackedFiles.length} tracked)`)
5521
5745
  );
5522
- console.log(chalk19.dim("\u2500".repeat(50)));
5746
+ console.log(chalk20.dim("\u2500".repeat(50)));
5523
5747
  for (const file of categoryFiles) {
5524
5748
  if (!showAll && file.alreadyTracked) continue;
5525
- const status = file.selected ? chalk19.green("[x]") : chalk19.dim("[ ]");
5749
+ const status = file.selected ? chalk20.green("[x]") : chalk20.dim("[ ]");
5526
5750
  const name = file.path;
5527
- const tracked = file.alreadyTracked ? chalk19.dim(" (tracked)") : "";
5528
- const sensitive = file.sensitive ? chalk19.yellow(" [!]") : "";
5529
- const dir = file.isDirectory ? chalk19.cyan(" [dir]") : "";
5751
+ const tracked = file.alreadyTracked ? chalk20.dim(" (tracked)") : "";
5752
+ const sensitive = file.sensitive ? chalk20.yellow(" [!]") : "";
5753
+ const dir = file.isDirectory ? chalk20.cyan(" [dir]") : "";
5530
5754
  console.log(` ${status} ${name}${dir}${sensitive}${tracked}`);
5531
- console.log(chalk19.dim(` ${file.description}`));
5755
+ console.log(chalk20.dim(` ${file.description}`));
5532
5756
  }
5533
5757
  }
5534
5758
  };
@@ -5543,16 +5767,16 @@ var runInteractiveSelection = async (files) => {
5543
5767
  for (const [category, categoryFiles] of Object.entries(grouped)) {
5544
5768
  const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
5545
5769
  console.log();
5546
- console.log(chalk19.bold(`${config.icon} ${config.name}`));
5547
- console.log(chalk19.dim(config.description || ""));
5770
+ console.log(chalk20.bold(`${config.icon} ${config.name}`));
5771
+ console.log(chalk20.dim(config.description || ""));
5548
5772
  console.log();
5549
5773
  const options = categoryFiles.map((file) => {
5550
5774
  let label = file.path;
5551
5775
  if (file.sensitive) {
5552
- label += chalk19.yellow(" [!]");
5776
+ label += chalk20.yellow(" [!]");
5553
5777
  }
5554
5778
  if (file.isDirectory) {
5555
- label += chalk19.cyan(" [dir]");
5779
+ label += chalk20.cyan(" [dir]");
5556
5780
  }
5557
5781
  return {
5558
5782
  value: file.path,
@@ -5578,11 +5802,11 @@ var runQuickScan = async (files) => {
5578
5802
  const trackedFiles = files.filter((f) => f.alreadyTracked);
5579
5803
  console.log();
5580
5804
  console.log(
5581
- chalk19.bold.cyan("Detected Dotfiles: ") + chalk19.white(`${newFiles.length} new, ${trackedFiles.length} already tracked`)
5805
+ chalk20.bold.cyan("Detected Dotfiles: ") + chalk20.white(`${newFiles.length} new, ${trackedFiles.length} already tracked`)
5582
5806
  );
5583
5807
  displayGroupedFiles(files, false);
5584
5808
  console.log();
5585
- console.log(chalk19.dim("\u2500".repeat(60)));
5809
+ console.log(chalk20.dim("\u2500".repeat(60)));
5586
5810
  console.log();
5587
5811
  if (newFiles.length > 0) {
5588
5812
  logger.info(`Found ${newFiles.length} new dotfiles to track`);
@@ -5598,32 +5822,36 @@ var showSummary = (selected) => {
5598
5822
  return;
5599
5823
  }
5600
5824
  console.log();
5601
- console.log(chalk19.bold.green(`Selected ${selected.length} files to track:`));
5825
+ console.log(chalk20.bold.cyan(`Selected ${selected.length} files to track:`));
5826
+ console.log(chalk20.dim("\u2500".repeat(50)));
5602
5827
  console.log();
5603
5828
  const grouped = groupSelectableByCategory(selected);
5604
5829
  for (const [category, files] of Object.entries(grouped)) {
5605
5830
  const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
5606
- console.log(chalk19.bold(`${config.icon} ${config.name}`));
5831
+ console.log(chalk20.bold(`${config.icon} ${config.name}`));
5607
5832
  for (const file of files) {
5608
- const sensitive = file.sensitive ? chalk19.yellow(" [!]") : "";
5609
- console.log(chalk19.dim(` \u2022 ${file.path}${sensitive}`));
5833
+ const sensitive = file.sensitive ? chalk20.yellow(" \u26A0") : "";
5834
+ console.log(chalk20.dim(` \u2022 ${collapsePath(file.path)}${sensitive}`));
5610
5835
  }
5836
+ console.log();
5611
5837
  }
5612
- console.log();
5613
5838
  const sensitiveFiles = selected.filter((f) => f.sensitive);
5614
5839
  if (sensitiveFiles.length > 0) {
5615
- console.log(chalk19.yellow("Warning: Some selected files may contain sensitive data:"));
5616
- for (const file of sensitiveFiles) {
5617
- console.log(chalk19.yellow(` \u2022 ${file.path}`));
5618
- }
5619
- console.log(chalk19.dim(" Make sure your repository is private!"));
5840
+ console.log(chalk20.yellow("\u26A0 Warning: Some files may contain sensitive data"));
5841
+ console.log(chalk20.dim(" Make sure your repository is private!"));
5620
5842
  console.log();
5621
5843
  }
5622
- const paths = selected.map((f) => f.path).join(" ");
5623
- console.log(chalk19.bold("Run this command to add the selected files:"));
5624
- console.log();
5625
- console.log(chalk19.cyan(` tuck add ${paths}`));
5626
- console.log();
5844
+ };
5845
+ var addFilesWithProgress = async (selected, tuckDir) => {
5846
+ const filesToTrack = selected.map((f) => ({
5847
+ path: f.path,
5848
+ category: f.category
5849
+ }));
5850
+ const result = await trackFilesWithProgress(filesToTrack, tuckDir, {
5851
+ showCategory: true,
5852
+ actionVerb: "Tracking"
5853
+ });
5854
+ return result.succeeded;
5627
5855
  };
5628
5856
  var runScan = async (options) => {
5629
5857
  const tuckDir = getTuckDir();
@@ -5657,7 +5885,7 @@ var runScan = async (options) => {
5657
5885
  logger.warning(`No dotfiles found in category: ${options.category}`);
5658
5886
  logger.info("Available categories:");
5659
5887
  for (const [key, config] of Object.entries(DETECTION_CATEGORIES)) {
5660
- console.log(chalk19.dim(` ${config.icon} ${key} - ${config.name}`));
5888
+ console.log(chalk20.dim(` ${config.icon} ${key} - ${config.name}`));
5661
5889
  }
5662
5890
  return;
5663
5891
  }
@@ -5716,17 +5944,27 @@ var runScan = async (options) => {
5716
5944
  }
5717
5945
  showSummary(selected);
5718
5946
  const confirmed = await prompts.confirm(
5719
- `Add ${selected.length} files to tuck?`,
5947
+ `Track these ${selected.length} files?`,
5720
5948
  true
5721
5949
  );
5722
5950
  if (!confirmed) {
5723
5951
  prompts.cancel("Operation cancelled");
5724
5952
  return;
5725
5953
  }
5726
- const { addFilesFromPaths: addFilesFromPaths2 } = await Promise.resolve().then(() => (init_add(), add_exports));
5727
- const paths = selected.map((f) => f.path);
5728
- await addFilesFromPaths2(paths, {});
5729
- prompts.outro(`Added ${selected.length} files to tuck!`);
5954
+ const addedCount = await addFilesWithProgress(selected, tuckDir);
5955
+ if (addedCount > 0) {
5956
+ console.log();
5957
+ const shouldSync = await prompts.confirm("Would you like to sync these changes now?", true);
5958
+ if (shouldSync) {
5959
+ console.log();
5960
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
5961
+ await runSync2({});
5962
+ } else {
5963
+ prompts.outro("Run 'tuck sync' when you're ready to commit changes");
5964
+ }
5965
+ } else {
5966
+ prompts.outro("No files were added");
5967
+ }
5730
5968
  };
5731
5969
  var scanCommand = new Command14("scan").description("Scan system for dotfiles and select which to track").option("-a, --all", "Show all files including already tracked ones").option("-c, --category <name>", "Filter by category (shell, git, editors, etc.)").option("-q, --quick", "Quick scan - just show detected files without interactive selection").option("--json", "Output results as JSON").action(async (options) => {
5732
5970
  await runScan(options);
@@ -5741,7 +5979,7 @@ init_manifest();
5741
5979
  init_git();
5742
5980
  var program = new Command15();
5743
5981
  program.name("tuck").description(DESCRIPTION).version(VERSION, "-v, --version", "Display version number").configureOutput({
5744
- outputError: (str, write) => write(chalk20.red(str))
5982
+ outputError: (str, write) => write(chalk21.red(str))
5745
5983
  }).addHelpText("beforeAll", customHelp(VERSION)).helpOption("-h, --help", "Display this help message").showHelpAfterError(false);
5746
5984
  program.configureHelp({
5747
5985
  formatHelp: () => ""
@@ -5764,12 +6002,12 @@ var runDefaultAction = async () => {
5764
6002
  const tuckDir = getTuckDir();
5765
6003
  if (!await pathExists(tuckDir)) {
5766
6004
  miniBanner();
5767
- console.log(chalk20.bold("Get started with tuck:\n"));
5768
- console.log(chalk20.cyan(" tuck init") + chalk20.dim(" - Set up tuck and create a GitHub repo"));
5769
- console.log(chalk20.cyan(" tuck scan") + chalk20.dim(" - Find dotfiles to track"));
6005
+ console.log(chalk21.bold("Get started with tuck:\n"));
6006
+ console.log(chalk21.cyan(" tuck init") + chalk21.dim(" - Set up tuck and create a GitHub repo"));
6007
+ console.log(chalk21.cyan(" tuck scan") + chalk21.dim(" - Find dotfiles to track"));
5770
6008
  console.log();
5771
- console.log(chalk20.dim("On a new machine:"));
5772
- console.log(chalk20.cyan(" tuck apply <username>") + chalk20.dim(" - Apply your dotfiles"));
6009
+ console.log(chalk21.dim("On a new machine:"));
6010
+ console.log(chalk21.cyan(" tuck apply <username>") + chalk21.dim(" - Apply your dotfiles"));
5773
6011
  console.log();
5774
6012
  return;
5775
6013
  }
@@ -5778,38 +6016,38 @@ var runDefaultAction = async () => {
5778
6016
  const trackedCount = Object.keys(manifest.files).length;
5779
6017
  const gitStatus = await getStatus(tuckDir);
5780
6018
  miniBanner();
5781
- console.log(chalk20.bold("Status:\n"));
5782
- console.log(` Tracked files: ${chalk20.cyan(trackedCount.toString())}`);
6019
+ console.log(chalk21.bold("Status:\n"));
6020
+ console.log(` Tracked files: ${chalk21.cyan(trackedCount.toString())}`);
5783
6021
  const pendingChanges = gitStatus.modified.length + gitStatus.staged.length;
5784
6022
  if (pendingChanges > 0) {
5785
- console.log(` Pending changes: ${chalk20.yellow(pendingChanges.toString())}`);
6023
+ console.log(` Pending changes: ${chalk21.yellow(pendingChanges.toString())}`);
5786
6024
  } else {
5787
- console.log(` Pending changes: ${chalk20.dim("none")}`);
6025
+ console.log(` Pending changes: ${chalk21.dim("none")}`);
5788
6026
  }
5789
6027
  if (gitStatus.ahead > 0) {
5790
- console.log(` Commits to push: ${chalk20.yellow(gitStatus.ahead.toString())}`);
6028
+ console.log(` Commits to push: ${chalk21.yellow(gitStatus.ahead.toString())}`);
5791
6029
  }
5792
6030
  console.log();
5793
- console.log(chalk20.bold("Next steps:\n"));
6031
+ console.log(chalk21.bold("Next steps:\n"));
5794
6032
  if (trackedCount === 0) {
5795
- console.log(chalk20.cyan(" tuck scan") + chalk20.dim(" - Find dotfiles to track"));
5796
- console.log(chalk20.cyan(" tuck add <file>") + chalk20.dim(" - Track a specific file"));
6033
+ console.log(chalk21.cyan(" tuck scan") + chalk21.dim(" - Find dotfiles to track"));
6034
+ console.log(chalk21.cyan(" tuck add <file>") + chalk21.dim(" - Track a specific file"));
5797
6035
  } else if (pendingChanges > 0) {
5798
- console.log(chalk20.cyan(" tuck sync") + chalk20.dim(" - Commit and push your changes"));
5799
- console.log(chalk20.cyan(" tuck diff") + chalk20.dim(" - Preview what changed"));
6036
+ console.log(chalk21.cyan(" tuck sync") + chalk21.dim(" - Commit and push your changes"));
6037
+ console.log(chalk21.cyan(" tuck diff") + chalk21.dim(" - Preview what changed"));
5800
6038
  } else if (gitStatus.ahead > 0) {
5801
- console.log(chalk20.cyan(" tuck push") + chalk20.dim(" - Push commits to GitHub"));
6039
+ console.log(chalk21.cyan(" tuck push") + chalk21.dim(" - Push commits to GitHub"));
5802
6040
  } else {
5803
- console.log(chalk20.dim(" All synced! Your dotfiles are up to date."));
6041
+ console.log(chalk21.dim(" All synced! Your dotfiles are up to date."));
5804
6042
  console.log();
5805
- console.log(chalk20.cyan(" tuck scan") + chalk20.dim(" - Find more dotfiles to track"));
5806
- console.log(chalk20.cyan(" tuck list") + chalk20.dim(" - See tracked files"));
6043
+ console.log(chalk21.cyan(" tuck scan") + chalk21.dim(" - Find more dotfiles to track"));
6044
+ console.log(chalk21.cyan(" tuck list") + chalk21.dim(" - See tracked files"));
5807
6045
  }
5808
6046
  console.log();
5809
6047
  } catch {
5810
6048
  miniBanner();
5811
- console.log(chalk20.yellow("Tuck directory exists but may be corrupted."));
5812
- console.log(chalk20.dim("Run `tuck init` to reinitialize."));
6049
+ console.log(chalk21.yellow("Tuck directory exists but may be corrupted."));
6050
+ console.log(chalk21.dim("Run `tuck init` to reinitialize."));
5813
6051
  console.log();
5814
6052
  }
5815
6053
  };