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