@prnv/tuck 1.5.2 → 1.7.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/README.md +52 -10
- package/dist/index.js +2100 -127
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -555,7 +555,7 @@ import { join, dirname } from "path";
|
|
|
555
555
|
import { readFileSync } from "fs";
|
|
556
556
|
import { fileURLToPath } from "url";
|
|
557
557
|
import figures4 from "figures";
|
|
558
|
-
var __dirname, packageJsonPath, VERSION_VALUE, VERSION, DESCRIPTION, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES;
|
|
558
|
+
var __dirname, packageJsonPath, VERSION_VALUE, VERSION, DESCRIPTION, APP_NAME, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES;
|
|
559
559
|
var init_constants = __esm({
|
|
560
560
|
"src/constants.ts"() {
|
|
561
561
|
"use strict";
|
|
@@ -563,12 +563,13 @@ var init_constants = __esm({
|
|
|
563
563
|
packageJsonPath = join(__dirname, "..", "package.json");
|
|
564
564
|
VERSION_VALUE = "1.0.0";
|
|
565
565
|
try {
|
|
566
|
-
const
|
|
567
|
-
VERSION_VALUE =
|
|
566
|
+
const pkg2 = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
567
|
+
VERSION_VALUE = pkg2.version;
|
|
568
568
|
} catch {
|
|
569
569
|
}
|
|
570
570
|
VERSION = VERSION_VALUE;
|
|
571
571
|
DESCRIPTION = "Modern dotfiles manager with a beautiful CLI";
|
|
572
|
+
APP_NAME = "tuck";
|
|
572
573
|
HOME_DIR = homedir();
|
|
573
574
|
DEFAULT_TUCK_DIR = join(HOME_DIR, ".tuck");
|
|
574
575
|
MANIFEST_FILE = ".tuckmanifest.json";
|
|
@@ -851,12 +852,25 @@ var init_secrets_schema = __esm({
|
|
|
851
852
|
|
|
852
853
|
// src/schemas/config.schema.ts
|
|
853
854
|
import { z as z2 } from "zod";
|
|
854
|
-
var fileStrategySchema, categoryConfigSchema, tuckConfigSchema, defaultConfig;
|
|
855
|
+
var fileStrategySchema, providerModeSchema, remoteConfigSchema, categoryConfigSchema, tuckConfigSchema, defaultConfig;
|
|
855
856
|
var init_config_schema = __esm({
|
|
856
857
|
"src/schemas/config.schema.ts"() {
|
|
857
858
|
"use strict";
|
|
858
859
|
init_secrets_schema();
|
|
859
860
|
fileStrategySchema = z2.enum(["copy", "symlink"]);
|
|
861
|
+
providerModeSchema = z2.enum(["github", "gitlab", "local", "custom"]);
|
|
862
|
+
remoteConfigSchema = z2.object({
|
|
863
|
+
/** Provider mode (github, gitlab, local, custom) */
|
|
864
|
+
mode: providerModeSchema.default("local"),
|
|
865
|
+
/** Custom remote URL (for custom mode or manual override) */
|
|
866
|
+
url: z2.string().optional(),
|
|
867
|
+
/** Provider instance URL (for self-hosted GitLab, etc.) */
|
|
868
|
+
providerUrl: z2.string().optional(),
|
|
869
|
+
/** Cached username from provider */
|
|
870
|
+
username: z2.string().optional(),
|
|
871
|
+
/** Repository name (without owner) */
|
|
872
|
+
repoName: z2.string().optional()
|
|
873
|
+
}).default({ mode: "local" });
|
|
860
874
|
categoryConfigSchema = z2.object({
|
|
861
875
|
patterns: z2.array(z2.string()),
|
|
862
876
|
icon: z2.string().optional()
|
|
@@ -895,7 +909,9 @@ var init_config_schema = __esm({
|
|
|
895
909
|
emoji: z2.boolean().default(true),
|
|
896
910
|
verbose: z2.boolean().default(false)
|
|
897
911
|
}).partial().default({}),
|
|
898
|
-
security: securityConfigSchema
|
|
912
|
+
security: securityConfigSchema,
|
|
913
|
+
/** Remote/provider configuration */
|
|
914
|
+
remote: remoteConfigSchema
|
|
899
915
|
});
|
|
900
916
|
defaultConfig = {
|
|
901
917
|
repository: {
|
|
@@ -932,6 +948,9 @@ var init_config_schema = __esm({
|
|
|
932
948
|
excludePatterns: [],
|
|
933
949
|
excludeFiles: [],
|
|
934
950
|
maxFileSize: 10 * 1024 * 1024
|
|
951
|
+
},
|
|
952
|
+
remote: {
|
|
953
|
+
mode: "local"
|
|
935
954
|
}
|
|
936
955
|
};
|
|
937
956
|
}
|
|
@@ -1476,13 +1495,13 @@ var init_git = __esm({
|
|
|
1476
1495
|
};
|
|
1477
1496
|
ensureGitCredentials = async () => {
|
|
1478
1497
|
try {
|
|
1479
|
-
const { execFile:
|
|
1480
|
-
const { promisify:
|
|
1481
|
-
const
|
|
1482
|
-
const { stdout, stderr } = await
|
|
1498
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
1499
|
+
const { promisify: promisify7 } = await import("util");
|
|
1500
|
+
const execFileAsync6 = promisify7(execFile6);
|
|
1501
|
+
const { stdout, stderr } = await execFileAsync6("gh", ["auth", "status"]);
|
|
1483
1502
|
const output = (stderr || stdout || "").trim();
|
|
1484
1503
|
if (output.includes("Logged in")) {
|
|
1485
|
-
await
|
|
1504
|
+
await execFileAsync6("gh", ["auth", "setup-git"]);
|
|
1486
1505
|
}
|
|
1487
1506
|
} catch {
|
|
1488
1507
|
}
|
|
@@ -1618,6 +1637,591 @@ var init_git = __esm({
|
|
|
1618
1637
|
}
|
|
1619
1638
|
});
|
|
1620
1639
|
|
|
1640
|
+
// src/lib/providers/types.ts
|
|
1641
|
+
var ProviderError, ProviderNotConfiguredError, LocalModeError;
|
|
1642
|
+
var init_types = __esm({
|
|
1643
|
+
"src/lib/providers/types.ts"() {
|
|
1644
|
+
"use strict";
|
|
1645
|
+
ProviderError = class extends Error {
|
|
1646
|
+
constructor(message, provider, suggestions = []) {
|
|
1647
|
+
super(message);
|
|
1648
|
+
this.provider = provider;
|
|
1649
|
+
this.suggestions = suggestions;
|
|
1650
|
+
this.name = "ProviderError";
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
ProviderNotConfiguredError = class extends ProviderError {
|
|
1654
|
+
constructor(provider) {
|
|
1655
|
+
super(`Provider "${provider}" is not configured`, provider, [
|
|
1656
|
+
"Run `tuck init` to set up your git provider",
|
|
1657
|
+
"Or use `tuck config remote` to change providers"
|
|
1658
|
+
]);
|
|
1659
|
+
this.name = "ProviderNotConfiguredError";
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
LocalModeError = class extends ProviderError {
|
|
1663
|
+
constructor(operation) {
|
|
1664
|
+
super(`Cannot ${operation} in local-only mode`, "local", [
|
|
1665
|
+
"Your tuck is configured for local-only storage (no remote sync)",
|
|
1666
|
+
"To enable remote sync, run: tuck config remote",
|
|
1667
|
+
"Or re-run: tuck init"
|
|
1668
|
+
]);
|
|
1669
|
+
this.name = "LocalModeError";
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
// src/lib/validation.ts
|
|
1676
|
+
function validateRepoName(repoName, provider) {
|
|
1677
|
+
if (/[\x00-\x1F\x7F]/.test(repoName)) {
|
|
1678
|
+
throw new Error("Repository name contains invalid control characters");
|
|
1679
|
+
}
|
|
1680
|
+
if (repoName.includes("://")) {
|
|
1681
|
+
validateHttpUrl(repoName, provider);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (repoName.startsWith("git@")) {
|
|
1685
|
+
validateSshUrl(repoName, provider);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const validPattern = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(?:\/[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)*$/;
|
|
1689
|
+
if (!validPattern.test(repoName)) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
'Repository names must start and end with alphanumeric characters and can only contain alphanumeric characters, hyphens, underscores, and dots. Format: "owner/repo" or "repo"'
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function validateHttpUrl(url, provider) {
|
|
1696
|
+
let parsedUrl;
|
|
1697
|
+
try {
|
|
1698
|
+
parsedUrl = new URL(url);
|
|
1699
|
+
} catch {
|
|
1700
|
+
throw new Error(`Invalid repository URL: ${url}`);
|
|
1701
|
+
}
|
|
1702
|
+
const allowedProtocols = /* @__PURE__ */ new Set(["http:", "https:", "ssh:", "git+ssh:"]);
|
|
1703
|
+
if (!allowedProtocols.has(parsedUrl.protocol)) {
|
|
1704
|
+
throw new Error(`Invalid protocol in URL. Allowed: ${Array.from(allowedProtocols).join(", ")}`);
|
|
1705
|
+
}
|
|
1706
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(parsedUrl.hostname)) {
|
|
1707
|
+
throw new Error("Invalid characters in hostname");
|
|
1708
|
+
}
|
|
1709
|
+
if (/[;&|`$(){}[\]<>!#*?'"\\]/.test(url)) {
|
|
1710
|
+
throw new Error("URL contains invalid characters");
|
|
1711
|
+
}
|
|
1712
|
+
if (provider === "github" && parsedUrl.hostname === "github.com") {
|
|
1713
|
+
const pathPattern = /^\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(?:\.git)?$/;
|
|
1714
|
+
if (!pathPattern.test(parsedUrl.pathname)) {
|
|
1715
|
+
throw new Error("Invalid GitHub repository URL format. Expected: /owner/repo or /owner/repo.git");
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function validateSshUrl(url, _provider) {
|
|
1720
|
+
if (/[;&|`$(){}[\]<>!#*?'"\\]/.test(url)) {
|
|
1721
|
+
throw new Error("URL contains invalid characters");
|
|
1722
|
+
}
|
|
1723
|
+
const sshPattern = /^git@([a-zA-Z0-9.-]+):([a-zA-Z0-9._/-]+)(?:\.git)?$/;
|
|
1724
|
+
if (!sshPattern.test(url)) {
|
|
1725
|
+
throw new Error("Invalid SSH URL format. Expected: git@host:path or git@host:path.git");
|
|
1726
|
+
}
|
|
1727
|
+
const match = url.match(sshPattern);
|
|
1728
|
+
if (match) {
|
|
1729
|
+
const [, host, path] = match;
|
|
1730
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
|
|
1731
|
+
throw new Error("Invalid hostname in SSH URL");
|
|
1732
|
+
}
|
|
1733
|
+
if (path.includes("..")) {
|
|
1734
|
+
throw new Error("Path traversal not allowed in repository URL");
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function validateDescription(description, maxLength = 350) {
|
|
1739
|
+
if (description.length > maxLength) {
|
|
1740
|
+
throw new Error(`Description too long (max ${maxLength} characters)`);
|
|
1741
|
+
}
|
|
1742
|
+
if (/[;&|`$(){}[\]<>!#*?'"\\n\r\t\x00-\x1F\x7F]/.test(description)) {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
"Description contains invalid characters. Cannot contain: ; & | ` $ ( ) { } [ ] < > ! # * ? ' \" \\ newlines or control characters"
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
const normalized = description.normalize("NFKC");
|
|
1748
|
+
if (normalized !== description) {
|
|
1749
|
+
throw new Error("Description contains unusual Unicode characters");
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function validateHostname(hostname) {
|
|
1753
|
+
if (!hostname) {
|
|
1754
|
+
throw new Error("Hostname is required");
|
|
1755
|
+
}
|
|
1756
|
+
if (hostname.length > 253) {
|
|
1757
|
+
throw new Error("Hostname too long (max 253 characters)");
|
|
1758
|
+
}
|
|
1759
|
+
const hostnamePattern = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
1760
|
+
if (!hostnamePattern.test(hostname)) {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
"Invalid hostname format. Must be a fully qualified domain name (e.g., gitlab.example.com)"
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
1766
|
+
if (pattern.test(hostname)) {
|
|
1767
|
+
throw new Error("Private IP addresses and localhost are not allowed for security reasons");
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const labels = hostname.split(".");
|
|
1771
|
+
if (labels.length < 2) {
|
|
1772
|
+
throw new Error("Hostname must have at least two labels (subdomain.domain.tld)");
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function validateGitUrl(url) {
|
|
1776
|
+
if (/[\x00-\x1F\x7F]/.test(url)) {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
const sshPattern = /^git@[a-zA-Z0-9.-]+:[a-zA-Z0-9._/-]+(?:\.git)?$/;
|
|
1780
|
+
const sshUrlPattern = /^ssh:\/\/git@[a-zA-Z0-9.-]+\/[a-zA-Z0-9._/-]+(?:\.git)?$/;
|
|
1781
|
+
const httpsPattern = /^https?:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9._/-]+(?:\.git)?$/;
|
|
1782
|
+
const gitPattern = /^git:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9._/-]+(?:\.git)?$/;
|
|
1783
|
+
if (url.startsWith("file://") || url.startsWith("/")) {
|
|
1784
|
+
return validateFileUrl(url);
|
|
1785
|
+
}
|
|
1786
|
+
if (sshPattern.test(url) || sshUrlPattern.test(url) || httpsPattern.test(url) || gitPattern.test(url)) {
|
|
1787
|
+
if (/[;&|`$(){}[\]<>!#*?]/.test(url.replace(/[/:@.]/g, ""))) {
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
function validateFileUrl(url) {
|
|
1795
|
+
const path = url.replace(/^file:\/\//, "");
|
|
1796
|
+
for (const blockedPath of BLOCKED_SYSTEM_PATHS) {
|
|
1797
|
+
if (path.startsWith(blockedPath)) {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (path.includes("../") || path.includes("/..")) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
if (!path.startsWith("/")) {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
const filePattern = /^\/[a-zA-Z0-9._/-]+$/;
|
|
1808
|
+
return filePattern.test(path);
|
|
1809
|
+
}
|
|
1810
|
+
function sanitizeErrorMessage(error, genericMessage) {
|
|
1811
|
+
if (error instanceof Error) {
|
|
1812
|
+
const message = error.message.toLowerCase();
|
|
1813
|
+
if (message.includes("enotfound") || message.includes("network")) {
|
|
1814
|
+
return "Network error. Please check your internet connection.";
|
|
1815
|
+
}
|
|
1816
|
+
if (message.includes("permission") || message.includes("eacces")) {
|
|
1817
|
+
return "Permission denied. Please check your access rights.";
|
|
1818
|
+
}
|
|
1819
|
+
if (message.includes("timeout") || message.includes("etimedout")) {
|
|
1820
|
+
return "Operation timed out. Please try again.";
|
|
1821
|
+
}
|
|
1822
|
+
if (message.includes("authentication") || message.includes("auth")) {
|
|
1823
|
+
return "Authentication failed. Please check your credentials.";
|
|
1824
|
+
}
|
|
1825
|
+
return genericMessage;
|
|
1826
|
+
}
|
|
1827
|
+
return genericMessage;
|
|
1828
|
+
}
|
|
1829
|
+
var GIT_OPERATION_TIMEOUTS, BLOCKED_SYSTEM_PATHS, PRIVATE_IP_PATTERNS;
|
|
1830
|
+
var init_validation = __esm({
|
|
1831
|
+
"src/lib/validation.ts"() {
|
|
1832
|
+
"use strict";
|
|
1833
|
+
GIT_OPERATION_TIMEOUTS = {
|
|
1834
|
+
LS_REMOTE: 3e4,
|
|
1835
|
+
// 30 seconds (increased from 10s)
|
|
1836
|
+
CLONE: 3e5,
|
|
1837
|
+
// 5 minutes
|
|
1838
|
+
FETCH: 6e4,
|
|
1839
|
+
// 1 minute
|
|
1840
|
+
PUSH: 6e4
|
|
1841
|
+
// 1 minute
|
|
1842
|
+
};
|
|
1843
|
+
BLOCKED_SYSTEM_PATHS = [
|
|
1844
|
+
"/etc/",
|
|
1845
|
+
"/proc/",
|
|
1846
|
+
"/sys/",
|
|
1847
|
+
"/dev/",
|
|
1848
|
+
"/boot/",
|
|
1849
|
+
"/root/",
|
|
1850
|
+
"/var/run/",
|
|
1851
|
+
"/var/log/"
|
|
1852
|
+
];
|
|
1853
|
+
PRIVATE_IP_PATTERNS = [
|
|
1854
|
+
/^localhost$/i,
|
|
1855
|
+
/^127\.\d+\.\d+\.\d+$/,
|
|
1856
|
+
/^::1$/,
|
|
1857
|
+
/^0\.0\.0\.0$/,
|
|
1858
|
+
/^10\./,
|
|
1859
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
1860
|
+
/^192\.168\./,
|
|
1861
|
+
/^169\.254\./,
|
|
1862
|
+
// Link-local
|
|
1863
|
+
/^fe80:/i
|
|
1864
|
+
// IPv6 link-local
|
|
1865
|
+
];
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
// src/lib/providers/gitlab.ts
|
|
1870
|
+
var gitlab_exports = {};
|
|
1871
|
+
__export(gitlab_exports, {
|
|
1872
|
+
GitLabProvider: () => GitLabProvider,
|
|
1873
|
+
gitlabProvider: () => gitlabProvider
|
|
1874
|
+
});
|
|
1875
|
+
import { execFile } from "child_process";
|
|
1876
|
+
import { promisify } from "util";
|
|
1877
|
+
var execFileAsync, COMMON_DOTFILE_REPO_NAMES, DEFAULT_GITLAB_HOST, GitLabProvider, gitlabProvider;
|
|
1878
|
+
var init_gitlab = __esm({
|
|
1879
|
+
"src/lib/providers/gitlab.ts"() {
|
|
1880
|
+
"use strict";
|
|
1881
|
+
init_types();
|
|
1882
|
+
init_validation();
|
|
1883
|
+
execFileAsync = promisify(execFile);
|
|
1884
|
+
COMMON_DOTFILE_REPO_NAMES = ["dotfiles", "tuck", ".dotfiles", "dot-files", "dots"];
|
|
1885
|
+
DEFAULT_GITLAB_HOST = "gitlab.com";
|
|
1886
|
+
GitLabProvider = class _GitLabProvider {
|
|
1887
|
+
mode = "gitlab";
|
|
1888
|
+
displayName = "GitLab";
|
|
1889
|
+
cliName = "glab";
|
|
1890
|
+
requiresRemote = true;
|
|
1891
|
+
/** The GitLab host (gitlab.com or self-hosted URL) */
|
|
1892
|
+
host;
|
|
1893
|
+
constructor(host = DEFAULT_GITLAB_HOST) {
|
|
1894
|
+
this.host = host.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
1895
|
+
}
|
|
1896
|
+
/** Create a provider for a specific host */
|
|
1897
|
+
static forHost(host) {
|
|
1898
|
+
return new _GitLabProvider(host);
|
|
1899
|
+
}
|
|
1900
|
+
// -------------------------------------------------------------------------
|
|
1901
|
+
// Detection & Authentication
|
|
1902
|
+
// -------------------------------------------------------------------------
|
|
1903
|
+
async isCliInstalled() {
|
|
1904
|
+
try {
|
|
1905
|
+
await execFileAsync("glab", ["--version"]);
|
|
1906
|
+
return true;
|
|
1907
|
+
} catch {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async isAuthenticated() {
|
|
1912
|
+
try {
|
|
1913
|
+
const { stdout, stderr } = await execFileAsync("glab", ["auth", "status", "-h", this.host]);
|
|
1914
|
+
const output = (stderr || stdout || "").trim();
|
|
1915
|
+
return output.includes("Logged in");
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
if (error instanceof Error && "stderr" in error) {
|
|
1918
|
+
const stderr = error.stderr;
|
|
1919
|
+
return stderr.includes("Logged in");
|
|
1920
|
+
}
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
async getUser() {
|
|
1925
|
+
if (!await this.isCliInstalled() || !await this.isAuthenticated()) {
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
const { stdout } = await execFileAsync("glab", ["api", "user", "-h", this.host]);
|
|
1930
|
+
const data = JSON.parse(stdout);
|
|
1931
|
+
return {
|
|
1932
|
+
login: data.username || "",
|
|
1933
|
+
name: data.name || null,
|
|
1934
|
+
email: data.email || null
|
|
1935
|
+
};
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
try {
|
|
1938
|
+
const { stdout, stderr } = await execFileAsync("glab", ["auth", "status", "-h", this.host]);
|
|
1939
|
+
const output = stderr || stdout || "";
|
|
1940
|
+
const match = output.match(/Logged in to .+ as (\S+)/);
|
|
1941
|
+
if (match) {
|
|
1942
|
+
return {
|
|
1943
|
+
login: match[1],
|
|
1944
|
+
name: null,
|
|
1945
|
+
email: null
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
} catch (fallbackError) {
|
|
1949
|
+
}
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
async detect() {
|
|
1954
|
+
const cliInstalled = await this.isCliInstalled();
|
|
1955
|
+
if (!cliInstalled) {
|
|
1956
|
+
return {
|
|
1957
|
+
mode: this.mode,
|
|
1958
|
+
displayName: this.getDisplayNameWithHost(),
|
|
1959
|
+
available: false,
|
|
1960
|
+
authStatus: {
|
|
1961
|
+
cliInstalled: false,
|
|
1962
|
+
authenticated: false,
|
|
1963
|
+
instanceUrl: this.getInstanceUrl()
|
|
1964
|
+
},
|
|
1965
|
+
unavailableReason: "GitLab CLI (glab) is not installed"
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const authenticated = await this.isAuthenticated();
|
|
1969
|
+
const user = authenticated ? await this.getUser() : void 0;
|
|
1970
|
+
return {
|
|
1971
|
+
mode: this.mode,
|
|
1972
|
+
displayName: this.getDisplayNameWithHost(),
|
|
1973
|
+
available: authenticated,
|
|
1974
|
+
authStatus: {
|
|
1975
|
+
cliInstalled: true,
|
|
1976
|
+
authenticated,
|
|
1977
|
+
user: user || void 0,
|
|
1978
|
+
instanceUrl: this.getInstanceUrl()
|
|
1979
|
+
},
|
|
1980
|
+
unavailableReason: !authenticated ? `Not logged in to GitLab (${this.host})` : void 0
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
// -------------------------------------------------------------------------
|
|
1984
|
+
// Repository Operations
|
|
1985
|
+
// -------------------------------------------------------------------------
|
|
1986
|
+
async repoExists(repoName) {
|
|
1987
|
+
this.validateRepoName(repoName);
|
|
1988
|
+
try {
|
|
1989
|
+
await execFileAsync("glab", ["repo", "view", repoName, "-h", this.host]);
|
|
1990
|
+
return true;
|
|
1991
|
+
} catch {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
async createRepo(options) {
|
|
1996
|
+
if (!await this.isCliInstalled()) {
|
|
1997
|
+
throw new ProviderError("GitLab CLI is not installed", "gitlab", [
|
|
1998
|
+
"Install with: brew install glab (macOS)",
|
|
1999
|
+
"Or see: https://gitlab.com/gitlab-org/cli"
|
|
2000
|
+
]);
|
|
2001
|
+
}
|
|
2002
|
+
if (!await this.isAuthenticated()) {
|
|
2003
|
+
throw new ProviderError(`Not authenticated with GitLab (${this.host})`, "gitlab", [
|
|
2004
|
+
`Run: glab auth login -h ${this.host}`
|
|
2005
|
+
]);
|
|
2006
|
+
}
|
|
2007
|
+
const user = await this.getUser();
|
|
2008
|
+
if (!user) {
|
|
2009
|
+
throw new ProviderError("Could not get GitLab user information", "gitlab");
|
|
2010
|
+
}
|
|
2011
|
+
this.validateRepoName(options.name);
|
|
2012
|
+
if (options.description) {
|
|
2013
|
+
try {
|
|
2014
|
+
validateDescription(options.description, 2e3);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
throw new ProviderError(
|
|
2017
|
+
error instanceof Error ? error.message : "Invalid description",
|
|
2018
|
+
"gitlab"
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
const fullName = `${user.login}/${options.name}`;
|
|
2023
|
+
if (await this.repoExists(fullName)) {
|
|
2024
|
+
throw new ProviderError(`Repository "${fullName}" already exists`, "gitlab", [
|
|
2025
|
+
`Use a different name or import the existing repo`
|
|
2026
|
+
]);
|
|
2027
|
+
}
|
|
2028
|
+
const args = ["repo", "create", options.name, "-h", this.host];
|
|
2029
|
+
if (options.isPrivate !== false) {
|
|
2030
|
+
args.push("--private");
|
|
2031
|
+
} else {
|
|
2032
|
+
args.push("--public");
|
|
2033
|
+
}
|
|
2034
|
+
if (options.description) {
|
|
2035
|
+
args.push("--description", options.description);
|
|
2036
|
+
}
|
|
2037
|
+
args.push("-y");
|
|
2038
|
+
try {
|
|
2039
|
+
const { stdout } = await execFileAsync("glab", args);
|
|
2040
|
+
const urlMatch = stdout.match(/https?:\/\/[^\s]+/);
|
|
2041
|
+
const repoUrl = urlMatch ? urlMatch[0] : `https://${this.host}/${fullName}`;
|
|
2042
|
+
return {
|
|
2043
|
+
name: options.name,
|
|
2044
|
+
fullName,
|
|
2045
|
+
url: repoUrl,
|
|
2046
|
+
sshUrl: `git@${this.host}:${fullName}.git`,
|
|
2047
|
+
httpsUrl: `https://${this.host}/${fullName}.git`,
|
|
2048
|
+
isPrivate: options.isPrivate !== false
|
|
2049
|
+
};
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
const sanitizedMessage = sanitizeErrorMessage(error, "Failed to create repository");
|
|
2052
|
+
throw new ProviderError(sanitizedMessage, "gitlab", [
|
|
2053
|
+
`Try creating the repository manually at https://${this.host}/projects/new`
|
|
2054
|
+
]);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async getRepoInfo(repoName) {
|
|
2058
|
+
this.validateRepoName(repoName);
|
|
2059
|
+
try {
|
|
2060
|
+
const { stdout } = await execFileAsync("glab", [
|
|
2061
|
+
"repo",
|
|
2062
|
+
"view",
|
|
2063
|
+
repoName,
|
|
2064
|
+
"-h",
|
|
2065
|
+
this.host,
|
|
2066
|
+
"-o",
|
|
2067
|
+
"json"
|
|
2068
|
+
]);
|
|
2069
|
+
const result = JSON.parse(stdout);
|
|
2070
|
+
const pathWithNamespace = result.path_with_namespace || repoName;
|
|
2071
|
+
return {
|
|
2072
|
+
name: result.name || repoName.split("/").pop() || repoName,
|
|
2073
|
+
fullName: pathWithNamespace,
|
|
2074
|
+
url: result.web_url || `https://${this.host}/${pathWithNamespace}`,
|
|
2075
|
+
sshUrl: result.ssh_url_to_repo || `git@${this.host}:${pathWithNamespace}.git`,
|
|
2076
|
+
httpsUrl: result.http_url_to_repo || `https://${this.host}/${pathWithNamespace}.git`,
|
|
2077
|
+
isPrivate: result.visibility === "private"
|
|
2078
|
+
};
|
|
2079
|
+
} catch {
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
async cloneRepo(repoName, targetDir) {
|
|
2084
|
+
if (!await this.isCliInstalled()) {
|
|
2085
|
+
throw new ProviderError("GitLab CLI is not installed", "gitlab");
|
|
2086
|
+
}
|
|
2087
|
+
this.validateRepoName(repoName);
|
|
2088
|
+
try {
|
|
2089
|
+
await execFileAsync("glab", ["repo", "clone", repoName, targetDir, "-h", this.host]);
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
throw new ProviderError(`Failed to clone repository "${repoName}"`, "gitlab", [
|
|
2092
|
+
String(error),
|
|
2093
|
+
"Check that the repository exists and you have access"
|
|
2094
|
+
]);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
async findDotfilesRepo(username) {
|
|
2098
|
+
const user = username || (await this.getUser())?.login;
|
|
2099
|
+
if (!user) return null;
|
|
2100
|
+
for (const name of COMMON_DOTFILE_REPO_NAMES) {
|
|
2101
|
+
const repoName = `${user}/${name}`;
|
|
2102
|
+
if (await this.repoExists(repoName)) {
|
|
2103
|
+
return repoName;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
// -------------------------------------------------------------------------
|
|
2109
|
+
// URL Utilities
|
|
2110
|
+
// -------------------------------------------------------------------------
|
|
2111
|
+
async getPreferredRepoUrl(repo) {
|
|
2112
|
+
const protocol = await this.getPreferredProtocol();
|
|
2113
|
+
return protocol === "ssh" ? repo.sshUrl : repo.httpsUrl;
|
|
2114
|
+
}
|
|
2115
|
+
validateUrl(url) {
|
|
2116
|
+
const host = this.host.replace(/\./g, "\\.");
|
|
2117
|
+
const httpsPattern = new RegExp(`^https://${host}/`);
|
|
2118
|
+
const sshPattern = new RegExp(`^git@${host}:`);
|
|
2119
|
+
const sshUrlPattern = new RegExp(`^ssh://git@${host}/`);
|
|
2120
|
+
return httpsPattern.test(url) || sshPattern.test(url) || sshUrlPattern.test(url);
|
|
2121
|
+
}
|
|
2122
|
+
buildRepoUrl(username, repoName, protocol) {
|
|
2123
|
+
if (protocol === "ssh") {
|
|
2124
|
+
return `git@${this.host}:${username}/${repoName}.git`;
|
|
2125
|
+
}
|
|
2126
|
+
return `https://${this.host}/${username}/${repoName}.git`;
|
|
2127
|
+
}
|
|
2128
|
+
// -------------------------------------------------------------------------
|
|
2129
|
+
// Instructions
|
|
2130
|
+
// -------------------------------------------------------------------------
|
|
2131
|
+
getSetupInstructions() {
|
|
2132
|
+
const { platform: platform2 } = process;
|
|
2133
|
+
let installCmd = "";
|
|
2134
|
+
if (platform2 === "darwin") {
|
|
2135
|
+
installCmd = "brew install glab";
|
|
2136
|
+
} else if (platform2 === "linux") {
|
|
2137
|
+
installCmd = `# Debian/Ubuntu:
|
|
2138
|
+
# Download from https://gitlab.com/gitlab-org/cli/-/releases
|
|
2139
|
+
|
|
2140
|
+
# Or using brew:
|
|
2141
|
+
brew install glab`;
|
|
2142
|
+
} else if (platform2 === "win32") {
|
|
2143
|
+
installCmd = `# Using winget:
|
|
2144
|
+
winget install GitLab.glab
|
|
2145
|
+
|
|
2146
|
+
# Using scoop:
|
|
2147
|
+
scoop install glab`;
|
|
2148
|
+
}
|
|
2149
|
+
const hostInstructions = this.host !== DEFAULT_GITLAB_HOST ? `
|
|
2150
|
+
For self-hosted GitLab (${this.host}):
|
|
2151
|
+
glab auth login -h ${this.host}` : "";
|
|
2152
|
+
return `GitLab CLI (glab) - Official GitLab command line tool
|
|
2153
|
+
|
|
2154
|
+
Installation:
|
|
2155
|
+
${installCmd}
|
|
2156
|
+
|
|
2157
|
+
After installing, authenticate:
|
|
2158
|
+
glab auth login
|
|
2159
|
+
${hostInstructions}
|
|
2160
|
+
|
|
2161
|
+
Benefits:
|
|
2162
|
+
- Automatic repository creation
|
|
2163
|
+
- No manual token management
|
|
2164
|
+
- Works with self-hosted GitLab
|
|
2165
|
+
|
|
2166
|
+
Learn more: https://gitlab.com/gitlab-org/cli`;
|
|
2167
|
+
}
|
|
2168
|
+
getAltAuthInstructions() {
|
|
2169
|
+
return `Alternative authentication methods for GitLab:
|
|
2170
|
+
|
|
2171
|
+
1. SSH Keys (recommended if glab CLI unavailable)
|
|
2172
|
+
- Generate: ssh-keygen -t ed25519
|
|
2173
|
+
- Add to GitLab: https://${this.host}/-/user_settings/ssh_keys
|
|
2174
|
+
- Test: ssh -T git@${this.host}
|
|
2175
|
+
|
|
2176
|
+
2. Personal Access Token
|
|
2177
|
+
- Create at: https://${this.host}/-/user_settings/personal_access_tokens
|
|
2178
|
+
- Required scopes: "api", "read_repository", "write_repository"
|
|
2179
|
+
- Use as password when pushing
|
|
2180
|
+
|
|
2181
|
+
For detailed instructions, see:
|
|
2182
|
+
https://docs.gitlab.com/ee/user/ssh.html`;
|
|
2183
|
+
}
|
|
2184
|
+
// -------------------------------------------------------------------------
|
|
2185
|
+
// Private Helpers
|
|
2186
|
+
// -------------------------------------------------------------------------
|
|
2187
|
+
validateRepoName(repoName) {
|
|
2188
|
+
try {
|
|
2189
|
+
validateRepoName(repoName, "gitlab");
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
throw new ProviderError(
|
|
2192
|
+
error instanceof Error ? error.message : "Invalid repository name",
|
|
2193
|
+
"gitlab"
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async getPreferredProtocol() {
|
|
2198
|
+
try {
|
|
2199
|
+
const { stdout } = await execFileAsync("glab", [
|
|
2200
|
+
"config",
|
|
2201
|
+
"get",
|
|
2202
|
+
"git_protocol",
|
|
2203
|
+
"-h",
|
|
2204
|
+
this.host
|
|
2205
|
+
]);
|
|
2206
|
+
return stdout.trim().toLowerCase() === "ssh" ? "ssh" : "https";
|
|
2207
|
+
} catch {
|
|
2208
|
+
return "https";
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
getDisplayNameWithHost() {
|
|
2212
|
+
if (this.host === DEFAULT_GITLAB_HOST) {
|
|
2213
|
+
return "GitLab";
|
|
2214
|
+
}
|
|
2215
|
+
return `GitLab (${this.host})`;
|
|
2216
|
+
}
|
|
2217
|
+
getInstanceUrl() {
|
|
2218
|
+
return `https://${this.host}`;
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
gitlabProvider = new GitLabProvider();
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
|
|
1621
2225
|
// src/lib/github.ts
|
|
1622
2226
|
var github_exports = {};
|
|
1623
2227
|
__export(github_exports, {
|
|
@@ -1649,14 +2253,14 @@ __export(github_exports, {
|
|
|
1649
2253
|
testStoredCredentials: () => testStoredCredentials,
|
|
1650
2254
|
updateStoredCredentials: () => updateStoredCredentials
|
|
1651
2255
|
});
|
|
1652
|
-
import { execFile } from "child_process";
|
|
1653
|
-
import { promisify } from "util";
|
|
1654
|
-
var
|
|
2256
|
+
import { execFile as execFile4 } from "child_process";
|
|
2257
|
+
import { promisify as promisify4 } from "util";
|
|
2258
|
+
var execFileAsync4, API_REQUEST_TIMEOUT_MS, GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS, TOKEN_EXPIRATION_WARNING_DAYS, MIN_GITHUB_TOKEN_LENGTH, GITHUB_TOKEN_PREFIXES, validateRepoName2, isGhInstalled, isGhAuthenticated, getAuthenticatedUser, repoExists, diagnoseRepoCreationFailure, createRepo, getPreferredRemoteProtocol, getRepoInfo, ghCloneRepo, findDotfilesRepo, getPreferredRepoUrl, checkSSHKeys, getStrictHostKeyCheckingOption, testSSHConnection, getSSHKeyInstructions, getFineGrainedTokenInstructions, getClassicTokenInstructions, getGitHubCLIInstallInstructions, getAuthMethods, configureGitCredentialHelper, getCredentialsPath, storeGitHubCredentials, getStoredCredentialMetadata, removeStoredCredentials, testStoredCredentials, diagnoseAuthIssue, updateStoredCredentials, detectTokenType;
|
|
1655
2259
|
var init_github = __esm({
|
|
1656
2260
|
"src/lib/github.ts"() {
|
|
1657
2261
|
"use strict";
|
|
1658
2262
|
init_errors();
|
|
1659
|
-
|
|
2263
|
+
execFileAsync4 = promisify4(execFile4);
|
|
1660
2264
|
API_REQUEST_TIMEOUT_MS = 1e4;
|
|
1661
2265
|
GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS = 86400;
|
|
1662
2266
|
TOKEN_EXPIRATION_WARNING_DAYS = 85;
|
|
@@ -1675,7 +2279,7 @@ var init_github = __esm({
|
|
|
1675
2279
|
"ghr_"
|
|
1676
2280
|
// Refresh token
|
|
1677
2281
|
];
|
|
1678
|
-
|
|
2282
|
+
validateRepoName2 = (repoName) => {
|
|
1679
2283
|
if (repoName.includes("://") || repoName.startsWith("git@")) {
|
|
1680
2284
|
if (/[;&|`$(){}[\]<>!#*?]/.test(repoName.replace(/[/:@.]/g, ""))) {
|
|
1681
2285
|
throw new GitHubCliError(`Invalid repository URL: ${repoName}`);
|
|
@@ -1692,7 +2296,7 @@ var init_github = __esm({
|
|
|
1692
2296
|
};
|
|
1693
2297
|
isGhInstalled = async () => {
|
|
1694
2298
|
try {
|
|
1695
|
-
await
|
|
2299
|
+
await execFileAsync4("gh", ["--version"]);
|
|
1696
2300
|
return true;
|
|
1697
2301
|
} catch {
|
|
1698
2302
|
return false;
|
|
@@ -1700,7 +2304,7 @@ var init_github = __esm({
|
|
|
1700
2304
|
};
|
|
1701
2305
|
isGhAuthenticated = async () => {
|
|
1702
2306
|
try {
|
|
1703
|
-
const { stdout, stderr } = await
|
|
2307
|
+
const { stdout, stderr } = await execFileAsync4("gh", ["auth", "status"]);
|
|
1704
2308
|
const output = (stderr || stdout || "").trim();
|
|
1705
2309
|
return output.includes("Logged in");
|
|
1706
2310
|
} catch (error) {
|
|
@@ -1721,7 +2325,7 @@ var init_github = __esm({
|
|
|
1721
2325
|
]);
|
|
1722
2326
|
}
|
|
1723
2327
|
try {
|
|
1724
|
-
const { stdout } = await
|
|
2328
|
+
const { stdout } = await execFileAsync4("gh", ["api", "user", "--jq", ".login, .name, .email"]);
|
|
1725
2329
|
const lines = stdout.trim().split("\n");
|
|
1726
2330
|
return {
|
|
1727
2331
|
login: lines[0] || "",
|
|
@@ -1737,8 +2341,8 @@ var init_github = __esm({
|
|
|
1737
2341
|
};
|
|
1738
2342
|
repoExists = async (repoName) => {
|
|
1739
2343
|
try {
|
|
1740
|
-
|
|
1741
|
-
await
|
|
2344
|
+
validateRepoName2(repoName);
|
|
2345
|
+
await execFileAsync4("gh", ["repo", "view", repoName, "--json", "name"]);
|
|
1742
2346
|
return true;
|
|
1743
2347
|
} catch {
|
|
1744
2348
|
return false;
|
|
@@ -1838,7 +2442,7 @@ var init_github = __esm({
|
|
|
1838
2442
|
`Use a different name or run \`tuck init --remote ${fullName}\``
|
|
1839
2443
|
]);
|
|
1840
2444
|
}
|
|
1841
|
-
|
|
2445
|
+
validateRepoName2(options.name);
|
|
1842
2446
|
if (options.description && /[;&|`$(){}[\]<>!#*?]/.test(options.description)) {
|
|
1843
2447
|
throw new GitHubCliError("Invalid description: contains unsafe characters");
|
|
1844
2448
|
}
|
|
@@ -1859,7 +2463,7 @@ var init_github = __esm({
|
|
|
1859
2463
|
args.push("--homepage", options.homepage);
|
|
1860
2464
|
}
|
|
1861
2465
|
args.push("--confirm", "--json", "name,url,sshUrl");
|
|
1862
|
-
const { stdout } = await
|
|
2466
|
+
const { stdout } = await execFileAsync4("gh", args);
|
|
1863
2467
|
const result = JSON.parse(stdout);
|
|
1864
2468
|
return {
|
|
1865
2469
|
name: result.name,
|
|
@@ -1877,7 +2481,7 @@ var init_github = __esm({
|
|
|
1877
2481
|
};
|
|
1878
2482
|
getPreferredRemoteProtocol = async () => {
|
|
1879
2483
|
try {
|
|
1880
|
-
const { stdout } = await
|
|
2484
|
+
const { stdout } = await execFileAsync4("gh", ["config", "get", "git_protocol"]);
|
|
1881
2485
|
const protocol = stdout.trim().toLowerCase();
|
|
1882
2486
|
return protocol === "ssh" ? "ssh" : "https";
|
|
1883
2487
|
} catch {
|
|
@@ -1886,8 +2490,8 @@ var init_github = __esm({
|
|
|
1886
2490
|
};
|
|
1887
2491
|
getRepoInfo = async (repoName) => {
|
|
1888
2492
|
try {
|
|
1889
|
-
|
|
1890
|
-
const { stdout } = await
|
|
2493
|
+
validateRepoName2(repoName);
|
|
2494
|
+
const { stdout } = await execFileAsync4("gh", [
|
|
1891
2495
|
"repo",
|
|
1892
2496
|
"view",
|
|
1893
2497
|
repoName,
|
|
@@ -1911,9 +2515,9 @@ var init_github = __esm({
|
|
|
1911
2515
|
if (!await isGhInstalled()) {
|
|
1912
2516
|
throw new GitHubCliError("GitHub CLI is not installed");
|
|
1913
2517
|
}
|
|
1914
|
-
|
|
2518
|
+
validateRepoName2(repoName);
|
|
1915
2519
|
try {
|
|
1916
|
-
await
|
|
2520
|
+
await execFileAsync4("gh", ["repo", "clone", repoName, targetDir]);
|
|
1917
2521
|
} catch (error) {
|
|
1918
2522
|
throw new GitHubCliError(`Failed to clone repository "${repoName}"`, [
|
|
1919
2523
|
String(error),
|
|
@@ -1995,7 +2599,7 @@ var init_github = __esm({
|
|
|
1995
2599
|
testSSHConnection = async () => {
|
|
1996
2600
|
try {
|
|
1997
2601
|
const strictHostKeyChecking = await getStrictHostKeyCheckingOption();
|
|
1998
|
-
const { stderr } = await
|
|
2602
|
+
const { stderr } = await execFileAsync4("ssh", ["-T", "-o", `StrictHostKeyChecking=${strictHostKeyChecking}`, "git@github.com"]);
|
|
1999
2603
|
const match = stderr.match(/Hi ([^!]+)!/);
|
|
2000
2604
|
if (match) {
|
|
2001
2605
|
return { success: true, username: match[1] };
|
|
@@ -2213,7 +2817,7 @@ Learn more: https://cli.github.com/
|
|
|
2213
2817
|
configureGitCredentialHelper = async () => {
|
|
2214
2818
|
const { platform: platform2 } = process;
|
|
2215
2819
|
try {
|
|
2216
|
-
const { stdout } = await
|
|
2820
|
+
const { stdout } = await execFileAsync4("git", ["config", "--global", "credential.helper"]);
|
|
2217
2821
|
if (stdout.trim()) {
|
|
2218
2822
|
return;
|
|
2219
2823
|
}
|
|
@@ -2221,21 +2825,21 @@ Learn more: https://cli.github.com/
|
|
|
2221
2825
|
}
|
|
2222
2826
|
try {
|
|
2223
2827
|
if (platform2 === "darwin") {
|
|
2224
|
-
await
|
|
2828
|
+
await execFileAsync4("git", ["config", "--global", "credential.helper", "osxkeychain"]);
|
|
2225
2829
|
} else if (platform2 === "linux") {
|
|
2226
2830
|
try {
|
|
2227
|
-
await
|
|
2831
|
+
await execFileAsync4("git", ["config", "--global", "credential.helper", "libsecret"]);
|
|
2228
2832
|
} catch (error) {
|
|
2229
2833
|
console.info(
|
|
2230
2834
|
`git-credential-libsecret is not available; falling back to git credential cache helper with timeout of ${GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS} seconds.`
|
|
2231
2835
|
);
|
|
2232
|
-
await
|
|
2836
|
+
await execFileAsync4("git", ["config", "--global", "credential.helper", `cache --timeout=${GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS}`]);
|
|
2233
2837
|
}
|
|
2234
2838
|
} else if (platform2 === "win32") {
|
|
2235
|
-
await
|
|
2839
|
+
await execFileAsync4("git", ["config", "--global", "credential.helper", "manager"]);
|
|
2236
2840
|
}
|
|
2237
2841
|
} catch {
|
|
2238
|
-
await
|
|
2842
|
+
await execFileAsync4("git", ["config", "--global", "credential.helper", "store"]);
|
|
2239
2843
|
}
|
|
2240
2844
|
};
|
|
2241
2845
|
getCredentialsPath = async () => {
|
|
@@ -3551,7 +4155,7 @@ var init_backup = __esm({
|
|
|
3551
4155
|
|
|
3552
4156
|
// src/lib/hooks.ts
|
|
3553
4157
|
import { exec } from "child_process";
|
|
3554
|
-
import { promisify as
|
|
4158
|
+
import { promisify as promisify5 } from "util";
|
|
3555
4159
|
import chalk4 from "chalk";
|
|
3556
4160
|
var execAsync, runHook, runPreSyncHook, runPostSyncHook, runPreRestoreHook, runPostRestoreHook;
|
|
3557
4161
|
var init_hooks = __esm({
|
|
@@ -3560,7 +4164,7 @@ var init_hooks = __esm({
|
|
|
3560
4164
|
init_config();
|
|
3561
4165
|
init_logger();
|
|
3562
4166
|
init_prompts();
|
|
3563
|
-
execAsync =
|
|
4167
|
+
execAsync = promisify5(exec);
|
|
3564
4168
|
runHook = async (hookType, tuckDir, options) => {
|
|
3565
4169
|
if (options?.skipHooks) {
|
|
3566
4170
|
return { success: true, skipped: true };
|
|
@@ -5194,16 +5798,16 @@ var init_redactor = __esm({
|
|
|
5194
5798
|
});
|
|
5195
5799
|
|
|
5196
5800
|
// src/lib/secrets/external.ts
|
|
5197
|
-
import { execFile as
|
|
5198
|
-
import { promisify as
|
|
5801
|
+
import { execFile as execFile5 } from "child_process";
|
|
5802
|
+
import { promisify as promisify6 } from "util";
|
|
5199
5803
|
import { z as z4 } from "zod";
|
|
5200
|
-
var
|
|
5804
|
+
var execFileAsync5, gitleaksResultSchema, gitleaksOutputSchema, isGitleaksInstalled, isTrufflehogInstalled, mapGitleaksSeverity, generatePlaceholderFromRule, scanWithGitleaks, scanWithScanner;
|
|
5201
5805
|
var init_external = __esm({
|
|
5202
5806
|
"src/lib/secrets/external.ts"() {
|
|
5203
5807
|
"use strict";
|
|
5204
5808
|
init_scanner();
|
|
5205
5809
|
init_paths();
|
|
5206
|
-
|
|
5810
|
+
execFileAsync5 = promisify6(execFile5);
|
|
5207
5811
|
gitleaksResultSchema = z4.object({
|
|
5208
5812
|
Description: z4.string(),
|
|
5209
5813
|
StartLine: z4.number(),
|
|
@@ -5227,7 +5831,7 @@ var init_external = __esm({
|
|
|
5227
5831
|
gitleaksOutputSchema = z4.array(gitleaksResultSchema);
|
|
5228
5832
|
isGitleaksInstalled = async () => {
|
|
5229
5833
|
try {
|
|
5230
|
-
await
|
|
5834
|
+
await execFileAsync5("gitleaks", ["version"]);
|
|
5231
5835
|
return true;
|
|
5232
5836
|
} catch {
|
|
5233
5837
|
return false;
|
|
@@ -5235,7 +5839,7 @@ var init_external = __esm({
|
|
|
5235
5839
|
};
|
|
5236
5840
|
isTrufflehogInstalled = async () => {
|
|
5237
5841
|
try {
|
|
5238
|
-
await
|
|
5842
|
+
await execFileAsync5("trufflehog", ["--version"]);
|
|
5239
5843
|
return true;
|
|
5240
5844
|
} catch {
|
|
5241
5845
|
return false;
|
|
@@ -5285,7 +5889,7 @@ var init_external = __esm({
|
|
|
5285
5889
|
const batchResults = await Promise.all(
|
|
5286
5890
|
batch.map(async (filepath) => {
|
|
5287
5891
|
try {
|
|
5288
|
-
const { stdout, stderr } = await
|
|
5892
|
+
const { stdout, stderr } = await execFileAsync5("gitleaks", [
|
|
5289
5893
|
"detect",
|
|
5290
5894
|
"--source",
|
|
5291
5895
|
filepath,
|
|
@@ -6322,7 +6926,7 @@ var init_sync = __esm({
|
|
|
6322
6926
|
|
|
6323
6927
|
// src/index.ts
|
|
6324
6928
|
import { Command as Command16 } from "commander";
|
|
6325
|
-
import
|
|
6929
|
+
import chalk6 from "chalk";
|
|
6326
6930
|
|
|
6327
6931
|
// src/commands/init.ts
|
|
6328
6932
|
init_ui();
|
|
@@ -6330,55 +6934,1077 @@ init_paths();
|
|
|
6330
6934
|
init_config();
|
|
6331
6935
|
init_manifest();
|
|
6332
6936
|
init_git();
|
|
6333
|
-
init_github();
|
|
6334
|
-
init_detect();
|
|
6335
|
-
init_errors();
|
|
6336
|
-
init_constants();
|
|
6337
|
-
init_config_schema();
|
|
6338
|
-
init_fileTracking();
|
|
6339
6937
|
import { Command as Command2 } from "commander";
|
|
6340
6938
|
import { join as join8 } from "path";
|
|
6341
6939
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
6342
6940
|
import { ensureDir as ensureDir4 } from "fs-extra";
|
|
6343
|
-
import { copy as copy3 } from "fs-extra";
|
|
6344
|
-
import { tmpdir } from "os";
|
|
6345
|
-
import { readFile as readFile4, rm as rm2 } from "fs/promises";
|
|
6346
|
-
var GITIGNORE_TEMPLATE = `# OS generated files
|
|
6347
|
-
.DS_Store
|
|
6348
|
-
.DS_Store?
|
|
6349
|
-
._*
|
|
6350
|
-
.Spotlight-V100
|
|
6351
|
-
.Trashes
|
|
6352
|
-
ehthumbs.db
|
|
6353
|
-
Thumbs.db
|
|
6354
|
-
|
|
6355
|
-
# Backup files
|
|
6356
|
-
*.bak
|
|
6357
|
-
*.backup
|
|
6358
|
-
*~
|
|
6359
|
-
|
|
6360
|
-
# Secret files (add patterns for files you want to exclude)
|
|
6361
|
-
# *.secret
|
|
6362
|
-
# .env.local
|
|
6363
|
-
`;
|
|
6364
|
-
var trackFilesWithProgressInit = async (selectedPaths, tuckDir) => {
|
|
6365
|
-
const filesToTrack = selectedPaths.map((path) => ({
|
|
6366
|
-
path
|
|
6367
|
-
}));
|
|
6368
|
-
const result = await trackFilesWithProgress(filesToTrack, tuckDir, {
|
|
6369
|
-
showCategory: true,
|
|
6370
|
-
actionVerb: "Tracking"
|
|
6371
|
-
});
|
|
6372
|
-
return result.succeeded;
|
|
6373
|
-
};
|
|
6374
|
-
var README_TEMPLATE = (machine) => `# Dotfiles
|
|
6375
6941
|
|
|
6376
|
-
|
|
6942
|
+
// src/lib/providerSetup.ts
|
|
6943
|
+
init_ui();
|
|
6377
6944
|
|
|
6378
|
-
|
|
6379
|
-
|
|
6945
|
+
// src/lib/providers/index.ts
|
|
6946
|
+
init_types();
|
|
6947
|
+
init_gitlab();
|
|
6380
6948
|
|
|
6381
|
-
|
|
6949
|
+
// src/lib/providers/github.ts
|
|
6950
|
+
init_types();
|
|
6951
|
+
init_validation();
|
|
6952
|
+
import { execFile as execFile2 } from "child_process";
|
|
6953
|
+
import { promisify as promisify2 } from "util";
|
|
6954
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
6955
|
+
var COMMON_DOTFILE_REPO_NAMES2 = ["dotfiles", "tuck", ".dotfiles", "dot-files", "dots"];
|
|
6956
|
+
var GitHubProvider = class {
|
|
6957
|
+
mode = "github";
|
|
6958
|
+
displayName = "GitHub";
|
|
6959
|
+
cliName = "gh";
|
|
6960
|
+
requiresRemote = true;
|
|
6961
|
+
// -------------------------------------------------------------------------
|
|
6962
|
+
// Detection & Authentication
|
|
6963
|
+
// -------------------------------------------------------------------------
|
|
6964
|
+
async isCliInstalled() {
|
|
6965
|
+
try {
|
|
6966
|
+
await execFileAsync2("gh", ["--version"]);
|
|
6967
|
+
return true;
|
|
6968
|
+
} catch {
|
|
6969
|
+
return false;
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
async isAuthenticated() {
|
|
6973
|
+
try {
|
|
6974
|
+
const { stdout, stderr } = await execFileAsync2("gh", ["auth", "status"]);
|
|
6975
|
+
const output = (stderr || stdout || "").trim();
|
|
6976
|
+
return output.includes("Logged in");
|
|
6977
|
+
} catch (error) {
|
|
6978
|
+
if (error instanceof Error && "stderr" in error) {
|
|
6979
|
+
const stderr = error.stderr;
|
|
6980
|
+
return stderr.includes("Logged in");
|
|
6981
|
+
}
|
|
6982
|
+
return false;
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
async getUser() {
|
|
6986
|
+
if (!await this.isCliInstalled() || !await this.isAuthenticated()) {
|
|
6987
|
+
return null;
|
|
6988
|
+
}
|
|
6989
|
+
try {
|
|
6990
|
+
const { stdout } = await execFileAsync2("gh", [
|
|
6991
|
+
"api",
|
|
6992
|
+
"user",
|
|
6993
|
+
"--jq",
|
|
6994
|
+
".login, .name, .email"
|
|
6995
|
+
]);
|
|
6996
|
+
const lines = stdout.trim().split("\n");
|
|
6997
|
+
return {
|
|
6998
|
+
login: lines[0] || "",
|
|
6999
|
+
name: lines[1] !== "null" ? lines[1] : null,
|
|
7000
|
+
email: lines[2] !== "null" ? lines[2] : null
|
|
7001
|
+
};
|
|
7002
|
+
} catch {
|
|
7003
|
+
return null;
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
async detect() {
|
|
7007
|
+
const cliInstalled = await this.isCliInstalled();
|
|
7008
|
+
if (!cliInstalled) {
|
|
7009
|
+
return {
|
|
7010
|
+
mode: this.mode,
|
|
7011
|
+
displayName: this.displayName,
|
|
7012
|
+
available: false,
|
|
7013
|
+
authStatus: {
|
|
7014
|
+
cliInstalled: false,
|
|
7015
|
+
authenticated: false
|
|
7016
|
+
},
|
|
7017
|
+
unavailableReason: "GitHub CLI (gh) is not installed"
|
|
7018
|
+
};
|
|
7019
|
+
}
|
|
7020
|
+
const authenticated = await this.isAuthenticated();
|
|
7021
|
+
const user = authenticated ? await this.getUser() : void 0;
|
|
7022
|
+
return {
|
|
7023
|
+
mode: this.mode,
|
|
7024
|
+
displayName: this.displayName,
|
|
7025
|
+
available: authenticated,
|
|
7026
|
+
authStatus: {
|
|
7027
|
+
cliInstalled: true,
|
|
7028
|
+
authenticated,
|
|
7029
|
+
user: user || void 0
|
|
7030
|
+
},
|
|
7031
|
+
unavailableReason: !authenticated ? "Not logged in to GitHub CLI" : void 0
|
|
7032
|
+
};
|
|
7033
|
+
}
|
|
7034
|
+
// -------------------------------------------------------------------------
|
|
7035
|
+
// Repository Operations
|
|
7036
|
+
// -------------------------------------------------------------------------
|
|
7037
|
+
async repoExists(repoName) {
|
|
7038
|
+
this.validateRepoName(repoName);
|
|
7039
|
+
try {
|
|
7040
|
+
await execFileAsync2("gh", ["repo", "view", repoName, "--json", "name"]);
|
|
7041
|
+
return true;
|
|
7042
|
+
} catch {
|
|
7043
|
+
return false;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
async createRepo(options) {
|
|
7047
|
+
if (!await this.isCliInstalled()) {
|
|
7048
|
+
throw new ProviderError("GitHub CLI is not installed", "github", [
|
|
7049
|
+
"Install with: brew install gh (macOS) or see https://cli.github.com/"
|
|
7050
|
+
]);
|
|
7051
|
+
}
|
|
7052
|
+
if (!await this.isAuthenticated()) {
|
|
7053
|
+
throw new ProviderError("Not authenticated with GitHub CLI", "github", [
|
|
7054
|
+
"Run: gh auth login"
|
|
7055
|
+
]);
|
|
7056
|
+
}
|
|
7057
|
+
const user = await this.getUser();
|
|
7058
|
+
if (!user) {
|
|
7059
|
+
throw new ProviderError("Could not get GitHub user information", "github");
|
|
7060
|
+
}
|
|
7061
|
+
this.validateRepoName(options.name);
|
|
7062
|
+
if (options.description) {
|
|
7063
|
+
try {
|
|
7064
|
+
validateDescription(options.description, 350);
|
|
7065
|
+
} catch (error) {
|
|
7066
|
+
throw new ProviderError(
|
|
7067
|
+
error instanceof Error ? error.message : "Invalid description",
|
|
7068
|
+
"github"
|
|
7069
|
+
);
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
const fullName = `${user.login}/${options.name}`;
|
|
7073
|
+
if (await this.repoExists(fullName)) {
|
|
7074
|
+
throw new ProviderError(`Repository "${fullName}" already exists`, "github", [
|
|
7075
|
+
`Use a different name or import the existing repo`
|
|
7076
|
+
]);
|
|
7077
|
+
}
|
|
7078
|
+
const args = ["repo", "create", options.name];
|
|
7079
|
+
if (options.isPrivate !== false) {
|
|
7080
|
+
args.push("--private");
|
|
7081
|
+
} else {
|
|
7082
|
+
args.push("--public");
|
|
7083
|
+
}
|
|
7084
|
+
if (options.description) {
|
|
7085
|
+
args.push("--description", options.description);
|
|
7086
|
+
}
|
|
7087
|
+
args.push("--confirm", "--json", "name,url,sshUrl");
|
|
7088
|
+
try {
|
|
7089
|
+
const { stdout } = await execFileAsync2("gh", args);
|
|
7090
|
+
const result = JSON.parse(stdout);
|
|
7091
|
+
return {
|
|
7092
|
+
name: result.name,
|
|
7093
|
+
fullName: `${user.login}/${result.name}`,
|
|
7094
|
+
url: result.url,
|
|
7095
|
+
sshUrl: result.sshUrl,
|
|
7096
|
+
httpsUrl: result.url,
|
|
7097
|
+
isPrivate: options.isPrivate !== false
|
|
7098
|
+
};
|
|
7099
|
+
} catch (error) {
|
|
7100
|
+
const sanitizedMessage = sanitizeErrorMessage(error, "Failed to create repository");
|
|
7101
|
+
throw new ProviderError(sanitizedMessage, "github", [
|
|
7102
|
+
"Try creating the repository manually at github.com/new"
|
|
7103
|
+
]);
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
async getRepoInfo(repoName) {
|
|
7107
|
+
this.validateRepoName(repoName);
|
|
7108
|
+
try {
|
|
7109
|
+
const { stdout } = await execFileAsync2("gh", [
|
|
7110
|
+
"repo",
|
|
7111
|
+
"view",
|
|
7112
|
+
repoName,
|
|
7113
|
+
"--json",
|
|
7114
|
+
"name,url,sshUrl,isPrivate,owner"
|
|
7115
|
+
]);
|
|
7116
|
+
const result = JSON.parse(stdout);
|
|
7117
|
+
return {
|
|
7118
|
+
name: result.name,
|
|
7119
|
+
fullName: `${result.owner.login}/${result.name}`,
|
|
7120
|
+
url: result.url,
|
|
7121
|
+
sshUrl: result.sshUrl,
|
|
7122
|
+
httpsUrl: result.url,
|
|
7123
|
+
isPrivate: result.isPrivate
|
|
7124
|
+
};
|
|
7125
|
+
} catch {
|
|
7126
|
+
return null;
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
async cloneRepo(repoName, targetDir) {
|
|
7130
|
+
if (!await this.isCliInstalled()) {
|
|
7131
|
+
throw new ProviderError("GitHub CLI is not installed", "github");
|
|
7132
|
+
}
|
|
7133
|
+
this.validateRepoName(repoName);
|
|
7134
|
+
try {
|
|
7135
|
+
await execFileAsync2("gh", ["repo", "clone", repoName, targetDir]);
|
|
7136
|
+
} catch (error) {
|
|
7137
|
+
throw new ProviderError(`Failed to clone repository "${repoName}"`, "github", [
|
|
7138
|
+
String(error),
|
|
7139
|
+
"Check that the repository exists and you have access"
|
|
7140
|
+
]);
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
async findDotfilesRepo(username) {
|
|
7144
|
+
const user = username || (await this.getUser())?.login;
|
|
7145
|
+
if (!user) return null;
|
|
7146
|
+
for (const name of COMMON_DOTFILE_REPO_NAMES2) {
|
|
7147
|
+
const repoName = `${user}/${name}`;
|
|
7148
|
+
if (await this.repoExists(repoName)) {
|
|
7149
|
+
return repoName;
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
return null;
|
|
7153
|
+
}
|
|
7154
|
+
// -------------------------------------------------------------------------
|
|
7155
|
+
// URL Utilities
|
|
7156
|
+
// -------------------------------------------------------------------------
|
|
7157
|
+
async getPreferredRepoUrl(repo) {
|
|
7158
|
+
const protocol = await this.getPreferredProtocol();
|
|
7159
|
+
return protocol === "ssh" ? repo.sshUrl : repo.httpsUrl;
|
|
7160
|
+
}
|
|
7161
|
+
validateUrl(url) {
|
|
7162
|
+
return url.startsWith("https://github.com/") || url.startsWith("git@github.com:") || url.startsWith("ssh://git@github.com/");
|
|
7163
|
+
}
|
|
7164
|
+
buildRepoUrl(username, repoName, protocol) {
|
|
7165
|
+
if (protocol === "ssh") {
|
|
7166
|
+
return `git@github.com:${username}/${repoName}.git`;
|
|
7167
|
+
}
|
|
7168
|
+
return `https://github.com/${username}/${repoName}.git`;
|
|
7169
|
+
}
|
|
7170
|
+
// -------------------------------------------------------------------------
|
|
7171
|
+
// Instructions
|
|
7172
|
+
// -------------------------------------------------------------------------
|
|
7173
|
+
getSetupInstructions() {
|
|
7174
|
+
const { platform: platform2 } = process;
|
|
7175
|
+
let installCmd = "";
|
|
7176
|
+
if (platform2 === "darwin") {
|
|
7177
|
+
installCmd = "brew install gh";
|
|
7178
|
+
} else if (platform2 === "linux") {
|
|
7179
|
+
installCmd = `# Debian/Ubuntu:
|
|
7180
|
+
sudo apt install gh
|
|
7181
|
+
|
|
7182
|
+
# Fedora:
|
|
7183
|
+
sudo dnf install gh
|
|
7184
|
+
|
|
7185
|
+
# Arch Linux:
|
|
7186
|
+
sudo pacman -S github-cli`;
|
|
7187
|
+
} else if (platform2 === "win32") {
|
|
7188
|
+
installCmd = `# Using winget:
|
|
7189
|
+
winget install GitHub.cli
|
|
7190
|
+
|
|
7191
|
+
# Using scoop:
|
|
7192
|
+
scoop install gh`;
|
|
7193
|
+
}
|
|
7194
|
+
return `GitHub CLI (gh) - Recommended for the best experience
|
|
7195
|
+
|
|
7196
|
+
Installation:
|
|
7197
|
+
${installCmd}
|
|
7198
|
+
|
|
7199
|
+
After installing, authenticate:
|
|
7200
|
+
gh auth login
|
|
7201
|
+
|
|
7202
|
+
Benefits:
|
|
7203
|
+
- Automatic repository creation
|
|
7204
|
+
- No manual token management
|
|
7205
|
+
- Easy authentication refresh
|
|
7206
|
+
|
|
7207
|
+
Learn more: https://cli.github.com/`;
|
|
7208
|
+
}
|
|
7209
|
+
getAltAuthInstructions() {
|
|
7210
|
+
return `Alternative authentication methods for GitHub:
|
|
7211
|
+
|
|
7212
|
+
1. SSH Keys (recommended if gh CLI unavailable)
|
|
7213
|
+
- Generate: ssh-keygen -t ed25519
|
|
7214
|
+
- Add to GitHub: https://github.com/settings/ssh/new
|
|
7215
|
+
- Test: ssh -T git@github.com
|
|
7216
|
+
|
|
7217
|
+
2. Personal Access Token
|
|
7218
|
+
- Create at: https://github.com/settings/tokens
|
|
7219
|
+
- Required scope: "repo" for private repositories
|
|
7220
|
+
- Use as password when pushing
|
|
7221
|
+
|
|
7222
|
+
For detailed instructions, see:
|
|
7223
|
+
https://docs.github.com/en/authentication`;
|
|
7224
|
+
}
|
|
7225
|
+
// -------------------------------------------------------------------------
|
|
7226
|
+
// Private Helpers
|
|
7227
|
+
// -------------------------------------------------------------------------
|
|
7228
|
+
validateRepoName(repoName) {
|
|
7229
|
+
try {
|
|
7230
|
+
validateRepoName(repoName, "github");
|
|
7231
|
+
} catch (error) {
|
|
7232
|
+
throw new ProviderError(
|
|
7233
|
+
error instanceof Error ? error.message : "Invalid repository name",
|
|
7234
|
+
"github"
|
|
7235
|
+
);
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
async getPreferredProtocol() {
|
|
7239
|
+
try {
|
|
7240
|
+
const { stdout } = await execFileAsync2("gh", ["config", "get", "git_protocol"]);
|
|
7241
|
+
return stdout.trim().toLowerCase() === "ssh" ? "ssh" : "https";
|
|
7242
|
+
} catch {
|
|
7243
|
+
return "https";
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
};
|
|
7247
|
+
var githubProvider = new GitHubProvider();
|
|
7248
|
+
|
|
7249
|
+
// src/lib/providers/local.ts
|
|
7250
|
+
init_types();
|
|
7251
|
+
var LocalProvider = class {
|
|
7252
|
+
mode = "local";
|
|
7253
|
+
displayName = "Local Only";
|
|
7254
|
+
cliName = null;
|
|
7255
|
+
requiresRemote = false;
|
|
7256
|
+
// -------------------------------------------------------------------------
|
|
7257
|
+
// Detection & Authentication
|
|
7258
|
+
// -------------------------------------------------------------------------
|
|
7259
|
+
async isCliInstalled() {
|
|
7260
|
+
return true;
|
|
7261
|
+
}
|
|
7262
|
+
async isAuthenticated() {
|
|
7263
|
+
return true;
|
|
7264
|
+
}
|
|
7265
|
+
async getUser() {
|
|
7266
|
+
return null;
|
|
7267
|
+
}
|
|
7268
|
+
async detect() {
|
|
7269
|
+
return {
|
|
7270
|
+
mode: this.mode,
|
|
7271
|
+
displayName: this.displayName,
|
|
7272
|
+
available: true,
|
|
7273
|
+
authStatus: {
|
|
7274
|
+
cliInstalled: true,
|
|
7275
|
+
authenticated: true
|
|
7276
|
+
}
|
|
7277
|
+
};
|
|
7278
|
+
}
|
|
7279
|
+
// -------------------------------------------------------------------------
|
|
7280
|
+
// Repository Operations
|
|
7281
|
+
// -------------------------------------------------------------------------
|
|
7282
|
+
async repoExists(_repoName) {
|
|
7283
|
+
throw new LocalModeError("check if remote repository exists");
|
|
7284
|
+
}
|
|
7285
|
+
async createRepo(_options) {
|
|
7286
|
+
throw new LocalModeError("create remote repository");
|
|
7287
|
+
}
|
|
7288
|
+
async getRepoInfo(_repoName) {
|
|
7289
|
+
throw new LocalModeError("get remote repository info");
|
|
7290
|
+
}
|
|
7291
|
+
async cloneRepo(_repoName, _targetDir) {
|
|
7292
|
+
throw new LocalModeError("clone remote repository");
|
|
7293
|
+
}
|
|
7294
|
+
async findDotfilesRepo(_username) {
|
|
7295
|
+
return null;
|
|
7296
|
+
}
|
|
7297
|
+
// -------------------------------------------------------------------------
|
|
7298
|
+
// URL Utilities
|
|
7299
|
+
// -------------------------------------------------------------------------
|
|
7300
|
+
async getPreferredRepoUrl(_repo) {
|
|
7301
|
+
throw new LocalModeError("get remote repository URL");
|
|
7302
|
+
}
|
|
7303
|
+
validateUrl(_url) {
|
|
7304
|
+
return false;
|
|
7305
|
+
}
|
|
7306
|
+
buildRepoUrl(_username, _repoName, _protocol) {
|
|
7307
|
+
throw new LocalModeError("build remote repository URL");
|
|
7308
|
+
}
|
|
7309
|
+
// -------------------------------------------------------------------------
|
|
7310
|
+
// Instructions
|
|
7311
|
+
// -------------------------------------------------------------------------
|
|
7312
|
+
getSetupInstructions() {
|
|
7313
|
+
return `Local Only Mode
|
|
7314
|
+
|
|
7315
|
+
Your dotfiles are stored in a local git repository without remote sync.
|
|
7316
|
+
This is useful for:
|
|
7317
|
+
- Testing tuck before setting up a remote
|
|
7318
|
+
- Machines that don't need cloud backup
|
|
7319
|
+
- Air-gapped or offline environments
|
|
7320
|
+
|
|
7321
|
+
Your dotfiles are still version controlled with git, so you can:
|
|
7322
|
+
- Track changes over time
|
|
7323
|
+
- Restore previous versions
|
|
7324
|
+
- Manually push to a remote later
|
|
7325
|
+
|
|
7326
|
+
To enable remote sync later, run:
|
|
7327
|
+
tuck config remote
|
|
7328
|
+
|
|
7329
|
+
This will guide you through setting up GitHub, GitLab, or another provider.`;
|
|
7330
|
+
}
|
|
7331
|
+
getAltAuthInstructions() {
|
|
7332
|
+
return `To sync your dotfiles to a remote, you'll need to configure a provider.
|
|
7333
|
+
|
|
7334
|
+
Run: tuck config remote
|
|
7335
|
+
|
|
7336
|
+
Available options:
|
|
7337
|
+
- GitHub (recommended) - via gh CLI
|
|
7338
|
+
- GitLab - via glab CLI
|
|
7339
|
+
- Custom - any git remote URL`;
|
|
7340
|
+
}
|
|
7341
|
+
};
|
|
7342
|
+
var localProvider = new LocalProvider();
|
|
7343
|
+
|
|
7344
|
+
// src/lib/providers/custom.ts
|
|
7345
|
+
init_types();
|
|
7346
|
+
init_validation();
|
|
7347
|
+
import { execFile as execFile3 } from "child_process";
|
|
7348
|
+
import { promisify as promisify3 } from "util";
|
|
7349
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
7350
|
+
var CustomProvider = class _CustomProvider {
|
|
7351
|
+
mode = "custom";
|
|
7352
|
+
displayName = "Custom Git Remote";
|
|
7353
|
+
cliName = null;
|
|
7354
|
+
requiresRemote = true;
|
|
7355
|
+
/** The custom remote URL */
|
|
7356
|
+
remoteUrl;
|
|
7357
|
+
constructor(remoteUrl) {
|
|
7358
|
+
this.remoteUrl = remoteUrl;
|
|
7359
|
+
}
|
|
7360
|
+
/** Create a provider with a specific URL */
|
|
7361
|
+
static withUrl(url) {
|
|
7362
|
+
const provider = new _CustomProvider(url);
|
|
7363
|
+
return provider;
|
|
7364
|
+
}
|
|
7365
|
+
/** Set the remote URL */
|
|
7366
|
+
setRemoteUrl(url) {
|
|
7367
|
+
this.remoteUrl = url;
|
|
7368
|
+
}
|
|
7369
|
+
/** Get the configured remote URL */
|
|
7370
|
+
getRemoteUrl() {
|
|
7371
|
+
return this.remoteUrl;
|
|
7372
|
+
}
|
|
7373
|
+
// -------------------------------------------------------------------------
|
|
7374
|
+
// Detection & Authentication
|
|
7375
|
+
// -------------------------------------------------------------------------
|
|
7376
|
+
async isCliInstalled() {
|
|
7377
|
+
try {
|
|
7378
|
+
await execFileAsync3("git", ["--version"]);
|
|
7379
|
+
return true;
|
|
7380
|
+
} catch {
|
|
7381
|
+
return false;
|
|
7382
|
+
}
|
|
7383
|
+
}
|
|
7384
|
+
async isAuthenticated() {
|
|
7385
|
+
return true;
|
|
7386
|
+
}
|
|
7387
|
+
async getUser() {
|
|
7388
|
+
try {
|
|
7389
|
+
const { stdout: name } = await execFileAsync3("git", ["config", "--global", "user.name"]);
|
|
7390
|
+
const { stdout: email } = await execFileAsync3("git", ["config", "--global", "user.email"]);
|
|
7391
|
+
const userName = name.trim();
|
|
7392
|
+
const userEmail = email.trim();
|
|
7393
|
+
if (userName || userEmail) {
|
|
7394
|
+
return {
|
|
7395
|
+
login: userName || userEmail.split("@")[0] || "user",
|
|
7396
|
+
name: userName || null,
|
|
7397
|
+
email: userEmail || null
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
} catch {
|
|
7401
|
+
}
|
|
7402
|
+
return null;
|
|
7403
|
+
}
|
|
7404
|
+
async detect() {
|
|
7405
|
+
const gitInstalled = await this.isCliInstalled();
|
|
7406
|
+
return {
|
|
7407
|
+
mode: this.mode,
|
|
7408
|
+
displayName: this.displayName,
|
|
7409
|
+
available: gitInstalled,
|
|
7410
|
+
authStatus: {
|
|
7411
|
+
cliInstalled: gitInstalled,
|
|
7412
|
+
authenticated: true
|
|
7413
|
+
// Assume authenticated for custom
|
|
7414
|
+
},
|
|
7415
|
+
unavailableReason: !gitInstalled ? "Git is not installed" : void 0
|
|
7416
|
+
};
|
|
7417
|
+
}
|
|
7418
|
+
// -------------------------------------------------------------------------
|
|
7419
|
+
// Repository Operations
|
|
7420
|
+
// -------------------------------------------------------------------------
|
|
7421
|
+
async repoExists(repoUrl) {
|
|
7422
|
+
try {
|
|
7423
|
+
await execFileAsync3("git", ["ls-remote", repoUrl], {
|
|
7424
|
+
timeout: GIT_OPERATION_TIMEOUTS.LS_REMOTE
|
|
7425
|
+
});
|
|
7426
|
+
return true;
|
|
7427
|
+
} catch {
|
|
7428
|
+
return false;
|
|
7429
|
+
}
|
|
7430
|
+
}
|
|
7431
|
+
async createRepo(_options) {
|
|
7432
|
+
throw new ProviderError("Cannot create repositories with custom provider", "custom", [
|
|
7433
|
+
"Create your repository manually on your git hosting service",
|
|
7434
|
+
"Then use: tuck init --remote <your-repo-url>"
|
|
7435
|
+
]);
|
|
7436
|
+
}
|
|
7437
|
+
async getRepoInfo(repoUrl) {
|
|
7438
|
+
const name = this.extractRepoName(repoUrl);
|
|
7439
|
+
const exists = await this.repoExists(repoUrl);
|
|
7440
|
+
if (!exists) {
|
|
7441
|
+
return null;
|
|
7442
|
+
}
|
|
7443
|
+
return {
|
|
7444
|
+
name,
|
|
7445
|
+
fullName: name,
|
|
7446
|
+
url: repoUrl,
|
|
7447
|
+
sshUrl: repoUrl.startsWith("git@") ? repoUrl : "",
|
|
7448
|
+
httpsUrl: repoUrl.startsWith("http") ? repoUrl : "",
|
|
7449
|
+
isPrivate: true
|
|
7450
|
+
// Assume private, we can't know
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
async cloneRepo(repoUrl, targetDir) {
|
|
7454
|
+
try {
|
|
7455
|
+
await execFileAsync3("git", ["clone", repoUrl, targetDir], {
|
|
7456
|
+
timeout: GIT_OPERATION_TIMEOUTS.CLONE,
|
|
7457
|
+
maxBuffer: 10 * 1024 * 1024
|
|
7458
|
+
// 10MB output limit
|
|
7459
|
+
});
|
|
7460
|
+
} catch (error) {
|
|
7461
|
+
if (error && typeof error === "object" && "killed" in error && error.killed) {
|
|
7462
|
+
throw new ProviderError("Clone operation timed out", "custom", [
|
|
7463
|
+
"The repository may be too large or the connection is too slow",
|
|
7464
|
+
"Try using git clone directly for large repositories"
|
|
7465
|
+
]);
|
|
7466
|
+
}
|
|
7467
|
+
const sanitizedMessage = sanitizeErrorMessage(error, "Failed to clone repository");
|
|
7468
|
+
throw new ProviderError(sanitizedMessage, "custom", [
|
|
7469
|
+
"Check that the URL is correct and you have access",
|
|
7470
|
+
"You may need to set up SSH keys or credentials"
|
|
7471
|
+
]);
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
async findDotfilesRepo(_username) {
|
|
7475
|
+
return null;
|
|
7476
|
+
}
|
|
7477
|
+
// -------------------------------------------------------------------------
|
|
7478
|
+
// URL Utilities
|
|
7479
|
+
// -------------------------------------------------------------------------
|
|
7480
|
+
async getPreferredRepoUrl(repo) {
|
|
7481
|
+
return repo.sshUrl || repo.httpsUrl || repo.url;
|
|
7482
|
+
}
|
|
7483
|
+
validateUrl(url) {
|
|
7484
|
+
return validateGitUrl(url);
|
|
7485
|
+
}
|
|
7486
|
+
buildRepoUrl(_username, _repoName, _protocol) {
|
|
7487
|
+
throw new ProviderError("Cannot build repository URLs for custom provider", "custom", [
|
|
7488
|
+
"Please provide the full repository URL"
|
|
7489
|
+
]);
|
|
7490
|
+
}
|
|
7491
|
+
// -------------------------------------------------------------------------
|
|
7492
|
+
// Instructions
|
|
7493
|
+
// -------------------------------------------------------------------------
|
|
7494
|
+
getSetupInstructions() {
|
|
7495
|
+
return `Custom Git Remote
|
|
7496
|
+
|
|
7497
|
+
Use any git hosting service by providing the repository URL directly.
|
|
7498
|
+
|
|
7499
|
+
Supported URL formats:
|
|
7500
|
+
- HTTPS: https://git.example.com/user/repo.git
|
|
7501
|
+
- SSH: git@git.example.com:user/repo.git
|
|
7502
|
+
|
|
7503
|
+
Steps:
|
|
7504
|
+
1. Create a repository on your git hosting service
|
|
7505
|
+
2. Copy the clone URL (SSH or HTTPS)
|
|
7506
|
+
3. Run: tuck init --remote <your-repo-url>
|
|
7507
|
+
|
|
7508
|
+
Note: You'll need to handle authentication separately:
|
|
7509
|
+
- For SSH: Set up SSH keys with your hosting service
|
|
7510
|
+
- For HTTPS: Configure git credentials or use a credential helper`;
|
|
7511
|
+
}
|
|
7512
|
+
getAltAuthInstructions() {
|
|
7513
|
+
return `Authentication for Custom Git Remotes
|
|
7514
|
+
|
|
7515
|
+
For SSH URLs (git@...):
|
|
7516
|
+
1. Generate an SSH key: ssh-keygen -t ed25519
|
|
7517
|
+
2. Add the public key to your git hosting service
|
|
7518
|
+
3. Test: ssh -T git@your-host.com
|
|
7519
|
+
|
|
7520
|
+
For HTTPS URLs:
|
|
7521
|
+
1. Create a personal access token on your hosting service
|
|
7522
|
+
2. Configure git credential helper:
|
|
7523
|
+
git config --global credential.helper store
|
|
7524
|
+
3. On first push, enter your token as password
|
|
7525
|
+
|
|
7526
|
+
Or use git credential manager for more secure storage.`;
|
|
7527
|
+
}
|
|
7528
|
+
// -------------------------------------------------------------------------
|
|
7529
|
+
// Private Helpers
|
|
7530
|
+
// -------------------------------------------------------------------------
|
|
7531
|
+
extractRepoName(url) {
|
|
7532
|
+
let name = url.replace(/\.git$/, "");
|
|
7533
|
+
if (name.includes(":") && !name.includes("://")) {
|
|
7534
|
+
name = name.split(":").pop() || name;
|
|
7535
|
+
}
|
|
7536
|
+
if (name.includes("://")) {
|
|
7537
|
+
const urlParts = name.split("/");
|
|
7538
|
+
name = urlParts.slice(3).join("/");
|
|
7539
|
+
}
|
|
7540
|
+
const parts = name.split("/");
|
|
7541
|
+
return parts[parts.length - 1] || "repository";
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7544
|
+
var customProvider = new CustomProvider();
|
|
7545
|
+
|
|
7546
|
+
// src/lib/providers/index.ts
|
|
7547
|
+
init_types();
|
|
7548
|
+
init_gitlab();
|
|
7549
|
+
function getProvider(mode, config) {
|
|
7550
|
+
switch (mode) {
|
|
7551
|
+
case "github":
|
|
7552
|
+
return githubProvider;
|
|
7553
|
+
case "gitlab":
|
|
7554
|
+
if (config?.providerUrl) {
|
|
7555
|
+
const host = config.providerUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
7556
|
+
return GitLabProvider.forHost(host);
|
|
7557
|
+
}
|
|
7558
|
+
return gitlabProvider;
|
|
7559
|
+
case "local":
|
|
7560
|
+
return localProvider;
|
|
7561
|
+
case "custom":
|
|
7562
|
+
if (config?.url) {
|
|
7563
|
+
return CustomProvider.withUrl(config.url);
|
|
7564
|
+
}
|
|
7565
|
+
return customProvider;
|
|
7566
|
+
default:
|
|
7567
|
+
throw new ProviderNotConfiguredError(mode);
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
async function detectProviders() {
|
|
7571
|
+
const detections = await Promise.all([
|
|
7572
|
+
githubProvider.detect(),
|
|
7573
|
+
gitlabProvider.detect(),
|
|
7574
|
+
localProvider.detect(),
|
|
7575
|
+
customProvider.detect()
|
|
7576
|
+
]);
|
|
7577
|
+
return detections;
|
|
7578
|
+
}
|
|
7579
|
+
async function getProviderOptions() {
|
|
7580
|
+
const detections = await detectProviders();
|
|
7581
|
+
const detectionMap = /* @__PURE__ */ new Map();
|
|
7582
|
+
for (const detection of detections) {
|
|
7583
|
+
detectionMap.set(detection.mode, detection);
|
|
7584
|
+
}
|
|
7585
|
+
const github = detectionMap.get("github");
|
|
7586
|
+
const gitlab = detectionMap.get("gitlab");
|
|
7587
|
+
const custom = detectionMap.get("custom");
|
|
7588
|
+
const options = [
|
|
7589
|
+
{
|
|
7590
|
+
mode: "github",
|
|
7591
|
+
displayName: "GitHub",
|
|
7592
|
+
description: "Store dotfiles on GitHub (recommended)",
|
|
7593
|
+
available: github?.available ?? false,
|
|
7594
|
+
authStatus: {
|
|
7595
|
+
authenticated: github?.authStatus.authenticated ?? false,
|
|
7596
|
+
username: github?.authStatus.user?.login
|
|
7597
|
+
},
|
|
7598
|
+
unavailableReason: github?.unavailableReason
|
|
7599
|
+
},
|
|
7600
|
+
{
|
|
7601
|
+
mode: "gitlab",
|
|
7602
|
+
displayName: "GitLab",
|
|
7603
|
+
description: "Store dotfiles on GitLab (supports self-hosted)",
|
|
7604
|
+
available: gitlab?.available ?? false,
|
|
7605
|
+
authStatus: {
|
|
7606
|
+
authenticated: gitlab?.authStatus.authenticated ?? false,
|
|
7607
|
+
username: gitlab?.authStatus.user?.login
|
|
7608
|
+
},
|
|
7609
|
+
unavailableReason: gitlab?.unavailableReason
|
|
7610
|
+
},
|
|
7611
|
+
{
|
|
7612
|
+
mode: "local",
|
|
7613
|
+
displayName: "Local Only",
|
|
7614
|
+
description: "Track dotfiles locally without remote sync",
|
|
7615
|
+
available: true
|
|
7616
|
+
},
|
|
7617
|
+
{
|
|
7618
|
+
mode: "custom",
|
|
7619
|
+
displayName: "Custom Remote",
|
|
7620
|
+
description: "Use any git remote URL (Bitbucket, Gitea, etc.)",
|
|
7621
|
+
available: custom?.available ?? true
|
|
7622
|
+
}
|
|
7623
|
+
];
|
|
7624
|
+
return options.sort((a, b) => {
|
|
7625
|
+
if (a.authStatus?.authenticated && !b.authStatus?.authenticated) return -1;
|
|
7626
|
+
if (!a.authStatus?.authenticated && b.authStatus?.authenticated) return 1;
|
|
7627
|
+
if (a.available && !b.available) return -1;
|
|
7628
|
+
if (!a.available && b.available) return 1;
|
|
7629
|
+
return 0;
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7632
|
+
function describeProviderConfig(config) {
|
|
7633
|
+
switch (config.mode) {
|
|
7634
|
+
case "github":
|
|
7635
|
+
return config.username ? `GitHub (@${config.username})` : "GitHub";
|
|
7636
|
+
case "gitlab":
|
|
7637
|
+
if (config.providerUrl) {
|
|
7638
|
+
const host = config.providerUrl.replace(/^https?:\/\//, "");
|
|
7639
|
+
return config.username ? `GitLab ${host} (@${config.username})` : `GitLab (${host})`;
|
|
7640
|
+
}
|
|
7641
|
+
return config.username ? `GitLab (@${config.username})` : "GitLab";
|
|
7642
|
+
case "local":
|
|
7643
|
+
return "Local only (no remote sync)";
|
|
7644
|
+
case "custom":
|
|
7645
|
+
return config.url ? `Custom: ${config.url}` : "Custom remote";
|
|
7646
|
+
default:
|
|
7647
|
+
return "Unknown provider";
|
|
7648
|
+
}
|
|
7649
|
+
}
|
|
7650
|
+
function buildRemoteConfig(mode, options) {
|
|
7651
|
+
return {
|
|
7652
|
+
mode,
|
|
7653
|
+
url: options?.url,
|
|
7654
|
+
providerUrl: options?.providerUrl,
|
|
7655
|
+
username: options?.username,
|
|
7656
|
+
repoName: options?.repoName
|
|
7657
|
+
};
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
// src/lib/providerSetup.ts
|
|
7661
|
+
init_validation();
|
|
7662
|
+
function displayProviderStatus(options) {
|
|
7663
|
+
console.log();
|
|
7664
|
+
console.log(colors.brand(" Available Git Providers:"));
|
|
7665
|
+
console.log();
|
|
7666
|
+
for (const opt of options) {
|
|
7667
|
+
const status = getProviderStatusIcon(opt);
|
|
7668
|
+
const authInfo = opt.authStatus?.username ? colors.muted(` (@${opt.authStatus.username})`) : "";
|
|
7669
|
+
console.log(` ${status} ${opt.displayName}${authInfo}`);
|
|
7670
|
+
if (opt.unavailableReason) {
|
|
7671
|
+
console.log(colors.muted(` ${opt.unavailableReason}`));
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
console.log();
|
|
7675
|
+
}
|
|
7676
|
+
function getProviderStatusIcon(opt) {
|
|
7677
|
+
if (opt.authStatus?.authenticated) {
|
|
7678
|
+
return colors.success("\u2713");
|
|
7679
|
+
}
|
|
7680
|
+
if (opt.available) {
|
|
7681
|
+
return colors.warning("\u25CB");
|
|
7682
|
+
}
|
|
7683
|
+
return colors.muted("\u2717");
|
|
7684
|
+
}
|
|
7685
|
+
async function selectProvider() {
|
|
7686
|
+
const spinner4 = prompts.spinner();
|
|
7687
|
+
spinner4.start("Detecting available git providers...");
|
|
7688
|
+
const options = await getProviderOptions();
|
|
7689
|
+
spinner4.stop("Provider detection complete");
|
|
7690
|
+
displayProviderStatus(options);
|
|
7691
|
+
const selectOptions = options.map(
|
|
7692
|
+
(opt) => {
|
|
7693
|
+
let hint = opt.description;
|
|
7694
|
+
if (opt.authStatus?.authenticated && opt.authStatus.username) {
|
|
7695
|
+
hint = `Logged in as @${opt.authStatus.username}`;
|
|
7696
|
+
} else if (opt.unavailableReason) {
|
|
7697
|
+
hint = opt.unavailableReason;
|
|
7698
|
+
}
|
|
7699
|
+
return {
|
|
7700
|
+
value: opt.mode,
|
|
7701
|
+
label: opt.displayName,
|
|
7702
|
+
hint
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
);
|
|
7706
|
+
selectOptions.push({
|
|
7707
|
+
value: "skip",
|
|
7708
|
+
label: "Skip for now",
|
|
7709
|
+
hint: "Set up remote later with tuck config remote"
|
|
7710
|
+
});
|
|
7711
|
+
const selected = await prompts.select(
|
|
7712
|
+
"Where would you like to store your dotfiles?",
|
|
7713
|
+
selectOptions
|
|
7714
|
+
);
|
|
7715
|
+
if (selected === "skip") {
|
|
7716
|
+
return null;
|
|
7717
|
+
}
|
|
7718
|
+
return selected;
|
|
7719
|
+
}
|
|
7720
|
+
async function setupProvider(initialMode) {
|
|
7721
|
+
const mode = initialMode ?? await selectProvider();
|
|
7722
|
+
if (!mode) {
|
|
7723
|
+
return {
|
|
7724
|
+
success: true,
|
|
7725
|
+
mode: "local",
|
|
7726
|
+
config: buildRemoteConfig("local"),
|
|
7727
|
+
provider: getProvider("local")
|
|
7728
|
+
};
|
|
7729
|
+
}
|
|
7730
|
+
const provider = getProvider(mode);
|
|
7731
|
+
switch (mode) {
|
|
7732
|
+
case "local":
|
|
7733
|
+
return await setupLocalProvider();
|
|
7734
|
+
case "github":
|
|
7735
|
+
return await setupGitHubProvider(provider);
|
|
7736
|
+
case "gitlab":
|
|
7737
|
+
return await setupGitLabProvider(provider);
|
|
7738
|
+
case "custom":
|
|
7739
|
+
return await setupCustomProvider(provider);
|
|
7740
|
+
default:
|
|
7741
|
+
prompts.log.error(`Unknown provider: ${mode}`);
|
|
7742
|
+
return null;
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
async function setupLocalProvider() {
|
|
7746
|
+
prompts.log.info("Your dotfiles will be tracked locally without remote sync.");
|
|
7747
|
+
prompts.log.info("You can set up a remote later with 'tuck config remote'.");
|
|
7748
|
+
return {
|
|
7749
|
+
success: true,
|
|
7750
|
+
mode: "local",
|
|
7751
|
+
config: buildRemoteConfig("local"),
|
|
7752
|
+
provider: getProvider("local")
|
|
7753
|
+
};
|
|
7754
|
+
}
|
|
7755
|
+
async function setupGitHubProvider(provider) {
|
|
7756
|
+
const detection = await provider.detect();
|
|
7757
|
+
if (!detection.authStatus.cliInstalled) {
|
|
7758
|
+
prompts.log.warning("GitHub CLI (gh) is not installed.");
|
|
7759
|
+
console.log();
|
|
7760
|
+
prompts.note(provider.getSetupInstructions(), "Installation Instructions");
|
|
7761
|
+
console.log();
|
|
7762
|
+
const altChoice = await prompts.select("How would you like to proceed?", [
|
|
7763
|
+
{ value: "install", label: "I will install gh CLI", hint: "Run tuck init again after" },
|
|
7764
|
+
{ value: "custom", label: "Use custom URL instead", hint: "Manual repository setup" },
|
|
7765
|
+
{ value: "local", label: "Stay local for now", hint: "No remote sync" }
|
|
7766
|
+
]);
|
|
7767
|
+
if (altChoice === "local") {
|
|
7768
|
+
return setupLocalProvider();
|
|
7769
|
+
}
|
|
7770
|
+
if (altChoice === "custom") {
|
|
7771
|
+
return setupCustomProvider(getProvider("custom"));
|
|
7772
|
+
}
|
|
7773
|
+
prompts.log.info("After installing, run 'tuck init' again.");
|
|
7774
|
+
return null;
|
|
7775
|
+
}
|
|
7776
|
+
if (!detection.authStatus.authenticated) {
|
|
7777
|
+
prompts.log.warning("GitHub CLI is installed but not authenticated.");
|
|
7778
|
+
const authChoice = await prompts.select("Would you like to authenticate now?", [
|
|
7779
|
+
{ value: "auth", label: "Run gh auth login", hint: "Authenticate via browser" },
|
|
7780
|
+
{ value: "custom", label: "Use custom URL instead", hint: "Manual repository setup" },
|
|
7781
|
+
{ value: "local", label: "Stay local for now", hint: "No remote sync" }
|
|
7782
|
+
]);
|
|
7783
|
+
if (authChoice === "local") {
|
|
7784
|
+
return setupLocalProvider();
|
|
7785
|
+
}
|
|
7786
|
+
if (authChoice === "custom") {
|
|
7787
|
+
return setupCustomProvider(getProvider("custom"));
|
|
7788
|
+
}
|
|
7789
|
+
prompts.log.info("Opening browser for GitHub authentication...");
|
|
7790
|
+
try {
|
|
7791
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
7792
|
+
const { promisify: promisify7 } = await import("util");
|
|
7793
|
+
const execFileAsync6 = promisify7(execFile6);
|
|
7794
|
+
await execFileAsync6("gh", ["auth", "login", "--web"]);
|
|
7795
|
+
const recheck = await provider.detect();
|
|
7796
|
+
if (!recheck.authStatus.authenticated) {
|
|
7797
|
+
prompts.log.warning("Authentication may have failed. Please try again.");
|
|
7798
|
+
return null;
|
|
7799
|
+
}
|
|
7800
|
+
prompts.log.success(`Authenticated as @${recheck.authStatus.user?.login}`);
|
|
7801
|
+
} catch (error) {
|
|
7802
|
+
prompts.log.error("Authentication failed. Please try again or run `gh auth login` manually.");
|
|
7803
|
+
return null;
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7806
|
+
const user = await provider.getUser();
|
|
7807
|
+
if (!user) {
|
|
7808
|
+
prompts.log.error("Could not get GitHub user information.");
|
|
7809
|
+
return null;
|
|
7810
|
+
}
|
|
7811
|
+
const confirmAccount = await prompts.confirm(`Use GitHub account @${user.login}?`, true);
|
|
7812
|
+
if (!confirmAccount) {
|
|
7813
|
+
prompts.log.info("Run 'gh auth logout' then 'gh auth login' to switch accounts.");
|
|
7814
|
+
return null;
|
|
7815
|
+
}
|
|
7816
|
+
return {
|
|
7817
|
+
success: true,
|
|
7818
|
+
mode: "github",
|
|
7819
|
+
config: buildRemoteConfig("github", { username: user.login }),
|
|
7820
|
+
provider
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
async function setupGitLabProvider(provider) {
|
|
7824
|
+
const hostType = await prompts.select("Which GitLab instance?", [
|
|
7825
|
+
{ value: "cloud", label: "gitlab.com", hint: "GitLab cloud service" },
|
|
7826
|
+
{ value: "self-hosted", label: "Self-hosted", hint: "Custom GitLab server" }
|
|
7827
|
+
]);
|
|
7828
|
+
let providerUrl;
|
|
7829
|
+
if (hostType === "self-hosted") {
|
|
7830
|
+
const host = await prompts.text("Enter your GitLab host:", {
|
|
7831
|
+
placeholder: "gitlab.example.com",
|
|
7832
|
+
validate: (value) => {
|
|
7833
|
+
try {
|
|
7834
|
+
validateHostname(value);
|
|
7835
|
+
return void 0;
|
|
7836
|
+
} catch (error) {
|
|
7837
|
+
return error instanceof Error ? error.message : "Invalid hostname";
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
});
|
|
7841
|
+
providerUrl = `https://${host}`;
|
|
7842
|
+
prompts.log.info(
|
|
7843
|
+
"For self-hosted instances with self-signed certificates, you may need to configure git to skip SSL verification"
|
|
7844
|
+
);
|
|
7845
|
+
const { GitLabProvider: GitLabProvider2 } = await Promise.resolve().then(() => (init_gitlab(), gitlab_exports));
|
|
7846
|
+
provider = GitLabProvider2.forHost(host);
|
|
7847
|
+
}
|
|
7848
|
+
const detection = await provider.detect();
|
|
7849
|
+
if (!detection.authStatus.cliInstalled) {
|
|
7850
|
+
prompts.log.warning("GitLab CLI (glab) is not installed.");
|
|
7851
|
+
console.log();
|
|
7852
|
+
prompts.note(provider.getSetupInstructions(), "Installation Instructions");
|
|
7853
|
+
console.log();
|
|
7854
|
+
const altChoice = await prompts.select("How would you like to proceed?", [
|
|
7855
|
+
{ value: "install", label: "I will install glab CLI", hint: "Run tuck init again after" },
|
|
7856
|
+
{ value: "custom", label: "Use custom URL instead", hint: "Manual repository setup" },
|
|
7857
|
+
{ value: "local", label: "Stay local for now", hint: "No remote sync" }
|
|
7858
|
+
]);
|
|
7859
|
+
if (altChoice === "local") {
|
|
7860
|
+
return setupLocalProvider();
|
|
7861
|
+
}
|
|
7862
|
+
if (altChoice === "custom") {
|
|
7863
|
+
return setupCustomProvider(getProvider("custom"));
|
|
7864
|
+
}
|
|
7865
|
+
prompts.log.info("After installing, run 'tuck init' again.");
|
|
7866
|
+
return null;
|
|
7867
|
+
}
|
|
7868
|
+
if (!detection.authStatus.authenticated) {
|
|
7869
|
+
prompts.log.warning(
|
|
7870
|
+
`GitLab CLI is installed but not authenticated${providerUrl ? ` for ${providerUrl}` : ""}.`
|
|
7871
|
+
);
|
|
7872
|
+
const authChoice = await prompts.select("Would you like to authenticate now?", [
|
|
7873
|
+
{ value: "auth", label: "Run glab auth login", hint: "Authenticate via browser" },
|
|
7874
|
+
{ value: "custom", label: "Use custom URL instead", hint: "Manual repository setup" },
|
|
7875
|
+
{ value: "local", label: "Stay local for now", hint: "No remote sync" }
|
|
7876
|
+
]);
|
|
7877
|
+
if (authChoice === "local") {
|
|
7878
|
+
return setupLocalProvider();
|
|
7879
|
+
}
|
|
7880
|
+
if (authChoice === "custom") {
|
|
7881
|
+
return setupCustomProvider(getProvider("custom"));
|
|
7882
|
+
}
|
|
7883
|
+
prompts.log.info("Opening browser for GitLab authentication...");
|
|
7884
|
+
try {
|
|
7885
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
7886
|
+
const { promisify: promisify7 } = await import("util");
|
|
7887
|
+
const execFileAsync6 = promisify7(execFile6);
|
|
7888
|
+
const args = ["auth", "login", "--web"];
|
|
7889
|
+
if (providerUrl) {
|
|
7890
|
+
args.push("-h", providerUrl.replace(/^https?:\/\//, ""));
|
|
7891
|
+
}
|
|
7892
|
+
await execFileAsync6("glab", args);
|
|
7893
|
+
const recheck = await provider.detect();
|
|
7894
|
+
if (!recheck.authStatus.authenticated) {
|
|
7895
|
+
prompts.log.warning("Authentication may have failed. Please try again.");
|
|
7896
|
+
return null;
|
|
7897
|
+
}
|
|
7898
|
+
prompts.log.success(`Authenticated as @${recheck.authStatus.user?.login}`);
|
|
7899
|
+
} catch (error) {
|
|
7900
|
+
prompts.log.error("Authentication failed. Please try again or run `glab auth login` manually.");
|
|
7901
|
+
return null;
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
const user = await provider.getUser();
|
|
7905
|
+
if (!user) {
|
|
7906
|
+
prompts.log.error("Could not get GitLab user information.");
|
|
7907
|
+
return null;
|
|
7908
|
+
}
|
|
7909
|
+
const confirmAccount = await prompts.confirm(
|
|
7910
|
+
`Use GitLab account @${user.login}${providerUrl ? ` on ${providerUrl}` : ""}?`,
|
|
7911
|
+
true
|
|
7912
|
+
);
|
|
7913
|
+
if (!confirmAccount) {
|
|
7914
|
+
prompts.log.info("Run 'glab auth logout' then 'glab auth login' to switch accounts.");
|
|
7915
|
+
return null;
|
|
7916
|
+
}
|
|
7917
|
+
return {
|
|
7918
|
+
success: true,
|
|
7919
|
+
mode: "gitlab",
|
|
7920
|
+
config: buildRemoteConfig("gitlab", { username: user.login, providerUrl }),
|
|
7921
|
+
provider
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7924
|
+
async function setupCustomProvider(provider) {
|
|
7925
|
+
prompts.log.info("You can use any git remote URL.");
|
|
7926
|
+
console.log();
|
|
7927
|
+
prompts.note(provider.getSetupInstructions(), "Custom Remote Setup");
|
|
7928
|
+
console.log();
|
|
7929
|
+
const hasRepo = await prompts.confirm("Do you have a repository URL ready?");
|
|
7930
|
+
if (!hasRepo) {
|
|
7931
|
+
prompts.log.info("Create a repository first, then run 'tuck config remote' to add it.");
|
|
7932
|
+
return setupLocalProvider();
|
|
7933
|
+
}
|
|
7934
|
+
const url = await prompts.text("Enter the repository URL:", {
|
|
7935
|
+
placeholder: "https://git.example.com/user/dotfiles.git",
|
|
7936
|
+
validate: (value) => {
|
|
7937
|
+
if (!value) return "URL is required";
|
|
7938
|
+
if (!provider.validateUrl(value)) {
|
|
7939
|
+
return "Invalid git URL format";
|
|
7940
|
+
}
|
|
7941
|
+
return void 0;
|
|
7942
|
+
}
|
|
7943
|
+
});
|
|
7944
|
+
return {
|
|
7945
|
+
success: true,
|
|
7946
|
+
mode: "custom",
|
|
7947
|
+
config: buildRemoteConfig("custom", { url }),
|
|
7948
|
+
provider,
|
|
7949
|
+
remoteUrl: url
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
function detectProviderFromUrl(url) {
|
|
7953
|
+
if (url.includes("github.com")) {
|
|
7954
|
+
return "github";
|
|
7955
|
+
}
|
|
7956
|
+
if (url.includes("gitlab.com") || url.includes("gitlab")) {
|
|
7957
|
+
return "gitlab";
|
|
7958
|
+
}
|
|
7959
|
+
return "custom";
|
|
7960
|
+
}
|
|
7961
|
+
|
|
7962
|
+
// src/commands/init.ts
|
|
7963
|
+
init_github();
|
|
7964
|
+
init_detect();
|
|
7965
|
+
init_errors();
|
|
7966
|
+
init_constants();
|
|
7967
|
+
init_config_schema();
|
|
7968
|
+
init_fileTracking();
|
|
7969
|
+
import { copy as copy3 } from "fs-extra";
|
|
7970
|
+
import { tmpdir } from "os";
|
|
7971
|
+
import { readFile as readFile4, rm as rm2 } from "fs/promises";
|
|
7972
|
+
var GITIGNORE_TEMPLATE = `# OS generated files
|
|
7973
|
+
.DS_Store
|
|
7974
|
+
.DS_Store?
|
|
7975
|
+
._*
|
|
7976
|
+
.Spotlight-V100
|
|
7977
|
+
.Trashes
|
|
7978
|
+
ehthumbs.db
|
|
7979
|
+
Thumbs.db
|
|
7980
|
+
|
|
7981
|
+
# Backup files
|
|
7982
|
+
*.bak
|
|
7983
|
+
*.backup
|
|
7984
|
+
*~
|
|
7985
|
+
|
|
7986
|
+
# Secret files (add patterns for files you want to exclude)
|
|
7987
|
+
# *.secret
|
|
7988
|
+
# .env.local
|
|
7989
|
+
`;
|
|
7990
|
+
var trackFilesWithProgressInit = async (selectedPaths, tuckDir) => {
|
|
7991
|
+
const filesToTrack = selectedPaths.map((path) => ({
|
|
7992
|
+
path
|
|
7993
|
+
}));
|
|
7994
|
+
const result = await trackFilesWithProgress(filesToTrack, tuckDir, {
|
|
7995
|
+
showCategory: true,
|
|
7996
|
+
actionVerb: "Tracking"
|
|
7997
|
+
});
|
|
7998
|
+
return result.succeeded;
|
|
7999
|
+
};
|
|
8000
|
+
var README_TEMPLATE = (machine) => `# Dotfiles
|
|
8001
|
+
|
|
8002
|
+
Managed with [tuck](https://github.com/Pranav-Karra-3301/tuck) - Modern Dotfiles Manager
|
|
8003
|
+
|
|
8004
|
+
${machine ? `## Machine: ${machine}
|
|
8005
|
+
` : ""}
|
|
8006
|
+
|
|
8007
|
+
## Quick Start
|
|
6382
8008
|
|
|
6383
8009
|
\`\`\`bash
|
|
6384
8010
|
# Restore dotfiles to a new machine
|
|
@@ -6435,7 +8061,7 @@ var validateGitHubUrl = (value) => {
|
|
|
6435
8061
|
}
|
|
6436
8062
|
return void 0;
|
|
6437
8063
|
};
|
|
6438
|
-
var
|
|
8064
|
+
var validateGitUrl2 = (value) => {
|
|
6439
8065
|
if (!value) return "Repository URL is required";
|
|
6440
8066
|
const trimmed = value.trim();
|
|
6441
8067
|
const isHttps = /^https?:\/\/.+\/.+/.test(trimmed);
|
|
@@ -6482,7 +8108,8 @@ var initFromScratch = async (tuckDir, options) => {
|
|
|
6482
8108
|
await saveConfig(
|
|
6483
8109
|
{
|
|
6484
8110
|
...defaultConfig,
|
|
6485
|
-
repository: { ...defaultConfig.repository, path: tuckDir }
|
|
8111
|
+
repository: { ...defaultConfig.repository, path: tuckDir },
|
|
8112
|
+
remote: options.remoteConfig || defaultConfig.remote
|
|
6486
8113
|
},
|
|
6487
8114
|
tuckDir
|
|
6488
8115
|
);
|
|
@@ -6765,10 +8392,10 @@ var setupGitHubRepo = async (tuckDir) => {
|
|
|
6765
8392
|
}
|
|
6766
8393
|
prompts.log.info("Please complete the authentication in your browser...");
|
|
6767
8394
|
try {
|
|
6768
|
-
const { execFile:
|
|
6769
|
-
const { promisify:
|
|
6770
|
-
const
|
|
6771
|
-
await
|
|
8395
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
8396
|
+
const { promisify: promisify7 } = await import("util");
|
|
8397
|
+
const execFileAsync6 = promisify7(execFile6);
|
|
8398
|
+
await execFileAsync6("gh", ["auth", "login", "--web"]);
|
|
6772
8399
|
if (!await isGhAuthenticated()) {
|
|
6773
8400
|
prompts.log.warning("Authentication may have failed");
|
|
6774
8401
|
return await setupAlternativeAuth(tuckDir);
|
|
@@ -7120,11 +8747,25 @@ var runInteractiveInit = async () => {
|
|
|
7120
8747
|
prompts.outro("Use `tuck status` to see current state");
|
|
7121
8748
|
return;
|
|
7122
8749
|
}
|
|
8750
|
+
console.log();
|
|
8751
|
+
let providerResult = null;
|
|
8752
|
+
providerResult = await setupProvider();
|
|
8753
|
+
if (!providerResult) {
|
|
8754
|
+
providerResult = {
|
|
8755
|
+
success: true,
|
|
8756
|
+
mode: "local",
|
|
8757
|
+
config: buildRemoteConfig("local"),
|
|
8758
|
+
provider: getProvider("local")
|
|
8759
|
+
};
|
|
8760
|
+
}
|
|
8761
|
+
console.log();
|
|
8762
|
+
prompts.log.info(`Provider: ${describeProviderConfig(providerResult.config)}`);
|
|
7123
8763
|
let skipExistingRepoQuestion = false;
|
|
7124
|
-
let remoteUrl = null;
|
|
8764
|
+
let remoteUrl = providerResult.remoteUrl || null;
|
|
7125
8765
|
let existingRepoToUseAsRemote = null;
|
|
8766
|
+
const canSearchForRepos = providerResult.mode === "github" || providerResult.mode === "gitlab";
|
|
7126
8767
|
const ghInstalled = await isGhInstalled();
|
|
7127
|
-
const ghAuth = ghInstalled && await isGhAuthenticated();
|
|
8768
|
+
const ghAuth = canSearchForRepos && providerResult.mode === "github" && ghInstalled && await isGhAuthenticated();
|
|
7128
8769
|
if (ghAuth) {
|
|
7129
8770
|
const spinner4 = prompts.spinner();
|
|
7130
8771
|
spinner4.start("Checking for existing dotfiles repository on GitHub...");
|
|
@@ -7238,7 +8879,7 @@ var runInteractiveInit = async () => {
|
|
|
7238
8879
|
if (hasExisting === "yes") {
|
|
7239
8880
|
const repoUrl = await prompts.text("Enter repository URL:", {
|
|
7240
8881
|
placeholder: "git@host:user/dotfiles.git or https://host/user/dotfiles.git",
|
|
7241
|
-
validate:
|
|
8882
|
+
validate: validateGitUrl2
|
|
7242
8883
|
});
|
|
7243
8884
|
await initFromRemote(tuckDir, repoUrl);
|
|
7244
8885
|
prompts.log.success("Repository cloned successfully!");
|
|
@@ -7257,7 +8898,7 @@ var runInteractiveInit = async () => {
|
|
|
7257
8898
|
return;
|
|
7258
8899
|
}
|
|
7259
8900
|
}
|
|
7260
|
-
await initFromScratch(tuckDir, {});
|
|
8901
|
+
await initFromScratch(tuckDir, { remoteConfig: providerResult.config });
|
|
7261
8902
|
if (existingRepoToUseAsRemote) {
|
|
7262
8903
|
const protocol = await getPreferredRemoteProtocol();
|
|
7263
8904
|
remoteUrl = protocol === "ssh" ? `git@github.com:${existingRepoToUseAsRemote}.git` : `https://github.com/${existingRepoToUseAsRemote}.git`;
|
|
@@ -7266,7 +8907,7 @@ var runInteractiveInit = async () => {
|
|
|
7266
8907
|
prompts.log.info("Your next push will update the remote repository");
|
|
7267
8908
|
console.log();
|
|
7268
8909
|
}
|
|
7269
|
-
if (!remoteUrl) {
|
|
8910
|
+
if (!remoteUrl && providerResult.mode !== "local") {
|
|
7270
8911
|
const wantsRemote = await prompts.confirm(
|
|
7271
8912
|
"Would you like to set up a remote repository?",
|
|
7272
8913
|
true
|
|
@@ -7498,9 +9139,11 @@ var runInit = async (options) => {
|
|
|
7498
9139
|
logger.info("Run `tuck restore --all` to restore dotfiles");
|
|
7499
9140
|
return;
|
|
7500
9141
|
}
|
|
9142
|
+
const detectedConfig = options.remote ? buildRemoteConfig(detectProviderFromUrl(options.remote), { url: options.remote }) : buildRemoteConfig("local");
|
|
7501
9143
|
await initFromScratch(tuckDir, {
|
|
7502
9144
|
remote: options.remote,
|
|
7503
|
-
bare: options.bare
|
|
9145
|
+
bare: options.bare,
|
|
9146
|
+
remoteConfig: detectedConfig
|
|
7504
9147
|
});
|
|
7505
9148
|
logger.success(`Tuck initialized at ${collapsePath(tuckDir)}`);
|
|
7506
9149
|
nextSteps([
|
|
@@ -8107,11 +9750,54 @@ init_sync();
|
|
|
8107
9750
|
init_ui();
|
|
8108
9751
|
init_paths();
|
|
8109
9752
|
init_manifest();
|
|
9753
|
+
import { Command as Command7 } from "commander";
|
|
9754
|
+
|
|
9755
|
+
// src/lib/remoteChecks.ts
|
|
9756
|
+
init_ui();
|
|
9757
|
+
init_config();
|
|
9758
|
+
var checkLocalMode = async (tuckDir) => {
|
|
9759
|
+
try {
|
|
9760
|
+
const config = await loadConfig(tuckDir);
|
|
9761
|
+
if (config.remote?.mode === "local") {
|
|
9762
|
+
return true;
|
|
9763
|
+
}
|
|
9764
|
+
} catch {
|
|
9765
|
+
}
|
|
9766
|
+
return false;
|
|
9767
|
+
};
|
|
9768
|
+
var showLocalModeWarningForPull = async () => {
|
|
9769
|
+
prompts.log.warning("Tuck is configured for local-only mode (no remote sync).");
|
|
9770
|
+
console.log();
|
|
9771
|
+
prompts.note(
|
|
9772
|
+
"Your dotfiles are tracked locally but not synced to a remote.\n\nTo enable remote sync, run:\n tuck config remote\n\nOr re-initialize with:\n tuck init",
|
|
9773
|
+
"Local Mode"
|
|
9774
|
+
);
|
|
9775
|
+
};
|
|
9776
|
+
var showLocalModeWarningForPush = async () => {
|
|
9777
|
+
prompts.log.warning("Tuck is configured for local-only mode (no remote sync).");
|
|
9778
|
+
console.log();
|
|
9779
|
+
prompts.note(
|
|
9780
|
+
"Your dotfiles are tracked locally but not synced to a remote.\n\nTo enable remote sync, run:\n tuck config remote\n\nOr re-initialize with:\n tuck init",
|
|
9781
|
+
"Local Mode"
|
|
9782
|
+
);
|
|
9783
|
+
console.log();
|
|
9784
|
+
const configureNow = await prompts.confirm("Would you like to configure a remote now?");
|
|
9785
|
+
if (configureNow) {
|
|
9786
|
+
prompts.log.info("Run 'tuck config remote' to set up a remote repository.");
|
|
9787
|
+
}
|
|
9788
|
+
return configureNow;
|
|
9789
|
+
};
|
|
9790
|
+
|
|
9791
|
+
// src/commands/push.ts
|
|
8110
9792
|
init_git();
|
|
8111
9793
|
init_errors();
|
|
8112
|
-
import { Command as Command7 } from "commander";
|
|
8113
9794
|
var runInteractivePush = async (tuckDir) => {
|
|
8114
9795
|
prompts.intro("tuck push");
|
|
9796
|
+
if (await checkLocalMode(tuckDir)) {
|
|
9797
|
+
await showLocalModeWarningForPush();
|
|
9798
|
+
prompts.outro("");
|
|
9799
|
+
return;
|
|
9800
|
+
}
|
|
8115
9801
|
const hasRemoteRepo = await hasRemote(tuckDir);
|
|
8116
9802
|
if (!hasRemoteRepo) {
|
|
8117
9803
|
prompts.log.warning("No remote configured");
|
|
@@ -8201,6 +9887,12 @@ var runPush = async (options) => {
|
|
|
8201
9887
|
} catch {
|
|
8202
9888
|
throw new NotInitializedError();
|
|
8203
9889
|
}
|
|
9890
|
+
if (await checkLocalMode(tuckDir)) {
|
|
9891
|
+
throw new GitError(
|
|
9892
|
+
"Cannot push in local-only mode",
|
|
9893
|
+
"Run 'tuck config remote' to configure a remote repository"
|
|
9894
|
+
);
|
|
9895
|
+
}
|
|
8204
9896
|
if (!options.force && !options.setUpstream) {
|
|
8205
9897
|
await runInteractivePush(tuckDir);
|
|
8206
9898
|
return;
|
|
@@ -8240,11 +9932,16 @@ var pushCommand = new Command7("push").description("Push changes to remote repos
|
|
|
8240
9932
|
init_ui();
|
|
8241
9933
|
init_paths();
|
|
8242
9934
|
init_manifest();
|
|
9935
|
+
import { Command as Command8 } from "commander";
|
|
8243
9936
|
init_git();
|
|
8244
9937
|
init_errors();
|
|
8245
|
-
import { Command as Command8 } from "commander";
|
|
8246
9938
|
var runInteractivePull = async (tuckDir) => {
|
|
8247
9939
|
prompts.intro("tuck pull");
|
|
9940
|
+
if (await checkLocalMode(tuckDir)) {
|
|
9941
|
+
await showLocalModeWarningForPull();
|
|
9942
|
+
prompts.outro("");
|
|
9943
|
+
return;
|
|
9944
|
+
}
|
|
8248
9945
|
const hasRemoteRepo = await hasRemote(tuckDir);
|
|
8249
9946
|
if (!hasRemoteRepo) {
|
|
8250
9947
|
prompts.log.error("No remote configured");
|
|
@@ -8300,6 +9997,12 @@ var runPull = async (options) => {
|
|
|
8300
9997
|
} catch {
|
|
8301
9998
|
throw new NotInitializedError();
|
|
8302
9999
|
}
|
|
10000
|
+
if (await checkLocalMode(tuckDir)) {
|
|
10001
|
+
throw new GitError(
|
|
10002
|
+
"Cannot pull in local-only mode",
|
|
10003
|
+
"Run 'tuck config remote' to configure a remote repository"
|
|
10004
|
+
);
|
|
10005
|
+
}
|
|
8303
10006
|
if (!options.rebase && !options.restore) {
|
|
8304
10007
|
await runInteractivePull(tuckDir);
|
|
8305
10008
|
return;
|
|
@@ -8958,6 +10661,7 @@ init_ui();
|
|
|
8958
10661
|
init_paths();
|
|
8959
10662
|
init_config();
|
|
8960
10663
|
init_manifest();
|
|
10664
|
+
init_git();
|
|
8961
10665
|
init_errors();
|
|
8962
10666
|
import { Command as Command12 } from "commander";
|
|
8963
10667
|
import { spawn } from "child_process";
|
|
@@ -9166,6 +10870,12 @@ var runConfigReset = async () => {
|
|
|
9166
10870
|
};
|
|
9167
10871
|
var showConfigView = async (config) => {
|
|
9168
10872
|
const configObj = config;
|
|
10873
|
+
if (config.remote) {
|
|
10874
|
+
console.log(colors.bold.cyan("~ Remote Provider"));
|
|
10875
|
+
console.log(colors.dim("-".repeat(40)));
|
|
10876
|
+
console.log(` ${describeProviderConfig(config.remote)}`);
|
|
10877
|
+
console.log();
|
|
10878
|
+
}
|
|
9169
10879
|
const sections = [
|
|
9170
10880
|
{ key: "repository", title: "Repository Settings", icon: "*" },
|
|
9171
10881
|
{ key: "files", title: "File Management", icon: ">" },
|
|
@@ -9291,6 +11001,7 @@ var runInteractiveConfig = async () => {
|
|
|
9291
11001
|
const action = await prompts.select("What would you like to do?", [
|
|
9292
11002
|
{ value: "view", label: "View current configuration", hint: "See all settings" },
|
|
9293
11003
|
{ value: "edit", label: "Edit a setting", hint: "Modify a specific value" },
|
|
11004
|
+
{ value: "remote", label: "Configure remote", hint: "Set up GitHub, GitLab, or local mode" },
|
|
9294
11005
|
{ value: "wizard", label: "Run setup wizard", hint: "Guided configuration" },
|
|
9295
11006
|
{ value: "reset", label: "Reset to defaults", hint: "Restore default values" },
|
|
9296
11007
|
{ value: "open", label: "Open in editor", hint: `Edit with ${process.env.EDITOR || "vim"}` }
|
|
@@ -9303,6 +11014,10 @@ var runInteractiveConfig = async () => {
|
|
|
9303
11014
|
case "edit":
|
|
9304
11015
|
await editConfigInteractive(config, tuckDir);
|
|
9305
11016
|
break;
|
|
11017
|
+
case "remote":
|
|
11018
|
+
await runConfigRemote();
|
|
11019
|
+
return;
|
|
11020
|
+
// runConfigRemote has its own outro
|
|
9306
11021
|
case "wizard":
|
|
9307
11022
|
await runConfigWizard(config, tuckDir);
|
|
9308
11023
|
break;
|
|
@@ -9315,6 +11030,105 @@ var runInteractiveConfig = async () => {
|
|
|
9315
11030
|
}
|
|
9316
11031
|
prompts.outro("Done!");
|
|
9317
11032
|
};
|
|
11033
|
+
var runConfigRemote = async () => {
|
|
11034
|
+
banner();
|
|
11035
|
+
prompts.intro("tuck config remote");
|
|
11036
|
+
const tuckDir = getTuckDir();
|
|
11037
|
+
const config = await loadConfig(tuckDir);
|
|
11038
|
+
if (config.remote) {
|
|
11039
|
+
console.log();
|
|
11040
|
+
console.log(colors.dim("Current remote configuration:"));
|
|
11041
|
+
console.log(` ${describeProviderConfig(config.remote)}`);
|
|
11042
|
+
console.log();
|
|
11043
|
+
}
|
|
11044
|
+
const shouldChange = await prompts.confirm("Configure remote provider?", true);
|
|
11045
|
+
if (!shouldChange) {
|
|
11046
|
+
prompts.outro("No changes made");
|
|
11047
|
+
return;
|
|
11048
|
+
}
|
|
11049
|
+
const result = await setupProvider();
|
|
11050
|
+
if (!result) {
|
|
11051
|
+
prompts.outro("Configuration cancelled");
|
|
11052
|
+
return;
|
|
11053
|
+
}
|
|
11054
|
+
const updatedConfig = {
|
|
11055
|
+
...config,
|
|
11056
|
+
remote: result.config
|
|
11057
|
+
};
|
|
11058
|
+
await saveConfig(updatedConfig, tuckDir);
|
|
11059
|
+
if (result.remoteUrl) {
|
|
11060
|
+
try {
|
|
11061
|
+
if (await hasRemote(tuckDir)) {
|
|
11062
|
+
await removeRemote(tuckDir, "origin");
|
|
11063
|
+
}
|
|
11064
|
+
await addRemote(tuckDir, "origin", result.remoteUrl);
|
|
11065
|
+
prompts.log.success("Git remote updated");
|
|
11066
|
+
} catch (error) {
|
|
11067
|
+
prompts.log.warning(
|
|
11068
|
+
`Could not update git remote: ${error instanceof Error ? error.message : String(error)}`
|
|
11069
|
+
);
|
|
11070
|
+
prompts.log.info(`Manually add remote: git remote add origin ${result.remoteUrl}`);
|
|
11071
|
+
}
|
|
11072
|
+
}
|
|
11073
|
+
if (result.mode !== "local" && result.mode !== "custom" && !result.remoteUrl) {
|
|
11074
|
+
const shouldCreateRepo = await prompts.confirm(
|
|
11075
|
+
"Would you like to create a repository now?",
|
|
11076
|
+
true
|
|
11077
|
+
);
|
|
11078
|
+
if (shouldCreateRepo) {
|
|
11079
|
+
const provider = getProvider(result.mode, result.config);
|
|
11080
|
+
const repoName = await prompts.text("Repository name:", {
|
|
11081
|
+
defaultValue: "dotfiles",
|
|
11082
|
+
placeholder: "dotfiles",
|
|
11083
|
+
validate: (value) => {
|
|
11084
|
+
if (!value) return "Repository name is required";
|
|
11085
|
+
if (!/^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(value)) {
|
|
11086
|
+
return "Repository name must start and end with alphanumeric characters";
|
|
11087
|
+
}
|
|
11088
|
+
return void 0;
|
|
11089
|
+
}
|
|
11090
|
+
});
|
|
11091
|
+
const visibility = await prompts.select("Repository visibility:", [
|
|
11092
|
+
{ value: "private", label: "Private (recommended)", hint: "Only you can see it" },
|
|
11093
|
+
{ value: "public", label: "Public", hint: "Anyone can see it" }
|
|
11094
|
+
]);
|
|
11095
|
+
try {
|
|
11096
|
+
const spinner4 = prompts.spinner();
|
|
11097
|
+
spinner4.start("Creating repository...");
|
|
11098
|
+
const repo = await provider.createRepo({
|
|
11099
|
+
name: repoName,
|
|
11100
|
+
description: "My dotfiles managed with tuck",
|
|
11101
|
+
isPrivate: visibility === "private"
|
|
11102
|
+
});
|
|
11103
|
+
spinner4.stop(`Repository created: ${repo.fullName}`);
|
|
11104
|
+
const remoteUrl = await provider.getPreferredRepoUrl(repo);
|
|
11105
|
+
try {
|
|
11106
|
+
if (await hasRemote(tuckDir)) {
|
|
11107
|
+
await removeRemote(tuckDir, "origin");
|
|
11108
|
+
}
|
|
11109
|
+
await addRemote(tuckDir, "origin", remoteUrl);
|
|
11110
|
+
prompts.log.success("Remote configured");
|
|
11111
|
+
} catch (error) {
|
|
11112
|
+
prompts.log.warning(
|
|
11113
|
+
`Could not add remote: ${error instanceof Error ? error.message : String(error)}`
|
|
11114
|
+
);
|
|
11115
|
+
}
|
|
11116
|
+
updatedConfig.remote = {
|
|
11117
|
+
...updatedConfig.remote,
|
|
11118
|
+
repoName
|
|
11119
|
+
};
|
|
11120
|
+
await saveConfig(updatedConfig, tuckDir);
|
|
11121
|
+
} catch (error) {
|
|
11122
|
+
prompts.log.error(
|
|
11123
|
+
`Failed to create repository: ${error instanceof Error ? error.message : String(error)}`
|
|
11124
|
+
);
|
|
11125
|
+
}
|
|
11126
|
+
}
|
|
11127
|
+
}
|
|
11128
|
+
console.log();
|
|
11129
|
+
prompts.log.success(`Remote configured: ${describeProviderConfig(result.config)}`);
|
|
11130
|
+
prompts.outro("Done!");
|
|
11131
|
+
};
|
|
9318
11132
|
var configCommand = new Command12("config").description("Manage tuck configuration").action(async () => {
|
|
9319
11133
|
const tuckDir = getTuckDir();
|
|
9320
11134
|
try {
|
|
@@ -9373,6 +11187,16 @@ var configCommand = new Command12("config").description("Manage tuck configurati
|
|
|
9373
11187
|
}
|
|
9374
11188
|
await runConfigReset();
|
|
9375
11189
|
})
|
|
11190
|
+
).addCommand(
|
|
11191
|
+
new Command12("remote").description("Configure remote provider").action(async () => {
|
|
11192
|
+
const tuckDir = getTuckDir();
|
|
11193
|
+
try {
|
|
11194
|
+
await loadManifest(tuckDir);
|
|
11195
|
+
} catch {
|
|
11196
|
+
throw new NotInitializedError();
|
|
11197
|
+
}
|
|
11198
|
+
await runConfigRemote();
|
|
11199
|
+
})
|
|
9376
11200
|
);
|
|
9377
11201
|
|
|
9378
11202
|
// src/commands/apply.ts
|
|
@@ -10723,13 +12547,155 @@ init_secrets2();
|
|
|
10723
12547
|
// src/index.ts
|
|
10724
12548
|
init_errors();
|
|
10725
12549
|
init_constants();
|
|
12550
|
+
|
|
12551
|
+
// src/lib/updater.ts
|
|
12552
|
+
init_constants();
|
|
12553
|
+
import updateNotifier from "update-notifier";
|
|
12554
|
+
import { execSync, spawnSync } from "child_process";
|
|
12555
|
+
import chalk5 from "chalk";
|
|
12556
|
+
import boxen3 from "boxen";
|
|
12557
|
+
import { createInterface } from "readline";
|
|
12558
|
+
var pkg = {
|
|
12559
|
+
name: "@prnv/tuck",
|
|
12560
|
+
version: VERSION
|
|
12561
|
+
};
|
|
12562
|
+
var detectPackageManager = () => {
|
|
12563
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
12564
|
+
if (userAgent.includes("pnpm")) {
|
|
12565
|
+
return "pnpm";
|
|
12566
|
+
}
|
|
12567
|
+
try {
|
|
12568
|
+
const pnpmList = execSync("pnpm list -g --depth=0 2>/dev/null", {
|
|
12569
|
+
encoding: "utf-8",
|
|
12570
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
12571
|
+
});
|
|
12572
|
+
if (pnpmList.includes("@prnv/tuck")) {
|
|
12573
|
+
return "pnpm";
|
|
12574
|
+
}
|
|
12575
|
+
} catch {
|
|
12576
|
+
}
|
|
12577
|
+
return "npm";
|
|
12578
|
+
};
|
|
12579
|
+
var getUpdateCommand = (packageManager) => {
|
|
12580
|
+
if (packageManager === "pnpm") {
|
|
12581
|
+
return "pnpm update -g @prnv/tuck";
|
|
12582
|
+
}
|
|
12583
|
+
return "npm update -g @prnv/tuck";
|
|
12584
|
+
};
|
|
12585
|
+
var waitForEnterOrCancel = () => {
|
|
12586
|
+
return new Promise((resolve2) => {
|
|
12587
|
+
const rl = createInterface({
|
|
12588
|
+
input: process.stdin,
|
|
12589
|
+
output: process.stdout
|
|
12590
|
+
});
|
|
12591
|
+
rl.on("close", () => {
|
|
12592
|
+
resolve2(false);
|
|
12593
|
+
});
|
|
12594
|
+
rl.on("line", () => {
|
|
12595
|
+
rl.close();
|
|
12596
|
+
resolve2(true);
|
|
12597
|
+
});
|
|
12598
|
+
process.on("SIGINT", () => {
|
|
12599
|
+
rl.close();
|
|
12600
|
+
resolve2(false);
|
|
12601
|
+
});
|
|
12602
|
+
});
|
|
12603
|
+
};
|
|
12604
|
+
var executeUpdate = (packageManager) => {
|
|
12605
|
+
const command = getUpdateCommand(packageManager);
|
|
12606
|
+
console.log(chalk5.dim(`
|
|
12607
|
+
Updating ${APP_NAME} via ${packageManager}...`));
|
|
12608
|
+
try {
|
|
12609
|
+
const result = spawnSync(packageManager, ["update", "-g", "@prnv/tuck"], {
|
|
12610
|
+
stdio: "inherit",
|
|
12611
|
+
shell: true
|
|
12612
|
+
});
|
|
12613
|
+
if (result.status === 0) {
|
|
12614
|
+
console.log(chalk5.green(`
|
|
12615
|
+
Successfully updated ${APP_NAME}!`));
|
|
12616
|
+
console.log(chalk5.dim("Restart tuck to use the new version.\n"));
|
|
12617
|
+
return true;
|
|
12618
|
+
} else {
|
|
12619
|
+
console.log(chalk5.red("\nUpdate failed."));
|
|
12620
|
+
console.log(chalk5.dim(`Run manually: ${command}
|
|
12621
|
+
`));
|
|
12622
|
+
return false;
|
|
12623
|
+
}
|
|
12624
|
+
} catch (error) {
|
|
12625
|
+
console.log(chalk5.red("\nUpdate failed."));
|
|
12626
|
+
console.log(chalk5.dim(`Run manually: ${command}
|
|
12627
|
+
`));
|
|
12628
|
+
return false;
|
|
12629
|
+
}
|
|
12630
|
+
};
|
|
12631
|
+
var shouldSkipUpdateCheck = () => {
|
|
12632
|
+
if (process.env.CI) {
|
|
12633
|
+
return true;
|
|
12634
|
+
}
|
|
12635
|
+
const execPath = process.env.npm_execpath || "";
|
|
12636
|
+
if (execPath.includes("npx")) {
|
|
12637
|
+
return true;
|
|
12638
|
+
}
|
|
12639
|
+
if (process.env.NO_UPDATE_CHECK) {
|
|
12640
|
+
return true;
|
|
12641
|
+
}
|
|
12642
|
+
if (!process.stdin.isTTY) {
|
|
12643
|
+
return true;
|
|
12644
|
+
}
|
|
12645
|
+
return false;
|
|
12646
|
+
};
|
|
12647
|
+
var checkForUpdates = async () => {
|
|
12648
|
+
if (shouldSkipUpdateCheck()) {
|
|
12649
|
+
return;
|
|
12650
|
+
}
|
|
12651
|
+
const notifier = updateNotifier({
|
|
12652
|
+
pkg,
|
|
12653
|
+
updateCheckInterval: 1e3 * 60 * 60 * 24
|
|
12654
|
+
// 24 hours
|
|
12655
|
+
});
|
|
12656
|
+
if (!notifier.update || notifier.update.latest === VERSION) {
|
|
12657
|
+
return;
|
|
12658
|
+
}
|
|
12659
|
+
const { latest } = notifier.update;
|
|
12660
|
+
const packageManager = detectPackageManager();
|
|
12661
|
+
const updateCommand = getUpdateCommand(packageManager);
|
|
12662
|
+
const message = [
|
|
12663
|
+
"",
|
|
12664
|
+
chalk5.bold(`Update available: ${chalk5.red(VERSION)} ${chalk5.dim("->")} ${chalk5.green(latest)}`),
|
|
12665
|
+
"",
|
|
12666
|
+
chalk5.dim("Press Enter to update, or Ctrl+C to skip"),
|
|
12667
|
+
""
|
|
12668
|
+
].join("\n");
|
|
12669
|
+
console.log(
|
|
12670
|
+
boxen3(message, {
|
|
12671
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
12672
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 },
|
|
12673
|
+
borderStyle: "round",
|
|
12674
|
+
borderColor: "cyan",
|
|
12675
|
+
textAlignment: "center"
|
|
12676
|
+
})
|
|
12677
|
+
);
|
|
12678
|
+
const shouldUpdate = await waitForEnterOrCancel();
|
|
12679
|
+
if (shouldUpdate) {
|
|
12680
|
+
const success = executeUpdate(packageManager);
|
|
12681
|
+
if (success) {
|
|
12682
|
+
process.exit(0);
|
|
12683
|
+
}
|
|
12684
|
+
} else {
|
|
12685
|
+
console.log(chalk5.dim(`
|
|
12686
|
+
Skipped. Run '${updateCommand}' to update later.
|
|
12687
|
+
`));
|
|
12688
|
+
}
|
|
12689
|
+
};
|
|
12690
|
+
|
|
12691
|
+
// src/index.ts
|
|
10726
12692
|
init_banner();
|
|
10727
12693
|
init_paths();
|
|
10728
12694
|
init_manifest();
|
|
10729
12695
|
init_git();
|
|
10730
12696
|
var program = new Command16();
|
|
10731
12697
|
program.name("tuck").description(DESCRIPTION).version(VERSION, "-v, --version", "Display version number").configureOutput({
|
|
10732
|
-
outputError: (str, write) => write(
|
|
12698
|
+
outputError: (str, write) => write(chalk6.red(str))
|
|
10733
12699
|
}).addHelpText("before", customHelp(VERSION)).helpOption("-h, --help", "Display this help message").showHelpAfterError(false);
|
|
10734
12700
|
program.addCommand(initCommand);
|
|
10735
12701
|
program.addCommand(addCommand);
|
|
@@ -10750,12 +12716,12 @@ var runDefaultAction = async () => {
|
|
|
10750
12716
|
const tuckDir = getTuckDir();
|
|
10751
12717
|
if (!await pathExists(tuckDir)) {
|
|
10752
12718
|
miniBanner();
|
|
10753
|
-
console.log(
|
|
10754
|
-
console.log(
|
|
10755
|
-
console.log(
|
|
12719
|
+
console.log(chalk6.bold("Get started with tuck:\n"));
|
|
12720
|
+
console.log(chalk6.cyan(" tuck init") + chalk6.dim(" - Set up tuck and create a GitHub repo"));
|
|
12721
|
+
console.log(chalk6.cyan(" tuck scan") + chalk6.dim(" - Find dotfiles to track"));
|
|
10756
12722
|
console.log();
|
|
10757
|
-
console.log(
|
|
10758
|
-
console.log(
|
|
12723
|
+
console.log(chalk6.dim("On a new machine:"));
|
|
12724
|
+
console.log(chalk6.cyan(" tuck apply <username>") + chalk6.dim(" - Apply your dotfiles"));
|
|
10759
12725
|
console.log();
|
|
10760
12726
|
return;
|
|
10761
12727
|
}
|
|
@@ -10764,38 +12730,38 @@ var runDefaultAction = async () => {
|
|
|
10764
12730
|
const trackedCount = Object.keys(manifest.files).length;
|
|
10765
12731
|
const gitStatus = await getStatus(tuckDir);
|
|
10766
12732
|
miniBanner();
|
|
10767
|
-
console.log(
|
|
10768
|
-
console.log(` Tracked files: ${
|
|
12733
|
+
console.log(chalk6.bold("Status:\n"));
|
|
12734
|
+
console.log(` Tracked files: ${chalk6.cyan(trackedCount.toString())}`);
|
|
10769
12735
|
const pendingChanges = gitStatus.modified.length + gitStatus.staged.length;
|
|
10770
12736
|
if (pendingChanges > 0) {
|
|
10771
|
-
console.log(` Pending changes: ${
|
|
12737
|
+
console.log(` Pending changes: ${chalk6.yellow(pendingChanges.toString())}`);
|
|
10772
12738
|
} else {
|
|
10773
|
-
console.log(` Pending changes: ${
|
|
12739
|
+
console.log(` Pending changes: ${chalk6.dim("none")}`);
|
|
10774
12740
|
}
|
|
10775
12741
|
if (gitStatus.ahead > 0) {
|
|
10776
|
-
console.log(` Commits to push: ${
|
|
12742
|
+
console.log(` Commits to push: ${chalk6.yellow(gitStatus.ahead.toString())}`);
|
|
10777
12743
|
}
|
|
10778
12744
|
console.log();
|
|
10779
|
-
console.log(
|
|
12745
|
+
console.log(chalk6.bold("Next steps:\n"));
|
|
10780
12746
|
if (trackedCount === 0) {
|
|
10781
|
-
console.log(
|
|
10782
|
-
console.log(
|
|
12747
|
+
console.log(chalk6.cyan(" tuck scan") + chalk6.dim(" - Find dotfiles to track"));
|
|
12748
|
+
console.log(chalk6.cyan(" tuck add <file>") + chalk6.dim(" - Track a specific file"));
|
|
10783
12749
|
} else if (pendingChanges > 0) {
|
|
10784
|
-
console.log(
|
|
10785
|
-
console.log(
|
|
12750
|
+
console.log(chalk6.cyan(" tuck sync") + chalk6.dim(" - Commit and push your changes"));
|
|
12751
|
+
console.log(chalk6.cyan(" tuck diff") + chalk6.dim(" - Preview what changed"));
|
|
10786
12752
|
} else if (gitStatus.ahead > 0) {
|
|
10787
|
-
console.log(
|
|
12753
|
+
console.log(chalk6.cyan(" tuck push") + chalk6.dim(" - Push commits to GitHub"));
|
|
10788
12754
|
} else {
|
|
10789
|
-
console.log(
|
|
12755
|
+
console.log(chalk6.dim(" All synced! Your dotfiles are up to date."));
|
|
10790
12756
|
console.log();
|
|
10791
|
-
console.log(
|
|
10792
|
-
console.log(
|
|
12757
|
+
console.log(chalk6.cyan(" tuck scan") + chalk6.dim(" - Find more dotfiles to track"));
|
|
12758
|
+
console.log(chalk6.cyan(" tuck list") + chalk6.dim(" - See tracked files"));
|
|
10793
12759
|
}
|
|
10794
12760
|
console.log();
|
|
10795
12761
|
} catch {
|
|
10796
12762
|
miniBanner();
|
|
10797
|
-
console.log(
|
|
10798
|
-
console.log(
|
|
12763
|
+
console.log(chalk6.yellow("Tuck directory exists but may be corrupted."));
|
|
12764
|
+
console.log(chalk6.dim("Run `tuck init` to reinitialize."));
|
|
10799
12765
|
console.log();
|
|
10800
12766
|
}
|
|
10801
12767
|
};
|
|
@@ -10804,9 +12770,16 @@ process.on("uncaughtException", handleError);
|
|
|
10804
12770
|
process.on("unhandledRejection", (reason) => {
|
|
10805
12771
|
handleError(reason instanceof Error ? reason : new Error(String(reason)));
|
|
10806
12772
|
});
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
}
|
|
12773
|
+
var isHelpOrVersion = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("--version") || process.argv.includes("-v");
|
|
12774
|
+
var main = async () => {
|
|
12775
|
+
if (!isHelpOrVersion) {
|
|
12776
|
+
await checkForUpdates();
|
|
12777
|
+
}
|
|
12778
|
+
if (!hasCommand && !isHelpOrVersion) {
|
|
12779
|
+
await runDefaultAction();
|
|
12780
|
+
} else {
|
|
12781
|
+
await program.parseAsync(process.argv);
|
|
12782
|
+
}
|
|
12783
|
+
};
|
|
12784
|
+
main().catch(handleError);
|
|
10812
12785
|
//# sourceMappingURL=index.js.map
|