@prnv/tuck 1.2.0 → 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,17 +376,28 @@ 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
 
366
383
  // src/constants.ts
367
384
  import { homedir } from "os";
368
- import { join } from "path";
369
- var VERSION, DESCRIPTION, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES, COMMON_DOTFILES;
385
+ import { join, dirname } from "path";
386
+ import { readFileSync } from "fs";
387
+ import { fileURLToPath } from "url";
388
+ var __dirname, packageJsonPath, VERSION_VALUE, VERSION, DESCRIPTION, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES;
370
389
  var init_constants = __esm({
371
390
  "src/constants.ts"() {
372
391
  "use strict";
373
- VERSION = "0.1.0";
392
+ __dirname = dirname(fileURLToPath(import.meta.url));
393
+ packageJsonPath = join(__dirname, "..", "package.json");
394
+ VERSION_VALUE = "1.0.0";
395
+ try {
396
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
397
+ VERSION_VALUE = pkg.version;
398
+ } catch {
399
+ }
400
+ VERSION = VERSION_VALUE;
374
401
  DESCRIPTION = "Modern dotfiles manager with a beautiful CLI";
375
402
  HOME_DIR = homedir();
376
403
  DEFAULT_TUCK_DIR = join(HOME_DIR, ".tuck");
@@ -430,23 +457,12 @@ var init_constants = __esm({
430
457
  icon: "-"
431
458
  }
432
459
  };
433
- COMMON_DOTFILES = [
434
- { path: "~/.zshrc", category: "shell" },
435
- { path: "~/.bashrc", category: "shell" },
436
- { path: "~/.bash_profile", category: "shell" },
437
- { path: "~/.gitconfig", category: "git" },
438
- { path: "~/.config/nvim", category: "editors" },
439
- { path: "~/.vimrc", category: "editors" },
440
- { path: "~/.tmux.conf", category: "terminal" },
441
- { path: "~/.ssh/config", category: "ssh" },
442
- { path: "~/.config/starship.toml", category: "terminal" }
443
- ];
444
460
  }
445
461
  });
446
462
 
447
463
  // src/lib/paths.ts
448
464
  import { homedir as homedir2 } from "os";
449
- import { join as join2, basename, dirname, relative, isAbsolute, resolve } from "path";
465
+ import { join as join2, basename, dirname as dirname2, relative, isAbsolute, resolve, sep } from "path";
450
466
  import { stat, access } from "fs/promises";
451
467
  import { constants } from "fs";
452
468
  var expandPath, collapsePath, getTuckDir, getManifestPath, getConfigPath, getFilesDir, getCategoryDir, getDestinationPath, getRelativeDestination, sanitizeFilename, detectCategory, pathExists, isDirectory, isPathWithinHome, validateSafeSourcePath, generateFileId;
@@ -532,7 +548,7 @@ var init_paths = __esm({
532
548
  const expandedPath = expandPath(path);
533
549
  const normalizedPath = resolve(expandedPath);
534
550
  const normalizedHome = resolve(home);
535
- return normalizedPath.startsWith(normalizedHome + "/") || normalizedPath === normalizedHome;
551
+ return normalizedPath.startsWith(normalizedHome + sep) || normalizedPath === normalizedHome;
536
552
  };
537
553
  validateSafeSourcePath = (source) => {
538
554
  if (isAbsolute(source) && !source.startsWith(homedir2())) {
@@ -629,7 +645,7 @@ var init_config_schema = __esm({
629
645
  });
630
646
 
631
647
  // src/errors.ts
632
- import chalk6 from "chalk";
648
+ import chalk7 from "chalk";
633
649
  var TuckError, NotInitializedError, AlreadyInitializedError, FileNotFoundError, FileNotTrackedError, FileAlreadyTrackedError, GitError, ConfigError, ManifestError, PermissionError, GitHubCliError, BackupError, handleError;
634
650
  var init_errors = __esm({
635
651
  "src/errors.ts"() {
@@ -726,22 +742,22 @@ var init_errors = __esm({
726
742
  };
727
743
  handleError = (error) => {
728
744
  if (error instanceof TuckError) {
729
- console.error(chalk6.red("x"), error.message);
745
+ console.error(chalk7.red("x"), error.message);
730
746
  if (error.suggestions && error.suggestions.length > 0) {
731
747
  console.error();
732
- console.error(chalk6.dim("Suggestions:"));
733
- 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}`)));
734
750
  }
735
751
  process.exit(1);
736
752
  }
737
753
  if (error instanceof Error) {
738
- console.error(chalk6.red("x"), "An unexpected error occurred:", error.message);
754
+ console.error(chalk7.red("x"), "An unexpected error occurred:", error.message);
739
755
  if (process.env.DEBUG) {
740
756
  console.error(error.stack);
741
757
  }
742
758
  process.exit(1);
743
759
  }
744
- console.error(chalk6.red("x"), "An unknown error occurred");
760
+ console.error(chalk7.red("x"), "An unknown error occurred");
745
761
  process.exit(1);
746
762
  };
747
763
  }
@@ -1160,9 +1176,9 @@ var init_git = __esm({
1160
1176
  const remote = options?.remote || "origin";
1161
1177
  const branch = options?.branch;
1162
1178
  if (branch) {
1163
- await git.push([remote, branch, ...args]);
1179
+ await git.push([...args, remote, branch]);
1164
1180
  } else {
1165
- await git.push([remote, ...args]);
1181
+ await git.push([...args, remote]);
1166
1182
  }
1167
1183
  } catch (error) {
1168
1184
  throw new GitError("Failed to push", String(error));
@@ -1234,9 +1250,15 @@ var init_git = __esm({
1234
1250
  try {
1235
1251
  const git = createGit(dir);
1236
1252
  const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
1237
- return branch;
1238
- } catch (error) {
1239
- throw new GitError("Failed to get current branch", String(error));
1253
+ return branch.trim();
1254
+ } catch {
1255
+ try {
1256
+ const git = createGit(dir);
1257
+ const ref = await git.raw(["symbolic-ref", "--short", "HEAD"]);
1258
+ return ref.trim();
1259
+ } catch {
1260
+ return "main";
1261
+ }
1240
1262
  }
1241
1263
  };
1242
1264
  hasRemote = async (dir, name = "origin") => {
@@ -1478,9 +1500,10 @@ var init_github = __esm({
1478
1500
 
1479
1501
  // src/lib/files.ts
1480
1502
  import { createHash } from "crypto";
1481
- import { readFile as readFile5, stat as stat4, readdir as readdir3, copyFile, symlink, unlink, rm as rm3 } from "fs/promises";
1482
- import { copy as copy3, ensureDir as ensureDir3 } from "fs-extra";
1483
- import { join as join7, dirname as dirname3 } 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";
1484
1507
  var getFileChecksum, getFileInfo, getDirectoryFiles, getDirectoryFileCount, copyFileOrDir, createSymlink, deleteFileOrDir;
1485
1508
  var init_files = __esm({
1486
1509
  "src/lib/files.ts"() {
@@ -1493,12 +1516,12 @@ var init_files = __esm({
1493
1516
  const files = await getDirectoryFiles(expandedPath);
1494
1517
  const hashes = [];
1495
1518
  for (const file of files) {
1496
- const content2 = await readFile5(file);
1519
+ const content2 = await readFile4(file);
1497
1520
  hashes.push(createHash("sha256").update(content2).digest("hex"));
1498
1521
  }
1499
1522
  return createHash("sha256").update(hashes.join("")).digest("hex");
1500
1523
  }
1501
- const content = await readFile5(expandedPath);
1524
+ const content = await readFile4(expandedPath);
1502
1525
  return createHash("sha256").update(content).digest("hex");
1503
1526
  };
1504
1527
  getFileInfo = async (filepath) => {
@@ -1524,14 +1547,27 @@ var init_files = __esm({
1524
1547
  getDirectoryFiles = async (dirpath) => {
1525
1548
  const expandedPath = expandPath(dirpath);
1526
1549
  const files = [];
1527
- 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
+ }
1528
1556
  for (const entry of entries) {
1529
- const entryPath = join7(expandedPath, entry.name);
1530
- if (entry.isDirectory()) {
1531
- const subFiles = await getDirectoryFiles(entryPath);
1532
- files.push(...subFiles);
1533
- } else if (entry.isFile()) {
1534
- 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;
1535
1571
  }
1536
1572
  }
1537
1573
  return files.sort();
@@ -1546,11 +1582,12 @@ var init_files = __esm({
1546
1582
  if (!await pathExists(expandedSource)) {
1547
1583
  throw new FileNotFoundError(source);
1548
1584
  }
1549
- await ensureDir3(dirname3(expandedDest));
1585
+ await ensureDir2(dirname4(expandedDest));
1550
1586
  const sourceIsDir = await isDirectory(expandedSource);
1551
1587
  try {
1588
+ const shouldOverwrite = options?.overwrite ?? true;
1552
1589
  if (sourceIsDir) {
1553
- await copy3(expandedSource, expandedDest, { overwrite: options?.overwrite ?? true });
1590
+ await copy2(expandedSource, expandedDest, { overwrite: shouldOverwrite });
1554
1591
  const fileCount = await getDirectoryFileCount(expandedDest);
1555
1592
  const files = await getDirectoryFiles(expandedDest);
1556
1593
  let totalSize = 0;
@@ -1560,7 +1597,8 @@ var init_files = __esm({
1560
1597
  }
1561
1598
  return { source: expandedSource, destination: expandedDest, fileCount, totalSize };
1562
1599
  } else {
1563
- await copyFile(expandedSource, expandedDest);
1600
+ const copyFlags = shouldOverwrite ? 0 : constants2.COPYFILE_EXCL;
1601
+ await copyFile(expandedSource, expandedDest, copyFlags);
1564
1602
  const stats = await stat4(expandedDest);
1565
1603
  return { source: expandedSource, destination: expandedDest, fileCount: 1, totalSize: stats.size };
1566
1604
  }
@@ -1574,7 +1612,7 @@ var init_files = __esm({
1574
1612
  if (!await pathExists(expandedTarget)) {
1575
1613
  throw new FileNotFoundError(target);
1576
1614
  }
1577
- await ensureDir3(dirname3(expandedLink));
1615
+ await ensureDir2(dirname4(expandedLink));
1578
1616
  if (options?.overwrite && await pathExists(expandedLink)) {
1579
1617
  await unlink(expandedLink);
1580
1618
  }
@@ -1591,7 +1629,7 @@ var init_files = __esm({
1591
1629
  }
1592
1630
  try {
1593
1631
  if (await isDirectory(expandedPath)) {
1594
- await rm3(expandedPath, { recursive: true });
1632
+ await rm2(expandedPath, { recursive: true });
1595
1633
  } else {
1596
1634
  await unlink(expandedPath);
1597
1635
  }
@@ -1602,235 +1640,620 @@ var init_files = __esm({
1602
1640
  }
1603
1641
  });
1604
1642
 
1605
- // src/commands/add.ts
1606
- var add_exports = {};
1607
- __export(add_exports, {
1608
- addCommand: () => addCommand,
1609
- 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
+ }
1610
1683
  });
1611
- import { Command as Command2 } from "commander";
1612
- import { basename as basename3 } from "path";
1613
- import chalk7 from "chalk";
1614
- var PRIVATE_KEY_PATTERNS, SENSITIVE_FILE_PATTERNS, isPrivateKey, isSensitiveFile, validateAndPrepareFiles, addFiles, runInteractiveAdd, addFilesFromPaths, runAdd, addCommand;
1615
- var init_add = __esm({
1616
- "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"() {
1617
1792
  "use strict";
1618
1793
  init_ui();
1619
1794
  init_paths();
1620
- init_config();
1621
1795
  init_manifest();
1796
+ init_config();
1622
1797
  init_files();
1798
+ init_backup();
1799
+ init_hooks();
1623
1800
  init_errors();
1624
1801
  init_constants();
1625
- PRIVATE_KEY_PATTERNS = [
1626
- /^id_rsa$/,
1627
- /^id_dsa$/,
1628
- /^id_ecdsa$/,
1629
- /^id_ed25519$/,
1630
- /^id_.*$/,
1631
- // Any id_ file without .pub
1632
- /\.pem$/,
1633
- /\.key$/,
1634
- /^.*_key$/
1635
- // aws_key, github_key, etc.
1636
- ];
1637
- SENSITIVE_FILE_PATTERNS = [
1638
- /^\.netrc$/,
1639
- /^\.aws\/credentials$/,
1640
- /^\.docker\/config\.json$/,
1641
- /^\.npmrc$/,
1642
- // May contain tokens
1643
- /^\.pypirc$/,
1644
- /^\.kube\/config$/,
1645
- /^\.ssh\/config$/,
1646
- /^\.gnupg\//,
1647
- /credentials/i,
1648
- /secrets?/i,
1649
- /tokens?\.json$/i,
1650
- /\.env$/,
1651
- /\.env\./
1652
- ];
1653
- isPrivateKey = (path) => {
1654
- const name = basename3(path);
1655
- if (path.includes(".ssh/") && !name.endsWith(".pub")) {
1656
- for (const pattern of PRIVATE_KEY_PATTERNS) {
1657
- if (pattern.test(name)) {
1658
- return true;
1659
- }
1660
- }
1802
+ fixSSHPermissions = async (path) => {
1803
+ const expandedPath = expandPath(path);
1804
+ if (!path.includes(".ssh/") && !path.endsWith(".ssh")) {
1805
+ return;
1661
1806
  }
1662
- if (name.endsWith(".pem") || name.endsWith(".key")) {
1663
- 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 {
1664
1815
  }
1665
- return false;
1666
1816
  };
1667
- isSensitiveFile = (path) => {
1668
- const pathToTest = path.startsWith("~/") ? path.slice(2) : path;
1669
- for (const pattern of SENSITIVE_FILE_PATTERNS) {
1670
- if (pattern.test(pathToTest)) {
1671
- 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);
1672
1828
  }
1829
+ } catch {
1673
1830
  }
1674
- return false;
1675
1831
  };
1676
- validateAndPrepareFiles = async (paths, tuckDir, options) => {
1677
- const filesToAdd = [];
1678
- for (const path of paths) {
1679
- const expandedPath = expandPath(path);
1680
- const collapsedPath = collapsePath(expandedPath);
1681
- if (isPrivateKey(collapsedPath)) {
1682
- throw new Error(
1683
- `Cannot track private key: ${path}
1684
- Private keys should NEVER be committed to a repository.
1685
- If you need to backup SSH keys, use a secure password manager.`
1686
- );
1687
- }
1688
- if (!await pathExists(expandedPath)) {
1689
- 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
+ });
1690
1850
  }
1691
- if (await isFileTracked(tuckDir, collapsedPath)) {
1692
- 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
+ });
1693
1861
  }
1694
- const isDir = await isDirectory(expandedPath);
1695
- const fileCount = isDir ? await getDirectoryFileCount(expandedPath) : 1;
1696
- const category = options.category || detectCategory(expandedPath);
1697
- const filename = options.name || sanitizeFilename(expandedPath);
1698
- const destination = getDestinationPath(tuckDir, category, filename);
1699
- const sensitive = isSensitiveFile(collapsedPath);
1700
- filesToAdd.push({
1701
- source: collapsedPath,
1702
- destination,
1703
- category,
1704
- filename,
1705
- isDir,
1706
- fileCount,
1707
- sensitive
1708
- });
1709
1862
  }
1710
- return filesToAdd;
1863
+ return filesToRestore;
1711
1864
  };
1712
- addFiles = async (filesToAdd, tuckDir, options) => {
1865
+ restoreFiles = async (tuckDir, files, options) => {
1713
1866
  const config = await loadConfig(tuckDir);
1714
- const strategy = options.symlink ? "symlink" : config.files.strategy || "copy";
1715
- for (const file of filesToAdd) {
1716
- const expandedSource = expandPath(file.source);
1717
- await withSpinner(`Copying ${file.source}...`, async () => {
1718
- await copyFileOrDir(expandedSource, file.destination, { overwrite: true });
1719
- });
1720
- const checksum = await getFileChecksum(file.destination);
1721
- const info = await getFileInfo(expandedSource);
1722
- const now = (/* @__PURE__ */ new Date()).toISOString();
1723
- const id = generateFileId(file.source);
1724
- await addFileToManifest(tuckDir, id, {
1725
- source: file.source,
1726
- destination: getRelativeDestination(file.category, file.filename),
1727
- category: file.category,
1728
- strategy,
1729
- encrypted: options.encrypt || false,
1730
- template: options.template || false,
1731
- permissions: info.permissions,
1732
- added: now,
1733
- modified: now,
1734
- checksum
1735
- });
1736
- const categoryInfo = CATEGORIES[file.category];
1737
- const icon = categoryInfo?.icon || "-";
1738
- logger.success(`Added ${file.source}`);
1739
- logger.dim(` ${icon} Category: ${file.category}`);
1740
- if (file.isDir) {
1741
- 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;
1742
1888
  }
1743
- if (file.sensitive) {
1744
- console.log(chalk7.yellow(` [!] Warning: This file may contain sensitive data`));
1745
- 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
+ });
1746
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++;
1747
1904
  }
1905
+ await runPostRestoreHook(tuckDir, hookOptions);
1906
+ return restoredCount;
1748
1907
  };
1749
- runInteractiveAdd = async (tuckDir) => {
1750
- prompts.intro("tuck add");
1751
- const pathsInput = await prompts.text("Enter file paths to track (space-separated):", {
1752
- placeholder: "~/.zshrc ~/.gitconfig",
1753
- validate: (value) => {
1754
- if (!value.trim()) return "At least one path is required";
1755
- return void 0;
1756
- }
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
+ };
1757
1924
  });
1758
- const paths = pathsInput.split(/\s+/).filter(Boolean);
1759
- let filesToAdd;
1760
- try {
1761
- filesToAdd = await validateAndPrepareFiles(paths, tuckDir, {});
1762
- } catch (error) {
1763
- if (error instanceof Error) {
1764
- prompts.log.error(error.message);
1765
- }
1766
- 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");
1767
1928
  return;
1768
1929
  }
1769
- for (const file of filesToAdd) {
1770
- prompts.log.step(`${file.source}`);
1771
- const categoryOptions = Object.entries(CATEGORIES).map(([name, config]) => ({
1772
- value: name,
1773
- label: `${config.icon} ${name}`,
1774
- hint: file.category === name ? "(auto-detected)" : void 0
1775
- }));
1776
- categoryOptions.sort((a, b) => {
1777
- if (a.value === file.category) return -1;
1778
- if (b.value === file.category) return 1;
1779
- return 0;
1780
- });
1781
- const selectedCategory = await prompts.select("Category:", categoryOptions);
1782
- file.category = selectedCategory;
1783
- 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();
1784
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
+ ]);
1785
1944
  const confirm2 = await prompts.confirm(
1786
- `Add ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}?`,
1945
+ `Restore ${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}?`,
1787
1946
  true
1788
1947
  );
1789
1948
  if (!confirm2) {
1790
1949
  prompts.cancel("Operation cancelled");
1791
1950
  return;
1792
1951
  }
1793
- await addFiles(filesToAdd, tuckDir, {});
1794
- prompts.outro(`Added ${filesToAdd.length} ${filesToAdd.length === 1 ? "file" : "files"}`);
1795
- 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!");
1796
2201
  };
1797
- addFilesFromPaths = async (paths, options = {}) => {
2202
+ runSync = async (options = {}) => {
1798
2203
  const tuckDir = getTuckDir();
1799
2204
  try {
1800
2205
  await loadManifest(tuckDir);
1801
2206
  } catch {
1802
2207
  throw new NotInitializedError();
1803
2208
  }
1804
- const filesToAdd = await validateAndPrepareFiles(paths, tuckDir, options);
1805
- await addFiles(filesToAdd, tuckDir, options);
1806
- return filesToAdd.length;
2209
+ await runInteractiveSync(tuckDir, options);
1807
2210
  };
1808
- runAdd = async (paths, options) => {
2211
+ runSyncCommand = async (messageArg, options) => {
1809
2212
  const tuckDir = getTuckDir();
1810
2213
  try {
1811
2214
  await loadManifest(tuckDir);
1812
2215
  } catch {
1813
2216
  throw new NotInitializedError();
1814
2217
  }
1815
- if (paths.length === 0) {
1816
- 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");
1817
2225
  return;
1818
2226
  }
1819
- const filesToAdd = await validateAndPrepareFiles(paths, tuckDir, options);
1820
- 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
+ }
2231
+ logger.blank();
2232
+ const message = messageArg || options.message;
2233
+ const result = await syncFiles(tuckDir, changes, { ...options, message });
1821
2234
  logger.blank();
1822
- logger.success(`Added ${filesToAdd.length} ${filesToAdd.length === 1 ? "item" : "items"}`);
1823
- logger.info("Run 'tuck sync' to commit changes");
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
+ }
1824
2247
  };
1825
- 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) => {
1826
- 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);
1827
2250
  });
1828
2251
  }
1829
2252
  });
1830
2253
 
1831
2254
  // src/index.ts
1832
2255
  import { Command as Command15 } from "commander";
1833
- import chalk20 from "chalk";
2256
+ import chalk21 from "chalk";
1834
2257
 
1835
2258
  // src/commands/init.ts
1836
2259
  init_ui();
@@ -1839,10 +2262,10 @@ init_config();
1839
2262
  init_manifest();
1840
2263
  init_git();
1841
2264
  init_github();
1842
- import { Command } from "commander";
1843
- 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";
1844
2267
  import { writeFile as writeFile4 } from "fs/promises";
1845
- import { ensureDir as ensureDir2 } from "fs-extra";
2268
+ import { ensureDir as ensureDir5 } from "fs-extra";
1846
2269
 
1847
2270
  // src/lib/detect.ts
1848
2271
  init_paths();
@@ -2239,7 +2662,7 @@ var detectDotfiles = async () => {
2239
2662
  // src/lib/timemachine.ts
2240
2663
  init_paths();
2241
2664
  init_errors();
2242
- import { join as join5, dirname as dirname2 } from "path";
2665
+ import { join as join5, dirname as dirname3 } from "path";
2243
2666
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3, rm, stat as stat3 } from "fs/promises";
2244
2667
  import { copy, ensureDir, pathExists as pathExists2 } from "fs-extra";
2245
2668
  import { homedir as homedir3 } from "os";
@@ -2273,7 +2696,7 @@ var createSnapshot = async (filePaths, reason, profile) => {
2273
2696
  const backupPath = join5(snapshotPath, "files", backupRelativePath);
2274
2697
  const existed = await pathExists(expandedPath);
2275
2698
  if (existed) {
2276
- await ensureDir(dirname2(backupPath));
2699
+ await ensureDir(dirname3(backupPath));
2277
2700
  await copy(expandedPath, backupPath, { overwrite: true, preserveTimestamps: true });
2278
2701
  }
2279
2702
  files.push({
@@ -2382,7 +2805,7 @@ var restoreSnapshot = async (snapshotId) => {
2382
2805
  continue;
2383
2806
  }
2384
2807
  if (await pathExists2(file.backupPath)) {
2385
- await ensureDir(dirname2(file.originalPath));
2808
+ await ensureDir(dirname3(file.originalPath));
2386
2809
  await copy(file.backupPath, file.originalPath, { overwrite: true, preserveTimestamps: true });
2387
2810
  restoredFiles.push(file.originalPath);
2388
2811
  }
@@ -2410,7 +2833,7 @@ var restoreFileFromSnapshot = async (snapshotId, filePath) => {
2410
2833
  if (!await pathExists2(file.backupPath)) {
2411
2834
  throw new BackupError(`Backup file is missing: ${file.backupPath}`);
2412
2835
  }
2413
- await ensureDir(dirname2(file.originalPath));
2836
+ await ensureDir(dirname3(file.originalPath));
2414
2837
  await copy(file.backupPath, file.originalPath, { overwrite: true, preserveTimestamps: true });
2415
2838
  return true;
2416
2839
  };
@@ -2468,33 +2891,189 @@ var formatSnapshotDate = (snapshotId) => {
2468
2891
  init_errors();
2469
2892
  init_constants();
2470
2893
  init_config_schema();
2471
- import { copy as copy2 } from "fs-extra";
2894
+ import { copy as copy4 } from "fs-extra";
2472
2895
  import { tmpdir } from "os";
2473
- import { readFile as readFile4, rm as rm2 } from "fs/promises";
2474
- var GITIGNORE_TEMPLATE = `# OS generated files
2475
- .DS_Store
2476
- .DS_Store?
2477
- ._*
2478
- .Spotlight-V100
2479
- .Trashes
2480
- ehthumbs.db
2481
- Thumbs.db
2482
-
2483
- # Backup files
2484
- *.bak
2485
- *.backup
2486
- *~
2487
-
2488
- # Secret files (add patterns for files you want to exclude)
2489
- # *.secret
2490
- # .env.local
2491
- `;
2492
- var README_TEMPLATE = (machine) => `# Dotfiles
2493
-
2494
- Managed with [tuck](https://github.com/Pranav-Karra-3301/tuck) - Modern Dotfiles Manager
2495
-
2496
- ${machine ? `## Machine: ${machine}
2497
- ` : ""}
2896
+ import { readFile as readFile5, rm as rm3 } from "fs/promises";
2897
+
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
+ ` : ""}
2498
3077
 
2499
3078
  ## Quick Start
2500
3079
 
@@ -2534,18 +3113,18 @@ tuck restore --all
2534
3113
  \`\`\`
2535
3114
  `;
2536
3115
  var createDirectoryStructure = async (tuckDir) => {
2537
- await ensureDir2(tuckDir);
2538
- await ensureDir2(getFilesDir(tuckDir));
3116
+ await ensureDir5(tuckDir);
3117
+ await ensureDir5(getFilesDir(tuckDir));
2539
3118
  for (const category of Object.keys(CATEGORIES)) {
2540
- await ensureDir2(getCategoryDir(tuckDir, category));
3119
+ await ensureDir5(getCategoryDir(tuckDir, category));
2541
3120
  }
2542
3121
  };
2543
3122
  var createDefaultFiles = async (tuckDir, machine) => {
2544
- const gitignorePath = join6(tuckDir, ".gitignore");
3123
+ const gitignorePath = join10(tuckDir, ".gitignore");
2545
3124
  if (!await pathExists(gitignorePath)) {
2546
3125
  await writeFile4(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
2547
3126
  }
2548
- const readmePath = join6(tuckDir, "README.md");
3127
+ const readmePath = join10(tuckDir, "README.md");
2549
3128
  if (!await pathExists(readmePath)) {
2550
3129
  await writeFile4(readmePath, README_TEMPLATE(machine), "utf-8");
2551
3130
  }
@@ -2665,10 +3244,10 @@ tuck apply ${user.login}`,
2665
3244
  return { remoteUrl, pushed: false };
2666
3245
  };
2667
3246
  var analyzeRepository = async (repoDir) => {
2668
- const manifestPath = join6(repoDir, ".tuckmanifest.json");
3247
+ const manifestPath = join10(repoDir, ".tuckmanifest.json");
2669
3248
  if (await pathExists(manifestPath)) {
2670
3249
  try {
2671
- const content = await readFile4(manifestPath, "utf-8");
3250
+ const content = await readFile5(manifestPath, "utf-8");
2672
3251
  const manifest = JSON.parse(content);
2673
3252
  if (manifest.files && Object.keys(manifest.files).length > 0) {
2674
3253
  return { type: "valid-tuck", manifest };
@@ -2678,7 +3257,7 @@ var analyzeRepository = async (repoDir) => {
2678
3257
  return { type: "messed-up", reason: "Manifest file is corrupted or invalid" };
2679
3258
  }
2680
3259
  }
2681
- const filesDir = join6(repoDir, "files");
3260
+ const filesDir = join10(repoDir, "files");
2682
3261
  const hasFilesDir = await pathExists(filesDir);
2683
3262
  const commonPatterns = [
2684
3263
  ".zshrc",
@@ -2699,7 +3278,7 @@ var analyzeRepository = async (repoDir) => {
2699
3278
  try {
2700
3279
  const categories = await readdir5(filesDir);
2701
3280
  for (const category of categories) {
2702
- const categoryPath = join6(filesDir, category);
3281
+ const categoryPath = join10(filesDir, category);
2703
3282
  const categoryStats = await import("fs/promises").then((fs) => fs.stat(categoryPath).catch(() => null));
2704
3283
  if (categoryStats?.isDirectory()) {
2705
3284
  const files = await readdir5(categoryPath);
@@ -2740,9 +3319,9 @@ var analyzeRepository = async (repoDir) => {
2740
3319
  return { type: "messed-up", reason: "Repository does not contain recognizable dotfiles" };
2741
3320
  };
2742
3321
  var validateDestinationPath = (tuckDir, destination) => {
2743
- const fullPath = resolve2(join6(tuckDir, destination));
3322
+ const fullPath = resolve2(join10(tuckDir, destination));
2744
3323
  const normalizedTuckDir = resolve2(tuckDir);
2745
- return fullPath.startsWith(normalizedTuckDir + sep) || fullPath === normalizedTuckDir;
3324
+ return fullPath.startsWith(normalizedTuckDir + sep2) || fullPath === normalizedTuckDir;
2746
3325
  };
2747
3326
  var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2748
3327
  const { getPreferredRemoteProtocol: getPreferredRemoteProtocol2 } = await Promise.resolve().then(() => (init_github(), github_exports));
@@ -2752,7 +3331,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2752
3331
  prompts.log.step("Importing tuck repository...");
2753
3332
  const spinner2 = prompts.spinner();
2754
3333
  spinner2.start("Copying repository...");
2755
- await copy2(repoDir, tuckDir, { overwrite: true });
3334
+ await copy4(repoDir, tuckDir, { overwrite: true });
2756
3335
  spinner2.stop("Repository copied");
2757
3336
  const fileCount = Object.keys(analysis.manifest.files).length;
2758
3337
  let appliedCount = 0;
@@ -2791,12 +3370,12 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2791
3370
  const applySpinner = prompts.spinner();
2792
3371
  applySpinner.start("Applying dotfiles...");
2793
3372
  for (const file of validFiles) {
2794
- const repoFilePath = join6(tuckDir, file.destination);
3373
+ const repoFilePath = join10(tuckDir, file.destination);
2795
3374
  const destPath = expandPath(file.source);
2796
3375
  if (await pathExists(repoFilePath)) {
2797
- const destDir = join6(destPath, "..");
2798
- await ensureDir2(destDir);
2799
- await copy2(repoFilePath, destPath, { overwrite: true });
3376
+ const destDir = join10(destPath, "..");
3377
+ await ensureDir5(destDir);
3378
+ await copy4(repoFilePath, destPath, { overwrite: true });
2800
3379
  appliedCount++;
2801
3380
  }
2802
3381
  }
@@ -2809,7 +3388,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2809
3388
  prompts.log.info("Importing repository and setting up tuck...");
2810
3389
  const copySpinner = prompts.spinner();
2811
3390
  copySpinner.start("Copying repository contents...");
2812
- await copy2(repoDir, tuckDir, { overwrite: true });
3391
+ await copy4(repoDir, tuckDir, { overwrite: true });
2813
3392
  copySpinner.stop("Repository contents copied");
2814
3393
  await setDefaultBranch(tuckDir, "main");
2815
3394
  const hostname2 = (await import("os")).hostname();
@@ -2864,7 +3443,7 @@ var importExistingRepo = async (tuckDir, repoName, analysis, repoDir) => {
2864
3443
  const entries = await readdir4(dir);
2865
3444
  for (const entry of entries) {
2866
3445
  if (entry === ".git" || entry === ".tuckmanifest.json" || entry === ".tuckrc.json") continue;
2867
- const fullPath = join6(dir, entry);
3446
+ const fullPath = join10(dir, entry);
2868
3447
  const stats = await stat7(fullPath).catch(() => null);
2869
3448
  if (stats?.isDirectory()) {
2870
3449
  count += await countFiles(fullPath);
@@ -2971,7 +3550,7 @@ var runInteractiveInit = async () => {
2971
3550
  true
2972
3551
  );
2973
3552
  if (importRepo) {
2974
- const tempDir = join6(tmpdir(), `tuck-import-${Date.now()}`);
3553
+ const tempDir = join10(tmpdir(), `tuck-import-${Date.now()}`);
2975
3554
  const cloneSpinner = prompts.spinner();
2976
3555
  cloneSpinner.start("Cloning repository...");
2977
3556
  let phase = "cloning";
@@ -3032,7 +3611,7 @@ var runInteractiveInit = async () => {
3032
3611
  } finally {
3033
3612
  if (await pathExists(tempDir)) {
3034
3613
  try {
3035
- await rm2(tempDir, { recursive: true, force: true });
3614
+ await rm3(tempDir, { recursive: true, force: true });
3036
3615
  } catch (cleanupError) {
3037
3616
  prompts.log.warning(
3038
3617
  `Failed to clean up temporary directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -3067,33 +3646,66 @@ var runInteractiveInit = async () => {
3067
3646
  prompts.log.success("Repository cloned successfully!");
3068
3647
  const shouldRestore = await prompts.confirm("Would you like to restore dotfiles now?", true);
3069
3648
  if (shouldRestore) {
3070
- 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 });
3071
3652
  }
3072
3653
  } else {
3073
- const existingDotfiles = [];
3074
- for (const df of COMMON_DOTFILES) {
3075
- const fullPath = expandPath(df.path);
3076
- if (await pathExists(fullPath)) {
3077
- existingDotfiles.push({
3078
- path: df.path,
3079
- label: `${df.path} (${df.category})`
3080
- });
3081
- }
3082
- }
3083
3654
  await initFromScratch(tuckDir, {});
3084
- if (existingDotfiles.length > 0) {
3085
- const selectedFiles = await prompts.multiselect(
3086
- "Would you like to track some common dotfiles?",
3087
- existingDotfiles.map((f) => ({
3655
+ const scanSpinner = prompts.spinner();
3656
+ scanSpinner.start("Scanning for dotfiles...");
3657
+ const detectedFiles = await detectDotfiles();
3658
+ const nonSensitiveFiles = detectedFiles.filter((f) => !f.sensitive);
3659
+ scanSpinner.stop(`Found ${nonSensitiveFiles.length} dotfiles on your system`);
3660
+ if (nonSensitiveFiles.length > 0) {
3661
+ const grouped = {};
3662
+ for (const file of nonSensitiveFiles) {
3663
+ if (!grouped[file.category]) grouped[file.category] = [];
3664
+ grouped[file.category].push(file);
3665
+ }
3666
+ console.log();
3667
+ const categoryOrder = ["shell", "git", "editors", "terminal", "ssh", "misc"];
3668
+ const sortedCategories = Object.keys(grouped).sort((a, b) => {
3669
+ const aIdx = categoryOrder.indexOf(a);
3670
+ const bIdx = categoryOrder.indexOf(b);
3671
+ if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
3672
+ if (aIdx === -1) return 1;
3673
+ if (bIdx === -1) return -1;
3674
+ return aIdx - bIdx;
3675
+ });
3676
+ for (const category of sortedCategories) {
3677
+ const files = grouped[category];
3678
+ const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
3679
+ console.log(` ${config.icon} ${config.name}: ${files.length} files`);
3680
+ }
3681
+ console.log();
3682
+ const trackNow = await prompts.confirm("Would you like to track some of these now?", true);
3683
+ if (trackNow) {
3684
+ const options = nonSensitiveFiles.map((f) => ({
3088
3685
  value: f.path,
3089
- label: f.label
3090
- }))
3091
- );
3092
- if (selectedFiles.length > 0) {
3093
- prompts.log.step(
3094
- `Run the following to track these files:
3095
- tuck add ${selectedFiles.join(" ")}`
3686
+ label: `${collapsePath(f.path)}`,
3687
+ hint: f.category
3688
+ }));
3689
+ const selectedFiles = await prompts.multiselect(
3690
+ "Select files to track:",
3691
+ options
3096
3692
  );
3693
+ if (selectedFiles.length > 0) {
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
+ }
3706
+ }
3707
+ } else {
3708
+ prompts.log.info("Run 'tuck scan' later to interactively add files");
3097
3709
  }
3098
3710
  }
3099
3711
  const wantsRemote = await prompts.confirm("Would you like to set up a remote repository?");
@@ -3139,7 +3751,7 @@ var runInit = async (options) => {
3139
3751
  `Push remote: tuck push`
3140
3752
  ]);
3141
3753
  };
3142
- 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) => {
3143
3755
  if (!options.remote && !options.bare && !options.from && options.dir === "~/.tuck") {
3144
3756
  await runInteractiveInit();
3145
3757
  } else {
@@ -3147,8 +3759,188 @@ var initCommand = new Command("init").description("Initialize tuck repository").
3147
3759
  }
3148
3760
  });
3149
3761
 
3150
- // src/commands/index.ts
3151
- 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
+ });
3152
3944
 
3153
3945
  // src/commands/remove.ts
3154
3946
  init_ui();
@@ -3156,8 +3948,8 @@ init_paths();
3156
3948
  init_manifest();
3157
3949
  init_files();
3158
3950
  init_errors();
3159
- import { Command as Command3 } from "commander";
3160
- import { join as join8 } from "path";
3951
+ import { Command as Command5 } from "commander";
3952
+ import { join as join11 } from "path";
3161
3953
  var validateAndPrepareFiles2 = async (paths, tuckDir) => {
3162
3954
  const filesToRemove = [];
3163
3955
  for (const path of paths) {
@@ -3170,7 +3962,7 @@ var validateAndPrepareFiles2 = async (paths, tuckDir) => {
3170
3962
  filesToRemove.push({
3171
3963
  id: tracked.id,
3172
3964
  source: tracked.file.source,
3173
- destination: join8(tuckDir, tracked.file.destination)
3965
+ destination: join11(tuckDir, tracked.file.destination)
3174
3966
  });
3175
3967
  }
3176
3968
  return filesToRemove;
@@ -3227,7 +4019,7 @@ var runInteractiveRemove = async (tuckDir) => {
3227
4019
  return {
3228
4020
  id,
3229
4021
  source: file.source,
3230
- destination: join8(tuckDir, file.destination)
4022
+ destination: join11(tuckDir, file.destination)
3231
4023
  };
3232
4024
  });
3233
4025
  await removeFiles(filesToRemove, tuckDir, { delete: shouldDelete });
@@ -3251,316 +4043,12 @@ var runRemove = async (paths, options) => {
3251
4043
  logger.success(`Removed ${filesToRemove.length} ${filesToRemove.length === 1 ? "item" : "items"} from tracking`);
3252
4044
  logger.info("Run 'tuck sync' to commit changes");
3253
4045
  };
3254
- 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) => {
3255
4047
  await runRemove(paths, options);
3256
4048
  });
3257
4049
 
3258
- // src/commands/sync.ts
3259
- init_ui();
3260
- init_paths();
3261
- init_manifest();
3262
- init_git();
3263
- init_files();
3264
- import { Command as Command4 } from "commander";
3265
- import chalk9 from "chalk";
3266
- import { join as join9 } from "path";
3267
-
3268
- // src/lib/hooks.ts
3269
- init_config();
3270
- init_logger();
3271
- init_prompts();
3272
- import { exec } from "child_process";
3273
- import { promisify as promisify2 } from "util";
3274
- import chalk8 from "chalk";
3275
- var execAsync = promisify2(exec);
3276
- var runHook = async (hookType, tuckDir, options) => {
3277
- if (options?.skipHooks) {
3278
- return { success: true, skipped: true };
3279
- }
3280
- const config = await loadConfig(tuckDir);
3281
- const command = config.hooks[hookType];
3282
- if (!command) {
3283
- return { success: true };
3284
- }
3285
- if (!options?.trustHooks) {
3286
- console.log();
3287
- console.log(chalk8.yellow.bold("WARNING: Hook Execution"));
3288
- console.log(chalk8.dim("\u2500".repeat(50)));
3289
- console.log(chalk8.white(`Hook type: ${chalk8.cyan(hookType)}`));
3290
- console.log(chalk8.white("Command:"));
3291
- console.log(chalk8.red(` ${command}`));
3292
- console.log(chalk8.dim("\u2500".repeat(50)));
3293
- console.log(
3294
- chalk8.yellow(
3295
- "SECURITY: Hooks can execute arbitrary commands on your system."
3296
- )
3297
- );
3298
- console.log(
3299
- chalk8.yellow(
3300
- "Only proceed if you trust the source of this configuration."
3301
- )
3302
- );
3303
- console.log();
3304
- const confirmed = await prompts.confirm(
3305
- "Execute this hook?",
3306
- false
3307
- // Default to NO for safety
3308
- );
3309
- if (!confirmed) {
3310
- logger.warning(`Hook ${hookType} skipped by user`);
3311
- return { success: true, skipped: true };
3312
- }
3313
- }
3314
- if (!options?.silent) {
3315
- logger.dim(`Running ${hookType} hook...`);
3316
- }
3317
- try {
3318
- const { stdout, stderr } = await execAsync(command, {
3319
- cwd: tuckDir,
3320
- timeout: 3e4,
3321
- // 30 second timeout
3322
- env: {
3323
- ...process.env,
3324
- TUCK_DIR: tuckDir,
3325
- TUCK_HOOK: hookType
3326
- }
3327
- });
3328
- if (stdout && !options?.silent) {
3329
- logger.dim(stdout.trim());
3330
- }
3331
- if (stderr && !options?.silent) {
3332
- logger.warning(stderr.trim());
3333
- }
3334
- return { success: true, output: stdout };
3335
- } catch (error) {
3336
- const errorMessage = error instanceof Error ? error.message : String(error);
3337
- if (!options?.silent) {
3338
- logger.error(`Hook ${hookType} failed: ${errorMessage}`);
3339
- }
3340
- return { success: false, error: errorMessage };
3341
- }
3342
- };
3343
- var runPreSyncHook = async (tuckDir, options) => {
3344
- return runHook("preSync", tuckDir, options);
3345
- };
3346
- var runPostSyncHook = async (tuckDir, options) => {
3347
- return runHook("postSync", tuckDir, options);
3348
- };
3349
- var runPreRestoreHook = async (tuckDir, options) => {
3350
- return runHook("preRestore", tuckDir, options);
3351
- };
3352
- var runPostRestoreHook = async (tuckDir, options) => {
3353
- return runHook("postRestore", tuckDir, options);
3354
- };
3355
-
3356
- // src/commands/sync.ts
3357
- init_errors();
3358
- var detectChanges = async (tuckDir) => {
3359
- const files = await getAllTrackedFiles(tuckDir);
3360
- const changes = [];
3361
- for (const [, file] of Object.entries(files)) {
3362
- const sourcePath = expandPath(file.source);
3363
- if (!await pathExists(sourcePath)) {
3364
- changes.push({
3365
- path: file.source,
3366
- status: "deleted",
3367
- source: file.source,
3368
- destination: file.destination
3369
- });
3370
- continue;
3371
- }
3372
- try {
3373
- const sourceChecksum = await getFileChecksum(sourcePath);
3374
- if (sourceChecksum !== file.checksum) {
3375
- changes.push({
3376
- path: file.source,
3377
- status: "modified",
3378
- source: file.source,
3379
- destination: file.destination
3380
- });
3381
- }
3382
- } catch {
3383
- changes.push({
3384
- path: file.source,
3385
- status: "modified",
3386
- source: file.source,
3387
- destination: file.destination
3388
- });
3389
- }
3390
- }
3391
- return changes;
3392
- };
3393
- var generateCommitMessage = (result) => {
3394
- const parts = [];
3395
- if (result.added.length > 0) {
3396
- parts.push(`Add: ${result.added.join(", ")}`);
3397
- }
3398
- if (result.modified.length > 0) {
3399
- parts.push(`Update: ${result.modified.join(", ")}`);
3400
- }
3401
- if (result.deleted.length > 0) {
3402
- parts.push(`Remove: ${result.deleted.join(", ")}`);
3403
- }
3404
- if (parts.length === 0) {
3405
- return "Sync dotfiles";
3406
- }
3407
- const totalCount = result.added.length + result.modified.length + result.deleted.length;
3408
- if (parts.length === 1 && totalCount <= 3) {
3409
- return parts[0];
3410
- }
3411
- return `Sync: ${totalCount} file${totalCount > 1 ? "s" : ""} changed`;
3412
- };
3413
- var syncFiles = async (tuckDir, changes, options) => {
3414
- const result = {
3415
- modified: [],
3416
- added: [],
3417
- deleted: []
3418
- };
3419
- const hookOptions = {
3420
- skipHooks: options.noHooks,
3421
- trustHooks: options.trustHooks
3422
- };
3423
- await runPreSyncHook(tuckDir, hookOptions);
3424
- for (const change of changes) {
3425
- const sourcePath = expandPath(change.source);
3426
- const destPath = join9(tuckDir, change.destination);
3427
- if (change.status === "modified") {
3428
- await withSpinner(`Syncing ${change.path}...`, async () => {
3429
- await copyFileOrDir(sourcePath, destPath, { overwrite: true });
3430
- const newChecksum = await getFileChecksum(destPath);
3431
- const files = await getAllTrackedFiles(tuckDir);
3432
- const fileId = Object.entries(files).find(([, f]) => f.source === change.source)?.[0];
3433
- if (fileId) {
3434
- await updateFileInManifest(tuckDir, fileId, {
3435
- checksum: newChecksum,
3436
- modified: (/* @__PURE__ */ new Date()).toISOString()
3437
- });
3438
- }
3439
- });
3440
- result.modified.push(change.path.split("/").pop() || change.path);
3441
- } else if (change.status === "deleted") {
3442
- logger.warning(`Source file deleted: ${change.path}`);
3443
- result.deleted.push(change.path.split("/").pop() || change.path);
3444
- }
3445
- }
3446
- if (!options.noCommit && (result.modified.length > 0 || result.deleted.length > 0)) {
3447
- await withSpinner("Staging changes...", async () => {
3448
- await stageAll(tuckDir);
3449
- });
3450
- const message = options.message || generateCommitMessage(result);
3451
- await withSpinner("Committing...", async () => {
3452
- result.commitHash = await commit(tuckDir, message);
3453
- });
3454
- }
3455
- await runPostSyncHook(tuckDir, hookOptions);
3456
- return result;
3457
- };
3458
- var runInteractiveSync = async (tuckDir, options = {}) => {
3459
- prompts.intro("tuck sync");
3460
- const spinner2 = prompts.spinner();
3461
- spinner2.start("Detecting changes...");
3462
- const changes = await detectChanges(tuckDir);
3463
- spinner2.stop("Changes detected");
3464
- if (changes.length === 0) {
3465
- const gitStatus = await getStatus(tuckDir);
3466
- if (gitStatus.hasChanges) {
3467
- prompts.log.info("No dotfile changes, but repository has uncommitted changes");
3468
- const commitAnyway = await prompts.confirm("Commit repository changes?");
3469
- if (commitAnyway) {
3470
- const message2 = await prompts.text("Commit message:", {
3471
- defaultValue: "Update dotfiles"
3472
- });
3473
- await stageAll(tuckDir);
3474
- const hash = await commit(tuckDir, message2);
3475
- prompts.log.success(`Committed: ${hash.slice(0, 7)}`);
3476
- }
3477
- } else {
3478
- prompts.log.success("Everything is up to date");
3479
- }
3480
- return;
3481
- }
3482
- console.log();
3483
- console.log(chalk9.bold("Changes detected:"));
3484
- for (const change of changes) {
3485
- if (change.status === "modified") {
3486
- console.log(chalk9.yellow(` ~ ${change.path}`));
3487
- } else if (change.status === "deleted") {
3488
- console.log(chalk9.red(` - ${change.path}`));
3489
- }
3490
- }
3491
- console.log();
3492
- const confirm2 = await prompts.confirm("Sync these changes?", true);
3493
- if (!confirm2) {
3494
- prompts.cancel("Operation cancelled");
3495
- return;
3496
- }
3497
- const autoMessage = generateCommitMessage({
3498
- modified: changes.filter((c) => c.status === "modified").map((c) => c.path),
3499
- added: [],
3500
- deleted: changes.filter((c) => c.status === "deleted").map((c) => c.path)
3501
- });
3502
- const message = await prompts.text("Commit message:", {
3503
- defaultValue: autoMessage
3504
- });
3505
- const result = await syncFiles(tuckDir, changes, { message });
3506
- console.log();
3507
- if (result.commitHash) {
3508
- prompts.log.success(`Committed: ${result.commitHash.slice(0, 7)}`);
3509
- if (options.push !== false && await hasRemote(tuckDir)) {
3510
- const spinner22 = prompts.spinner();
3511
- spinner22.start("Pushing to remote...");
3512
- try {
3513
- await push(tuckDir);
3514
- spinner22.stop("Pushed to remote");
3515
- } catch {
3516
- spinner22.stop("Push failed (will retry on next sync)");
3517
- }
3518
- } else if (options.push === false) {
3519
- prompts.log.info("Run 'tuck push' when ready to upload");
3520
- }
3521
- }
3522
- prompts.outro("Synced successfully!");
3523
- };
3524
- var runSync = async (messageArg, options) => {
3525
- const tuckDir = getTuckDir();
3526
- try {
3527
- await loadManifest(tuckDir);
3528
- } catch {
3529
- throw new NotInitializedError();
3530
- }
3531
- if (!messageArg && !options.message && !options.all && !options.noCommit && !options.amend) {
3532
- await runInteractiveSync(tuckDir, options);
3533
- return;
3534
- }
3535
- const changes = await detectChanges(tuckDir);
3536
- if (changes.length === 0) {
3537
- logger.info("No changes detected");
3538
- return;
3539
- }
3540
- logger.heading("Changes detected:");
3541
- for (const change of changes) {
3542
- logger.file(change.status === "modified" ? "modify" : "delete", change.path);
3543
- }
3544
- logger.blank();
3545
- const message = messageArg || options.message;
3546
- const result = await syncFiles(tuckDir, changes, { ...options, message });
3547
- logger.blank();
3548
- logger.success(`Synced ${changes.length} file${changes.length > 1 ? "s" : ""}`);
3549
- if (result.commitHash) {
3550
- logger.info(`Commit: ${result.commitHash.slice(0, 7)}`);
3551
- if (options.push !== false && await hasRemote(tuckDir)) {
3552
- await withSpinner("Pushing to remote...", async () => {
3553
- await push(tuckDir);
3554
- });
3555
- logger.success("Pushed to remote");
3556
- } else if (options.push === false) {
3557
- logger.info("Run 'tuck push' when ready to upload");
3558
- }
3559
- }
3560
- };
3561
- 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) => {
3562
- await runSync(messageArg, options);
3563
- });
4050
+ // src/commands/index.ts
4051
+ init_sync();
3564
4052
 
3565
4053
  // src/commands/push.ts
3566
4054
  init_ui();
@@ -3568,8 +4056,8 @@ init_paths();
3568
4056
  init_manifest();
3569
4057
  init_git();
3570
4058
  init_errors();
3571
- import { Command as Command5 } from "commander";
3572
- import chalk10 from "chalk";
4059
+ import { Command as Command6 } from "commander";
4060
+ import chalk12 from "chalk";
3573
4061
  var runInteractivePush = async (tuckDir) => {
3574
4062
  prompts.intro("tuck push");
3575
4063
  const hasRemoteRepo = await hasRemote(tuckDir);
@@ -3598,13 +4086,13 @@ var runInteractivePush = async (tuckDir) => {
3598
4086
  return;
3599
4087
  }
3600
4088
  console.log();
3601
- console.log(chalk10.dim("Remote:"), remoteUrl);
3602
- console.log(chalk10.dim("Branch:"), branch);
4089
+ console.log(chalk12.dim("Remote:"), remoteUrl);
4090
+ console.log(chalk12.dim("Branch:"), branch);
3603
4091
  if (status.ahead > 0) {
3604
- 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`));
3605
4093
  }
3606
4094
  if (status.behind > 0) {
3607
- 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`));
3608
4096
  const pullFirst = await prompts.confirm("Pull changes first?", true);
3609
4097
  if (pullFirst) {
3610
4098
  prompts.log.info("Run 'tuck pull' first, then push");
@@ -3618,20 +4106,39 @@ var runInteractivePush = async (tuckDir) => {
3618
4106
  return;
3619
4107
  }
3620
4108
  const needsUpstream = !status.tracking;
3621
- await withSpinner("Pushing...", async () => {
3622
- await push(tuckDir, {
3623
- setUpstream: needsUpstream,
3624
- branch: needsUpstream ? branch : void 0
4109
+ try {
4110
+ await withSpinner("Pushing...", async () => {
4111
+ await push(tuckDir, {
4112
+ setUpstream: needsUpstream,
4113
+ branch: needsUpstream ? branch : void 0
4114
+ });
3625
4115
  });
3626
- });
3627
- prompts.log.success("Pushed successfully!");
4116
+ prompts.log.success("Pushed successfully!");
4117
+ } catch (error) {
4118
+ const errorMsg = error instanceof Error ? error.message : String(error);
4119
+ if (errorMsg.includes("Permission denied") || errorMsg.includes("publickey")) {
4120
+ prompts.log.error("Authentication failed");
4121
+ prompts.log.info("Check your SSH keys with: ssh -T git@github.com");
4122
+ prompts.log.info("Or try switching to HTTPS: git remote set-url origin https://...");
4123
+ } else if (errorMsg.includes("Could not resolve host") || errorMsg.includes("Network")) {
4124
+ prompts.log.error("Network error - could not reach remote");
4125
+ prompts.log.info("Check your internet connection and try again");
4126
+ } else if (errorMsg.includes("rejected") || errorMsg.includes("non-fast-forward")) {
4127
+ prompts.log.error("Push rejected - remote has changes");
4128
+ prompts.log.info("Run 'tuck pull' first, then push again");
4129
+ prompts.log.info("Or use 'tuck push --force' to overwrite (use with caution)");
4130
+ } else {
4131
+ prompts.log.error(`Push failed: ${errorMsg}`);
4132
+ }
4133
+ return;
4134
+ }
3628
4135
  if (remoteUrl) {
3629
4136
  let viewUrl = remoteUrl;
3630
4137
  if (remoteUrl.startsWith("git@github.com:")) {
3631
4138
  viewUrl = remoteUrl.replace("git@github.com:", "https://github.com/").replace(".git", "");
3632
4139
  }
3633
4140
  console.log();
3634
- console.log(chalk10.dim("View at:"), chalk10.cyan(viewUrl));
4141
+ console.log(chalk12.dim("View at:"), chalk12.cyan(viewUrl));
3635
4142
  }
3636
4143
  prompts.outro("");
3637
4144
  };
@@ -3651,16 +4158,29 @@ var runPush = async (options) => {
3651
4158
  throw new GitError("No remote configured", "Run 'tuck init -r <url>' or add a remote manually");
3652
4159
  }
3653
4160
  const branch = await getCurrentBranch(tuckDir);
3654
- await withSpinner("Pushing...", async () => {
3655
- await push(tuckDir, {
3656
- force: options.force,
3657
- setUpstream: Boolean(options.setUpstream),
3658
- branch: options.setUpstream || branch
4161
+ try {
4162
+ await withSpinner("Pushing...", async () => {
4163
+ await push(tuckDir, {
4164
+ force: options.force,
4165
+ setUpstream: Boolean(options.setUpstream),
4166
+ branch: options.setUpstream || branch
4167
+ });
3659
4168
  });
3660
- });
3661
- logger.success("Pushed successfully!");
4169
+ logger.success("Pushed successfully!");
4170
+ } catch (error) {
4171
+ const errorMsg = error instanceof Error ? error.message : String(error);
4172
+ if (errorMsg.includes("Permission denied") || errorMsg.includes("publickey")) {
4173
+ throw new GitError("Authentication failed", "Check your SSH keys: ssh -T git@github.com");
4174
+ } else if (errorMsg.includes("Could not resolve host") || errorMsg.includes("Network")) {
4175
+ throw new GitError("Network error", "Check your internet connection");
4176
+ } else if (errorMsg.includes("rejected") || errorMsg.includes("non-fast-forward")) {
4177
+ throw new GitError("Push rejected", "Run 'tuck pull' first, or use --force");
4178
+ } else {
4179
+ throw new GitError("Push failed", errorMsg);
4180
+ }
4181
+ }
3662
4182
  };
3663
- 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) => {
3664
4184
  await runPush(options);
3665
4185
  });
3666
4186
 
@@ -3670,8 +4190,8 @@ init_paths();
3670
4190
  init_manifest();
3671
4191
  init_git();
3672
4192
  init_errors();
3673
- import { Command as Command6 } from "commander";
3674
- import chalk11 from "chalk";
4193
+ import { Command as Command7 } from "commander";
4194
+ import chalk13 from "chalk";
3675
4195
  var runInteractivePull = async (tuckDir) => {
3676
4196
  prompts.intro("tuck pull");
3677
4197
  const hasRemoteRepo = await hasRemote(tuckDir);
@@ -3687,23 +4207,23 @@ var runInteractivePull = async (tuckDir) => {
3687
4207
  const branch = await getCurrentBranch(tuckDir);
3688
4208
  const remoteUrl = await getRemoteUrl(tuckDir);
3689
4209
  console.log();
3690
- console.log(chalk11.dim("Remote:"), remoteUrl);
3691
- console.log(chalk11.dim("Branch:"), branch);
4210
+ console.log(chalk13.dim("Remote:"), remoteUrl);
4211
+ console.log(chalk13.dim("Branch:"), branch);
3692
4212
  if (status.behind === 0) {
3693
4213
  prompts.log.success("Already up to date");
3694
4214
  return;
3695
4215
  }
3696
- 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`));
3697
4217
  if (status.ahead > 0) {
3698
4218
  console.log(
3699
- chalk11.dim("Note:"),
3700
- 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`)
3701
4221
  );
3702
4222
  }
3703
4223
  if (status.modified.length > 0 || status.staged.length > 0) {
3704
4224
  console.log();
3705
4225
  prompts.log.warning("You have uncommitted changes");
3706
- console.log(chalk11.dim("Modified:"), status.modified.join(", "));
4226
+ console.log(chalk13.dim("Modified:"), status.modified.join(", "));
3707
4227
  const continueAnyway = await prompts.confirm("Pull anyway? (may cause merge conflicts)");
3708
4228
  if (!continueAnyway) {
3709
4229
  prompts.cancel("Commit or stash your changes first with 'tuck sync'");
@@ -3748,249 +4268,12 @@ var runPull = async (options) => {
3748
4268
  logger.info("Run 'tuck restore --all' to restore dotfiles");
3749
4269
  }
3750
4270
  };
3751
- 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) => {
3752
4272
  await runPull(options);
3753
4273
  });
3754
4274
 
3755
- // src/commands/restore.ts
3756
- init_ui();
3757
- init_paths();
3758
- init_manifest();
3759
- init_config();
3760
- init_files();
3761
- import { Command as Command7 } from "commander";
3762
- import chalk12 from "chalk";
3763
- import { join as join11 } from "path";
3764
- import { chmod, stat as stat5 } from "fs/promises";
3765
-
3766
- // src/lib/backup.ts
3767
- import { join as join10 } from "path";
3768
- init_constants();
3769
- init_paths();
3770
- import { copy as copy4, ensureDir as ensureDir4, pathExists as pathExists3 } from "fs-extra";
3771
- var getBackupDir = () => {
3772
- return expandPath(BACKUP_DIR);
3773
- };
3774
- var formatDateForBackup = (date) => {
3775
- return date.toISOString().slice(0, 10);
3776
- };
3777
- var getTimestampedBackupDir = (date) => {
3778
- const backupRoot = getBackupDir();
3779
- const timestamp = formatDateForBackup(date);
3780
- return join10(backupRoot, timestamp);
3781
- };
3782
- var createBackup = async (sourcePath, customBackupDir) => {
3783
- const expandedSource = expandPath(sourcePath);
3784
- const date = /* @__PURE__ */ new Date();
3785
- if (!await pathExists(expandedSource)) {
3786
- throw new Error(`Source path does not exist: ${sourcePath}`);
3787
- }
3788
- const backupRoot = customBackupDir ? expandPath(customBackupDir) : getTimestampedBackupDir(date);
3789
- await ensureDir4(backupRoot);
3790
- const collapsed = collapsePath(expandedSource);
3791
- const backupName = collapsed.replace(/^~\//, "").replace(/\//g, "_").replace(/^\./, "dot-");
3792
- const timestamp = date.toISOString().replace(/[:.]/g, "-").slice(11, 19);
3793
- const backupPath = join10(backupRoot, `${backupName}_${timestamp}`);
3794
- await copy4(expandedSource, backupPath, { overwrite: true });
3795
- return {
3796
- originalPath: expandedSource,
3797
- backupPath,
3798
- date
3799
- };
3800
- };
3801
-
3802
- // src/commands/restore.ts
3803
- init_errors();
3804
- init_constants();
3805
- var fixSSHPermissions = async (path) => {
3806
- const expandedPath = expandPath(path);
3807
- if (!path.includes(".ssh/") && !path.endsWith(".ssh")) {
3808
- return;
3809
- }
3810
- try {
3811
- const stats = await stat5(expandedPath);
3812
- if (stats.isDirectory()) {
3813
- await chmod(expandedPath, 448);
3814
- } else {
3815
- await chmod(expandedPath, 384);
3816
- }
3817
- } catch {
3818
- }
3819
- };
3820
- var fixGPGPermissions = async (path) => {
3821
- const expandedPath = expandPath(path);
3822
- if (!path.includes(".gnupg/") && !path.endsWith(".gnupg")) {
3823
- return;
3824
- }
3825
- try {
3826
- const stats = await stat5(expandedPath);
3827
- if (stats.isDirectory()) {
3828
- await chmod(expandedPath, 448);
3829
- } else {
3830
- await chmod(expandedPath, 384);
3831
- }
3832
- } catch {
3833
- }
3834
- };
3835
- var prepareFilesToRestore = async (tuckDir, paths) => {
3836
- const allFiles = await getAllTrackedFiles(tuckDir);
3837
- const filesToRestore = [];
3838
- if (paths && paths.length > 0) {
3839
- for (const path of paths) {
3840
- const expandedPath = expandPath(path);
3841
- const collapsedPath = collapsePath(expandedPath);
3842
- const tracked = await getTrackedFileBySource(tuckDir, collapsedPath);
3843
- if (!tracked) {
3844
- throw new FileNotFoundError(`Not tracked: ${path}`);
3845
- }
3846
- filesToRestore.push({
3847
- id: tracked.id,
3848
- source: tracked.file.source,
3849
- destination: join11(tuckDir, tracked.file.destination),
3850
- category: tracked.file.category,
3851
- existsAtTarget: await pathExists(expandedPath)
3852
- });
3853
- }
3854
- } else {
3855
- for (const [id, file] of Object.entries(allFiles)) {
3856
- const targetPath = expandPath(file.source);
3857
- filesToRestore.push({
3858
- id,
3859
- source: file.source,
3860
- destination: join11(tuckDir, file.destination),
3861
- category: file.category,
3862
- existsAtTarget: await pathExists(targetPath)
3863
- });
3864
- }
3865
- }
3866
- return filesToRestore;
3867
- };
3868
- var restoreFiles = async (tuckDir, files, options) => {
3869
- const config = await loadConfig(tuckDir);
3870
- const useSymlink = options.symlink || config.files.strategy === "symlink";
3871
- const shouldBackup = options.backup ?? config.files.backupOnRestore;
3872
- const hookOptions = {
3873
- skipHooks: options.noHooks,
3874
- trustHooks: options.trustHooks
3875
- };
3876
- await runPreRestoreHook(tuckDir, hookOptions);
3877
- let restoredCount = 0;
3878
- for (const file of files) {
3879
- const targetPath = expandPath(file.source);
3880
- if (!await pathExists(file.destination)) {
3881
- logger.warning(`Source not found in repository: ${file.source}`);
3882
- continue;
3883
- }
3884
- if (options.dryRun) {
3885
- if (file.existsAtTarget) {
3886
- logger.file("modify", `${file.source} (would overwrite)`);
3887
- } else {
3888
- logger.file("add", `${file.source} (would create)`);
3889
- }
3890
- continue;
3891
- }
3892
- if (shouldBackup && file.existsAtTarget) {
3893
- await withSpinner(`Backing up ${file.source}...`, async () => {
3894
- await createBackup(targetPath);
3895
- });
3896
- }
3897
- await withSpinner(`Restoring ${file.source}...`, async () => {
3898
- if (useSymlink) {
3899
- await createSymlink(file.destination, targetPath, { overwrite: true });
3900
- } else {
3901
- await copyFileOrDir(file.destination, targetPath, { overwrite: true });
3902
- }
3903
- await fixSSHPermissions(file.source);
3904
- await fixGPGPermissions(file.source);
3905
- });
3906
- restoredCount++;
3907
- }
3908
- await runPostRestoreHook(tuckDir, hookOptions);
3909
- return restoredCount;
3910
- };
3911
- var runInteractiveRestore = async (tuckDir) => {
3912
- prompts.intro("tuck restore");
3913
- const files = await prepareFilesToRestore(tuckDir);
3914
- if (files.length === 0) {
3915
- prompts.log.warning("No files to restore");
3916
- prompts.note("Run 'tuck add <path>' to track files first", "Tip");
3917
- return;
3918
- }
3919
- const fileOptions = files.map((file) => {
3920
- const categoryConfig = CATEGORIES[file.category] || { icon: "\u{1F4C4}" };
3921
- const status = file.existsAtTarget ? chalk12.yellow("(exists, will backup)") : "";
3922
- return {
3923
- value: file.id,
3924
- label: `${categoryConfig.icon} ${file.source} ${status}`,
3925
- hint: file.category
3926
- };
3927
- });
3928
- const selectedIds = await prompts.multiselect("Select files to restore:", fileOptions, true);
3929
- if (selectedIds.length === 0) {
3930
- prompts.cancel("No files selected");
3931
- return;
3932
- }
3933
- const selectedFiles = files.filter((f) => selectedIds.includes(f.id));
3934
- const existingFiles = selectedFiles.filter((f) => f.existsAtTarget);
3935
- if (existingFiles.length > 0) {
3936
- console.log();
3937
- prompts.log.warning(
3938
- `${existingFiles.length} file${existingFiles.length > 1 ? "s" : ""} will be backed up:`
3939
- );
3940
- existingFiles.forEach((f) => console.log(chalk12.dim(` ${f.source}`)));
3941
- console.log();
3942
- }
3943
- const useSymlink = await prompts.select("Restore method:", [
3944
- { value: false, label: "Copy files", hint: "Recommended" },
3945
- { value: true, label: "Create symlinks", hint: "Files stay in tuck repo" }
3946
- ]);
3947
- const confirm2 = await prompts.confirm(
3948
- `Restore ${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}?`,
3949
- true
3950
- );
3951
- if (!confirm2) {
3952
- prompts.cancel("Operation cancelled");
3953
- return;
3954
- }
3955
- const restoredCount = await restoreFiles(tuckDir, selectedFiles, {
3956
- symlink: useSymlink,
3957
- backup: true
3958
- });
3959
- console.log();
3960
- prompts.outro(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
3961
- };
3962
- var runRestore = async (paths, options) => {
3963
- const tuckDir = getTuckDir();
3964
- try {
3965
- await loadManifest(tuckDir);
3966
- } catch {
3967
- throw new NotInitializedError();
3968
- }
3969
- if (paths.length === 0 && !options.all) {
3970
- await runInteractiveRestore(tuckDir);
3971
- return;
3972
- }
3973
- const files = await prepareFilesToRestore(tuckDir, options.all ? void 0 : paths);
3974
- if (files.length === 0) {
3975
- logger.warning("No files to restore");
3976
- return;
3977
- }
3978
- if (options.dryRun) {
3979
- logger.heading("Dry run - would restore:");
3980
- } else {
3981
- logger.heading("Restoring:");
3982
- }
3983
- const restoredCount = await restoreFiles(tuckDir, files, options);
3984
- logger.blank();
3985
- if (options.dryRun) {
3986
- logger.info(`Would restore ${files.length} file${files.length > 1 ? "s" : ""}`);
3987
- } else {
3988
- logger.success(`Restored ${restoredCount} file${restoredCount > 1 ? "s" : ""}`);
3989
- }
3990
- };
3991
- 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) => {
3992
- await runRestore(paths, options);
3993
- });
4275
+ // src/commands/index.ts
4276
+ init_restore();
3994
4277
 
3995
4278
  // src/commands/status.ts
3996
4279
  init_ui();
@@ -3999,8 +4282,10 @@ init_manifest();
3999
4282
  init_git();
4000
4283
  init_files();
4001
4284
  init_errors();
4285
+ init_constants();
4002
4286
  import { Command as Command8 } from "commander";
4003
- import chalk13 from "chalk";
4287
+ import chalk14 from "chalk";
4288
+ import boxen2 from "boxen";
4004
4289
  var detectFileChanges = async (tuckDir) => {
4005
4290
  const files = await getAllTrackedFiles(tuckDir);
4006
4291
  const changes = [];
@@ -4055,6 +4340,10 @@ var getFullStatus = async (tuckDir) => {
4055
4340
  }
4056
4341
  }
4057
4342
  const fileChanges = await detectFileChanges(tuckDir);
4343
+ const categoryCounts = {};
4344
+ for (const file of Object.values(manifest.files)) {
4345
+ categoryCounts[file.category] = (categoryCounts[file.category] || 0) + 1;
4346
+ }
4058
4347
  return {
4059
4348
  tuckDir,
4060
4349
  branch,
@@ -4063,6 +4352,7 @@ var getFullStatus = async (tuckDir) => {
4063
4352
  ahead: gitStatus.ahead,
4064
4353
  behind: gitStatus.behind,
4065
4354
  trackedCount: Object.keys(manifest.files).length,
4355
+ categoryCounts,
4066
4356
  changes: fileChanges,
4067
4357
  gitChanges: {
4068
4358
  staged: gitStatus.staged,
@@ -4072,56 +4362,81 @@ var getFullStatus = async (tuckDir) => {
4072
4362
  };
4073
4363
  };
4074
4364
  var printStatus = (status) => {
4075
- prompts.intro("tuck status");
4076
- console.log();
4077
- console.log(chalk13.dim("Repository:"), collapsePath(status.tuckDir));
4078
- console.log(chalk13.dim("Branch:"), chalk13.cyan(status.branch));
4365
+ const headerLines = [
4366
+ `${chalk14.bold.cyan("tuck")} ${chalk14.dim(`v${VERSION}`)}`,
4367
+ "",
4368
+ `${chalk14.dim("Repository:")} ${collapsePath(status.tuckDir)}`,
4369
+ `${chalk14.dim("Branch:")} ${chalk14.cyan(status.branch)}`
4370
+ ];
4371
+ if (status.remote) {
4372
+ const shortRemote = status.remote.length > 40 ? status.remote.replace(/^https?:\/\//, "").replace(/\.git$/, "") : status.remote;
4373
+ headerLines.push(`${chalk14.dim("Remote:")} ${shortRemote}`);
4374
+ } else {
4375
+ headerLines.push(`${chalk14.dim("Remote:")} ${chalk14.yellow("not configured")}`);
4376
+ }
4377
+ console.log(boxen2(headerLines.join("\n"), {
4378
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
4379
+ borderColor: "cyan",
4380
+ borderStyle: "round"
4381
+ }));
4079
4382
  if (status.remote) {
4080
- console.log(chalk13.dim("Remote:"), status.remote);
4081
4383
  let remoteInfo = "";
4082
4384
  switch (status.remoteStatus) {
4083
4385
  case "up-to-date":
4084
- remoteInfo = chalk13.green("up to date");
4386
+ remoteInfo = chalk14.green("\u2713 Up to date with remote");
4085
4387
  break;
4086
4388
  case "ahead":
4087
- remoteInfo = chalk13.yellow(`${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead`);
4389
+ remoteInfo = chalk14.yellow(`\u2191 ${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead of remote`);
4088
4390
  break;
4089
4391
  case "behind":
4090
- remoteInfo = chalk13.yellow(`${status.behind} commit${status.behind > 1 ? "s" : ""} behind`);
4392
+ remoteInfo = chalk14.yellow(`\u2193 ${status.behind} commit${status.behind > 1 ? "s" : ""} behind remote`);
4091
4393
  break;
4092
4394
  case "diverged":
4093
- remoteInfo = chalk13.red(`diverged (${status.ahead} ahead, ${status.behind} behind)`);
4395
+ remoteInfo = chalk14.red(`\u26A0 Diverged (${status.ahead} ahead, ${status.behind} behind)`);
4094
4396
  break;
4095
4397
  }
4096
- console.log(chalk13.dim("Status:"), remoteInfo);
4097
- } else {
4098
- console.log(chalk13.dim("Remote:"), chalk13.yellow("not configured"));
4398
+ console.log("\n" + remoteInfo);
4099
4399
  }
4100
4400
  console.log();
4101
- console.log(chalk13.dim("Tracked files:"), status.trackedCount);
4401
+ console.log(chalk14.bold(`Tracked Files: ${status.trackedCount}`));
4402
+ const categoryOrder = ["shell", "git", "editors", "terminal", "ssh", "misc"];
4403
+ const sortedCategories = Object.keys(status.categoryCounts).sort((a, b) => {
4404
+ const aIdx = categoryOrder.indexOf(a);
4405
+ const bIdx = categoryOrder.indexOf(b);
4406
+ if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
4407
+ if (aIdx === -1) return 1;
4408
+ if (bIdx === -1) return -1;
4409
+ return aIdx - bIdx;
4410
+ });
4411
+ if (sortedCategories.length > 0) {
4412
+ for (const category of sortedCategories) {
4413
+ const count = status.categoryCounts[category];
4414
+ console.log(chalk14.dim(` ${category}: ${count} file${count > 1 ? "s" : ""}`));
4415
+ }
4416
+ }
4102
4417
  if (status.changes.length > 0) {
4103
4418
  console.log();
4104
- console.log(chalk13.bold("Changes detected:"));
4419
+ console.log(chalk14.bold("Changes detected:"));
4105
4420
  for (const change of status.changes) {
4106
4421
  const statusText = formatStatus(change.status);
4107
- console.log(` ${statusText}: ${chalk13.cyan(change.path)}`);
4422
+ console.log(` ${statusText}: ${chalk14.cyan(change.path)}`);
4108
4423
  }
4109
4424
  }
4110
4425
  const hasGitChanges = status.gitChanges.staged.length > 0 || status.gitChanges.modified.length > 0 || status.gitChanges.untracked.length > 0;
4111
4426
  if (hasGitChanges) {
4112
4427
  console.log();
4113
- console.log(chalk13.bold("Repository changes:"));
4428
+ console.log(chalk14.bold("Repository changes:"));
4114
4429
  if (status.gitChanges.staged.length > 0) {
4115
- console.log(chalk13.green(" Staged:"));
4116
- 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}`)));
4117
4432
  }
4118
4433
  if (status.gitChanges.modified.length > 0) {
4119
- console.log(chalk13.yellow(" Modified:"));
4120
- 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}`)));
4121
4436
  }
4122
4437
  if (status.gitChanges.untracked.length > 0) {
4123
- console.log(chalk13.dim(" Untracked:"));
4124
- 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}`)));
4125
4440
  }
4126
4441
  }
4127
4442
  console.log();
@@ -4186,7 +4501,7 @@ init_manifest();
4186
4501
  init_errors();
4187
4502
  init_constants();
4188
4503
  import { Command as Command9 } from "commander";
4189
- import chalk14 from "chalk";
4504
+ import chalk15 from "chalk";
4190
4505
  var groupByCategory = async (tuckDir) => {
4191
4506
  const files = await getAllTrackedFiles(tuckDir);
4192
4507
  const groups = /* @__PURE__ */ new Map();
@@ -4225,15 +4540,15 @@ var printList = (groups) => {
4225
4540
  totalFiles += fileCount;
4226
4541
  console.log();
4227
4542
  console.log(
4228
- chalk14.bold(`${group2.icon} ${group2.name}`) + chalk14.dim(` (${formatCount(fileCount, "file")})`)
4543
+ chalk15.bold(`${group2.icon} ${group2.name}`) + chalk15.dim(` (${formatCount(fileCount, "file")})`)
4229
4544
  );
4230
4545
  group2.files.forEach((file, index) => {
4231
4546
  const isLast = index === group2.files.length - 1;
4232
4547
  const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4233
4548
  const name = file.source.split("/").pop() || file.source;
4234
- const arrow = chalk14.dim(" \u2192 ");
4235
- const dest = chalk14.dim(file.source);
4236
- 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);
4237
4552
  });
4238
4553
  }
4239
4554
  console.log();
@@ -4294,7 +4609,7 @@ init_git();
4294
4609
  init_files();
4295
4610
  init_errors();
4296
4611
  import { Command as Command10 } from "commander";
4297
- import chalk15 from "chalk";
4612
+ import chalk16 from "chalk";
4298
4613
  import { join as join12 } from "path";
4299
4614
  import { readFile as readFile6 } from "fs/promises";
4300
4615
  var getFileDiff = async (tuckDir, source) => {
@@ -4332,19 +4647,19 @@ var getFileDiff = async (tuckDir, source) => {
4332
4647
  };
4333
4648
  var formatUnifiedDiff = (source, systemContent, repoContent) => {
4334
4649
  const lines = [];
4335
- lines.push(chalk15.bold(`--- a/${source} (system)`));
4336
- lines.push(chalk15.bold(`+++ b/${source} (repository)`));
4650
+ lines.push(chalk16.bold(`--- a/${source} (system)`));
4651
+ lines.push(chalk16.bold(`+++ b/${source} (repository)`));
4337
4652
  if (!systemContent && repoContent) {
4338
- lines.push(chalk15.red("File missing on system"));
4339
- lines.push(chalk15.dim("Repository content:"));
4653
+ lines.push(chalk16.red("File missing on system"));
4654
+ lines.push(chalk16.dim("Repository content:"));
4340
4655
  repoContent.split("\n").forEach((line) => {
4341
- lines.push(chalk15.green(`+ ${line}`));
4656
+ lines.push(chalk16.green(`+ ${line}`));
4342
4657
  });
4343
4658
  } else if (systemContent && !repoContent) {
4344
- lines.push(chalk15.yellow("File not yet synced to repository"));
4345
- lines.push(chalk15.dim("System content:"));
4659
+ lines.push(chalk16.yellow("File not yet synced to repository"));
4660
+ lines.push(chalk16.dim("System content:"));
4346
4661
  systemContent.split("\n").forEach((line) => {
4347
- lines.push(chalk15.red(`- ${line}`));
4662
+ lines.push(chalk16.red(`- ${line}`));
4348
4663
  });
4349
4664
  } else if (systemContent && repoContent) {
4350
4665
  const systemLines = systemContent.split("\n");
@@ -4359,16 +4674,16 @@ var formatUnifiedDiff = (source, systemContent, repoContent) => {
4359
4674
  if (!inDiff) {
4360
4675
  inDiff = true;
4361
4676
  diffStart = i;
4362
- lines.push(chalk15.cyan(`@@ -${i + 1} +${i + 1} @@`));
4677
+ lines.push(chalk16.cyan(`@@ -${i + 1} +${i + 1} @@`));
4363
4678
  }
4364
4679
  if (sysLine !== void 0) {
4365
- lines.push(chalk15.red(`- ${sysLine}`));
4680
+ lines.push(chalk16.red(`- ${sysLine}`));
4366
4681
  }
4367
4682
  if (repoLine !== void 0) {
4368
- lines.push(chalk15.green(`+ ${repoLine}`));
4683
+ lines.push(chalk16.green(`+ ${repoLine}`));
4369
4684
  }
4370
4685
  } else if (inDiff) {
4371
- lines.push(chalk15.dim(` ${sysLine || ""}`));
4686
+ lines.push(chalk16.dim(` ${sysLine || ""}`));
4372
4687
  if (i - diffStart > 3) {
4373
4688
  inDiff = false;
4374
4689
  }
@@ -4409,10 +4724,10 @@ var runDiff = async (paths, options) => {
4409
4724
  if (options.stat) {
4410
4725
  prompts.intro("tuck diff");
4411
4726
  console.log();
4412
- 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:`));
4413
4728
  console.log();
4414
4729
  for (const diff of changedFiles) {
4415
- console.log(chalk15.yellow(` ~ ${diff.source}`));
4730
+ console.log(chalk16.yellow(` ~ ${diff.source}`));
4416
4731
  }
4417
4732
  console.log();
4418
4733
  return;
@@ -4433,7 +4748,7 @@ var runDiff = async (paths, options) => {
4433
4748
  continue;
4434
4749
  }
4435
4750
  if (options.stat) {
4436
- console.log(chalk15.yellow(`~ ${path}`));
4751
+ console.log(chalk16.yellow(`~ ${path}`));
4437
4752
  } else {
4438
4753
  console.log(formatUnifiedDiff(path, diff.systemContent, diff.repoContent));
4439
4754
  console.log();
@@ -4451,7 +4766,7 @@ init_config();
4451
4766
  init_manifest();
4452
4767
  init_errors();
4453
4768
  import { Command as Command11 } from "commander";
4454
- import chalk16 from "chalk";
4769
+ import chalk17 from "chalk";
4455
4770
  import { spawn } from "child_process";
4456
4771
  var printConfig = (config) => {
4457
4772
  console.log(JSON.stringify(config, null, 2));
@@ -4517,7 +4832,7 @@ var runConfigList = async () => {
4517
4832
  const config = await loadConfig(tuckDir);
4518
4833
  prompts.intro("tuck config");
4519
4834
  console.log();
4520
- console.log(chalk16.dim("Configuration file:"), collapsePath(getConfigPath(tuckDir)));
4835
+ console.log(chalk17.dim("Configuration file:"), collapsePath(getConfigPath(tuckDir)));
4521
4836
  console.log();
4522
4837
  printConfig(config);
4523
4838
  };
@@ -4613,9 +4928,9 @@ init_github();
4613
4928
  import { Command as Command12 } from "commander";
4614
4929
  import { join as join13 } from "path";
4615
4930
  import { readFile as readFile8, rm as rm4, chmod as chmod2, stat as stat6 } from "fs/promises";
4616
- import { ensureDir as ensureDir5, pathExists as fsPathExists } from "fs-extra";
4931
+ import { ensureDir as ensureDir6, pathExists as fsPathExists } from "fs-extra";
4617
4932
  import { tmpdir as tmpdir2 } from "os";
4618
- import chalk17 from "chalk";
4933
+ import chalk18 from "chalk";
4619
4934
 
4620
4935
  // src/lib/merge.ts
4621
4936
  init_paths();
@@ -4881,7 +5196,7 @@ var resolveSource = async (source) => {
4881
5196
  };
4882
5197
  var cloneSource = async (repoId, isUrl) => {
4883
5198
  const tempDir = join13(tmpdir2(), `tuck-apply-${Date.now()}`);
4884
- await ensureDir5(tempDir);
5199
+ await ensureDir6(tempDir);
4885
5200
  if (isUrl) {
4886
5201
  await cloneRepo(repoId, tempDir);
4887
5202
  } else {
@@ -4937,9 +5252,9 @@ var applyWithMerge = async (files, dryRun) => {
4937
5252
  logger.file("merge", `${collapsePath(file.destination)} (${mergeResult.preservedBlocks} blocks preserved)`);
4938
5253
  } else {
4939
5254
  const { writeFile: writeFile5 } = await import("fs/promises");
4940
- const { ensureDir: ensureDir6 } = await import("fs-extra");
4941
- const { dirname: dirname4 } = await import("path");
4942
- await ensureDir6(dirname4(file.destination));
5255
+ const { ensureDir: ensureDir7 } = await import("fs-extra");
5256
+ const { dirname: dirname6 } = await import("path");
5257
+ await ensureDir7(dirname6(file.destination));
4943
5258
  await writeFile5(file.destination, mergeResult.content, "utf-8");
4944
5259
  logger.file("merge", collapsePath(file.destination));
4945
5260
  }
@@ -5035,11 +5350,11 @@ var runInteractiveApply = async (source, options) => {
5035
5350
  }
5036
5351
  for (const [category, categoryFiles] of Object.entries(byCategory)) {
5037
5352
  const categoryConfig = CATEGORIES[category] || { icon: "\u{1F4C4}" };
5038
- console.log(chalk17.bold(` ${categoryConfig.icon} ${category}`));
5353
+ console.log(chalk18.bold(` ${categoryConfig.icon} ${category}`));
5039
5354
  for (const file of categoryFiles) {
5040
5355
  const exists = await pathExists(file.destination);
5041
- const status = exists ? chalk17.yellow("(will update)") : chalk17.green("(new)");
5042
- 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}`));
5043
5358
  }
5044
5359
  }
5045
5360
  console.log();
@@ -5199,7 +5514,7 @@ var applyCommand = new Command12("apply").description("Apply dotfiles from a rep
5199
5514
  init_ui();
5200
5515
  init_paths();
5201
5516
  import { Command as Command13 } from "commander";
5202
- import chalk18 from "chalk";
5517
+ import chalk19 from "chalk";
5203
5518
  var showSnapshotList = async () => {
5204
5519
  const snapshots = await listSnapshots();
5205
5520
  if (snapshots.length === 0) {
@@ -5212,11 +5527,11 @@ var showSnapshotList = async () => {
5212
5527
  for (const snapshot of snapshots) {
5213
5528
  const date = formatSnapshotDate(snapshot.id);
5214
5529
  const fileCount = snapshot.files.filter((f) => f.existed).length;
5215
- console.log(chalk18.cyan(` ${snapshot.id}`));
5216
- console.log(chalk18.dim(` Date: ${date}`));
5217
- console.log(chalk18.dim(` Reason: ${snapshot.reason}`));
5218
- console.log(chalk18.dim(` Files: ${fileCount} file(s) backed up`));
5219
- 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}`));
5220
5535
  console.log();
5221
5536
  }
5222
5537
  const totalSize = await getSnapshotsSize();
@@ -5227,18 +5542,18 @@ var showSnapshotList = async () => {
5227
5542
  };
5228
5543
  var showSnapshotDetails = (snapshot) => {
5229
5544
  console.log();
5230
- console.log(chalk18.bold("Snapshot Details:"));
5231
- console.log(chalk18.dim(` ID: ${snapshot.id}`));
5232
- console.log(chalk18.dim(` Date: ${formatSnapshotDate(snapshot.id)}`));
5233
- console.log(chalk18.dim(` Reason: ${snapshot.reason}`));
5234
- 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}`));
5235
5550
  console.log();
5236
- console.log(chalk18.bold("Files in snapshot:"));
5551
+ console.log(chalk19.bold("Files in snapshot:"));
5237
5552
  for (const file of snapshot.files) {
5238
5553
  if (file.existed) {
5239
- console.log(chalk18.dim(` ok ${collapsePath(file.originalPath)}`));
5554
+ console.log(chalk19.dim(` ok ${collapsePath(file.originalPath)}`));
5240
5555
  } else {
5241
- console.log(chalk18.dim(` - ${collapsePath(file.originalPath)} (did not exist)`));
5556
+ console.log(chalk19.dim(` - ${collapsePath(file.originalPath)} (did not exist)`));
5242
5557
  }
5243
5558
  }
5244
5559
  console.log();
@@ -5346,11 +5661,11 @@ var runInteractiveUndo = async () => {
5346
5661
  prompts.log.info("Files in this snapshot:");
5347
5662
  for (const file of snapshot.files.slice(0, 10)) {
5348
5663
  if (file.existed) {
5349
- console.log(chalk18.dim(` ${collapsePath(file.originalPath)}`));
5664
+ console.log(chalk19.dim(` ${collapsePath(file.originalPath)}`));
5350
5665
  }
5351
5666
  }
5352
5667
  if (snapshot.files.length > 10) {
5353
- console.log(chalk18.dim(` ... and ${snapshot.files.length - 10} more`));
5668
+ console.log(chalk19.dim(` ... and ${snapshot.files.length - 10} more`));
5354
5669
  }
5355
5670
  console.log();
5356
5671
  const confirmed = await prompts.confirm("Restore these files?", true);
@@ -5401,7 +5716,7 @@ init_ui();
5401
5716
  init_paths();
5402
5717
  init_manifest();
5403
5718
  import { Command as Command14 } from "commander";
5404
- import chalk19 from "chalk";
5719
+ import chalk20 from "chalk";
5405
5720
  init_errors();
5406
5721
  var groupSelectableByCategory = (files) => {
5407
5722
  const grouped = {};
@@ -5426,18 +5741,18 @@ var displayGroupedFiles = (files, showAll) => {
5426
5741
  const trackedFiles = categoryFiles.filter((f) => f.alreadyTracked);
5427
5742
  console.log();
5428
5743
  console.log(
5429
- 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)`)
5430
5745
  );
5431
- console.log(chalk19.dim("\u2500".repeat(50)));
5746
+ console.log(chalk20.dim("\u2500".repeat(50)));
5432
5747
  for (const file of categoryFiles) {
5433
5748
  if (!showAll && file.alreadyTracked) continue;
5434
- const status = file.selected ? chalk19.green("[x]") : chalk19.dim("[ ]");
5749
+ const status = file.selected ? chalk20.green("[x]") : chalk20.dim("[ ]");
5435
5750
  const name = file.path;
5436
- const tracked = file.alreadyTracked ? chalk19.dim(" (tracked)") : "";
5437
- const sensitive = file.sensitive ? chalk19.yellow(" [!]") : "";
5438
- 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]") : "";
5439
5754
  console.log(` ${status} ${name}${dir}${sensitive}${tracked}`);
5440
- console.log(chalk19.dim(` ${file.description}`));
5755
+ console.log(chalk20.dim(` ${file.description}`));
5441
5756
  }
5442
5757
  }
5443
5758
  };
@@ -5452,16 +5767,16 @@ var runInteractiveSelection = async (files) => {
5452
5767
  for (const [category, categoryFiles] of Object.entries(grouped)) {
5453
5768
  const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
5454
5769
  console.log();
5455
- console.log(chalk19.bold(`${config.icon} ${config.name}`));
5456
- console.log(chalk19.dim(config.description || ""));
5770
+ console.log(chalk20.bold(`${config.icon} ${config.name}`));
5771
+ console.log(chalk20.dim(config.description || ""));
5457
5772
  console.log();
5458
5773
  const options = categoryFiles.map((file) => {
5459
5774
  let label = file.path;
5460
5775
  if (file.sensitive) {
5461
- label += chalk19.yellow(" [!]");
5776
+ label += chalk20.yellow(" [!]");
5462
5777
  }
5463
5778
  if (file.isDirectory) {
5464
- label += chalk19.cyan(" [dir]");
5779
+ label += chalk20.cyan(" [dir]");
5465
5780
  }
5466
5781
  return {
5467
5782
  value: file.path,
@@ -5487,11 +5802,11 @@ var runQuickScan = async (files) => {
5487
5802
  const trackedFiles = files.filter((f) => f.alreadyTracked);
5488
5803
  console.log();
5489
5804
  console.log(
5490
- 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`)
5491
5806
  );
5492
5807
  displayGroupedFiles(files, false);
5493
5808
  console.log();
5494
- console.log(chalk19.dim("\u2500".repeat(60)));
5809
+ console.log(chalk20.dim("\u2500".repeat(60)));
5495
5810
  console.log();
5496
5811
  if (newFiles.length > 0) {
5497
5812
  logger.info(`Found ${newFiles.length} new dotfiles to track`);
@@ -5507,32 +5822,36 @@ var showSummary = (selected) => {
5507
5822
  return;
5508
5823
  }
5509
5824
  console.log();
5510
- 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)));
5511
5827
  console.log();
5512
5828
  const grouped = groupSelectableByCategory(selected);
5513
5829
  for (const [category, files] of Object.entries(grouped)) {
5514
5830
  const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
5515
- console.log(chalk19.bold(`${config.icon} ${config.name}`));
5831
+ console.log(chalk20.bold(`${config.icon} ${config.name}`));
5516
5832
  for (const file of files) {
5517
- const sensitive = file.sensitive ? chalk19.yellow(" [!]") : "";
5518
- 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}`));
5519
5835
  }
5836
+ console.log();
5520
5837
  }
5521
- console.log();
5522
5838
  const sensitiveFiles = selected.filter((f) => f.sensitive);
5523
5839
  if (sensitiveFiles.length > 0) {
5524
- console.log(chalk19.yellow("Warning: Some selected files may contain sensitive data:"));
5525
- for (const file of sensitiveFiles) {
5526
- console.log(chalk19.yellow(` \u2022 ${file.path}`));
5527
- }
5528
- 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!"));
5529
5842
  console.log();
5530
5843
  }
5531
- const paths = selected.map((f) => f.path).join(" ");
5532
- console.log(chalk19.bold("Run this command to add the selected files:"));
5533
- console.log();
5534
- console.log(chalk19.cyan(` tuck add ${paths}`));
5535
- 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;
5536
5855
  };
5537
5856
  var runScan = async (options) => {
5538
5857
  const tuckDir = getTuckDir();
@@ -5566,7 +5885,7 @@ var runScan = async (options) => {
5566
5885
  logger.warning(`No dotfiles found in category: ${options.category}`);
5567
5886
  logger.info("Available categories:");
5568
5887
  for (const [key, config] of Object.entries(DETECTION_CATEGORIES)) {
5569
- console.log(chalk19.dim(` ${config.icon} ${key} - ${config.name}`));
5888
+ console.log(chalk20.dim(` ${config.icon} ${key} - ${config.name}`));
5570
5889
  }
5571
5890
  return;
5572
5891
  }
@@ -5625,17 +5944,27 @@ var runScan = async (options) => {
5625
5944
  }
5626
5945
  showSummary(selected);
5627
5946
  const confirmed = await prompts.confirm(
5628
- `Add ${selected.length} files to tuck?`,
5947
+ `Track these ${selected.length} files?`,
5629
5948
  true
5630
5949
  );
5631
5950
  if (!confirmed) {
5632
5951
  prompts.cancel("Operation cancelled");
5633
5952
  return;
5634
5953
  }
5635
- const { addFilesFromPaths: addFilesFromPaths2 } = await Promise.resolve().then(() => (init_add(), add_exports));
5636
- const paths = selected.map((f) => f.path);
5637
- await addFilesFromPaths2(paths, {});
5638
- 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
+ }
5639
5968
  };
5640
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) => {
5641
5970
  await runScan(options);
@@ -5650,7 +5979,7 @@ init_manifest();
5650
5979
  init_git();
5651
5980
  var program = new Command15();
5652
5981
  program.name("tuck").description(DESCRIPTION).version(VERSION, "-v, --version", "Display version number").configureOutput({
5653
- outputError: (str, write) => write(chalk20.red(str))
5982
+ outputError: (str, write) => write(chalk21.red(str))
5654
5983
  }).addHelpText("beforeAll", customHelp(VERSION)).helpOption("-h, --help", "Display this help message").showHelpAfterError(false);
5655
5984
  program.configureHelp({
5656
5985
  formatHelp: () => ""
@@ -5673,12 +6002,12 @@ var runDefaultAction = async () => {
5673
6002
  const tuckDir = getTuckDir();
5674
6003
  if (!await pathExists(tuckDir)) {
5675
6004
  miniBanner();
5676
- console.log(chalk20.bold("Get started with tuck:\n"));
5677
- console.log(chalk20.cyan(" tuck init") + chalk20.dim(" - Set up tuck and create a GitHub repo"));
5678
- 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"));
5679
6008
  console.log();
5680
- console.log(chalk20.dim("On a new machine:"));
5681
- 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"));
5682
6011
  console.log();
5683
6012
  return;
5684
6013
  }
@@ -5687,38 +6016,38 @@ var runDefaultAction = async () => {
5687
6016
  const trackedCount = Object.keys(manifest.files).length;
5688
6017
  const gitStatus = await getStatus(tuckDir);
5689
6018
  miniBanner();
5690
- console.log(chalk20.bold("Status:\n"));
5691
- console.log(` Tracked files: ${chalk20.cyan(trackedCount.toString())}`);
6019
+ console.log(chalk21.bold("Status:\n"));
6020
+ console.log(` Tracked files: ${chalk21.cyan(trackedCount.toString())}`);
5692
6021
  const pendingChanges = gitStatus.modified.length + gitStatus.staged.length;
5693
6022
  if (pendingChanges > 0) {
5694
- console.log(` Pending changes: ${chalk20.yellow(pendingChanges.toString())}`);
6023
+ console.log(` Pending changes: ${chalk21.yellow(pendingChanges.toString())}`);
5695
6024
  } else {
5696
- console.log(` Pending changes: ${chalk20.dim("none")}`);
6025
+ console.log(` Pending changes: ${chalk21.dim("none")}`);
5697
6026
  }
5698
6027
  if (gitStatus.ahead > 0) {
5699
- console.log(` Commits to push: ${chalk20.yellow(gitStatus.ahead.toString())}`);
6028
+ console.log(` Commits to push: ${chalk21.yellow(gitStatus.ahead.toString())}`);
5700
6029
  }
5701
6030
  console.log();
5702
- console.log(chalk20.bold("Next steps:\n"));
6031
+ console.log(chalk21.bold("Next steps:\n"));
5703
6032
  if (trackedCount === 0) {
5704
- console.log(chalk20.cyan(" tuck scan") + chalk20.dim(" - Find dotfiles to track"));
5705
- 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"));
5706
6035
  } else if (pendingChanges > 0) {
5707
- console.log(chalk20.cyan(" tuck sync") + chalk20.dim(" - Commit and push your changes"));
5708
- 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"));
5709
6038
  } else if (gitStatus.ahead > 0) {
5710
- 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"));
5711
6040
  } else {
5712
- 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."));
5713
6042
  console.log();
5714
- console.log(chalk20.cyan(" tuck scan") + chalk20.dim(" - Find more dotfiles to track"));
5715
- 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"));
5716
6045
  }
5717
6046
  console.log();
5718
6047
  } catch {
5719
6048
  miniBanner();
5720
- console.log(chalk20.yellow("Tuck directory exists but may be corrupted."));
5721
- 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."));
5722
6051
  console.log();
5723
6052
  }
5724
6053
  };