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