@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/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 pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
567
- VERSION_VALUE = pkg.version;
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: execFile3 } = await import("child_process");
1480
- const { promisify: promisify4 } = await import("util");
1481
- const execFileAsync3 = promisify4(execFile3);
1482
- const { stdout, stderr } = await execFileAsync3("gh", ["auth", "status"]);
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 execFileAsync3("gh", ["auth", "setup-git"]);
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 execFileAsync, API_REQUEST_TIMEOUT_MS, GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS, TOKEN_EXPIRATION_WARNING_DAYS, MIN_GITHUB_TOKEN_LENGTH, GITHUB_TOKEN_PREFIXES, validateRepoName, 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;
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
- execFileAsync = promisify(execFile);
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
- validateRepoName = (repoName) => {
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 execFileAsync("gh", ["--version"]);
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 execFileAsync("gh", ["auth", "status"]);
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 execFileAsync("gh", ["api", "user", "--jq", ".login, .name, .email"]);
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
- validateRepoName(repoName);
1741
- await execFileAsync("gh", ["repo", "view", repoName, "--json", "name"]);
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
- validateRepoName(options.name);
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 execFileAsync("gh", args);
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 execFileAsync("gh", ["config", "get", "git_protocol"]);
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
- validateRepoName(repoName);
1890
- const { stdout } = await execFileAsync("gh", [
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
- validateRepoName(repoName);
2518
+ validateRepoName2(repoName);
1915
2519
  try {
1916
- await execFileAsync("gh", ["repo", "clone", repoName, targetDir]);
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 execFileAsync("ssh", ["-T", "-o", `StrictHostKeyChecking=${strictHostKeyChecking}`, "git@github.com"]);
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 execFileAsync("git", ["config", "--global", "credential.helper"]);
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 execFileAsync("git", ["config", "--global", "credential.helper", "osxkeychain"]);
2828
+ await execFileAsync4("git", ["config", "--global", "credential.helper", "osxkeychain"]);
2225
2829
  } else if (platform2 === "linux") {
2226
2830
  try {
2227
- await execFileAsync("git", ["config", "--global", "credential.helper", "libsecret"]);
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 execFileAsync("git", ["config", "--global", "credential.helper", `cache --timeout=${GIT_CREDENTIAL_CACHE_FALLBACK_TIMEOUT_SECONDS}`]);
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 execFileAsync("git", ["config", "--global", "credential.helper", "manager"]);
2839
+ await execFileAsync4("git", ["config", "--global", "credential.helper", "manager"]);
2236
2840
  }
2237
2841
  } catch {
2238
- await execFileAsync("git", ["config", "--global", "credential.helper", "store"]);
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 promisify2 } from "util";
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 = promisify2(exec);
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 execFile2 } from "child_process";
5198
- import { promisify as promisify3 } from "util";
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 execFileAsync2, gitleaksResultSchema, gitleaksOutputSchema, isGitleaksInstalled, isTrufflehogInstalled, mapGitleaksSeverity, generatePlaceholderFromRule, scanWithGitleaks, scanWithScanner;
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
- execFileAsync2 = promisify3(execFile2);
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 execFileAsync2("gitleaks", ["version"]);
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 execFileAsync2("trufflehog", ["--version"]);
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 execFileAsync2("gitleaks", [
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 chalk5 from "chalk";
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
- Managed with [tuck](https://github.com/Pranav-Karra-3301/tuck) - Modern Dotfiles Manager
6942
+ // src/lib/providerSetup.ts
6943
+ init_ui();
6377
6944
 
6378
- ${machine ? `## Machine: ${machine}
6379
- ` : ""}
6945
+ // src/lib/providers/index.ts
6946
+ init_types();
6947
+ init_gitlab();
6380
6948
 
6381
- ## Quick Start
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 validateGitUrl = (value) => {
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: execFile3 } = await import("child_process");
6769
- const { promisify: promisify4 } = await import("util");
6770
- const execFileAsync3 = promisify4(execFile3);
6771
- await execFileAsync3("gh", ["auth", "login", "--web"]);
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: validateGitUrl
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(chalk5.red(str))
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(chalk5.bold("Get started with tuck:\n"));
10754
- console.log(chalk5.cyan(" tuck init") + chalk5.dim(" - Set up tuck and create a GitHub repo"));
10755
- console.log(chalk5.cyan(" tuck scan") + chalk5.dim(" - Find dotfiles to track"));
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(chalk5.dim("On a new machine:"));
10758
- console.log(chalk5.cyan(" tuck apply <username>") + chalk5.dim(" - Apply your dotfiles"));
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(chalk5.bold("Status:\n"));
10768
- console.log(` Tracked files: ${chalk5.cyan(trackedCount.toString())}`);
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: ${chalk5.yellow(pendingChanges.toString())}`);
12737
+ console.log(` Pending changes: ${chalk6.yellow(pendingChanges.toString())}`);
10772
12738
  } else {
10773
- console.log(` Pending changes: ${chalk5.dim("none")}`);
12739
+ console.log(` Pending changes: ${chalk6.dim("none")}`);
10774
12740
  }
10775
12741
  if (gitStatus.ahead > 0) {
10776
- console.log(` Commits to push: ${chalk5.yellow(gitStatus.ahead.toString())}`);
12742
+ console.log(` Commits to push: ${chalk6.yellow(gitStatus.ahead.toString())}`);
10777
12743
  }
10778
12744
  console.log();
10779
- console.log(chalk5.bold("Next steps:\n"));
12745
+ console.log(chalk6.bold("Next steps:\n"));
10780
12746
  if (trackedCount === 0) {
10781
- console.log(chalk5.cyan(" tuck scan") + chalk5.dim(" - Find dotfiles to track"));
10782
- console.log(chalk5.cyan(" tuck add <file>") + chalk5.dim(" - Track a specific file"));
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(chalk5.cyan(" tuck sync") + chalk5.dim(" - Commit and push your changes"));
10785
- console.log(chalk5.cyan(" tuck diff") + chalk5.dim(" - Preview what changed"));
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(chalk5.cyan(" tuck push") + chalk5.dim(" - Push commits to GitHub"));
12753
+ console.log(chalk6.cyan(" tuck push") + chalk6.dim(" - Push commits to GitHub"));
10788
12754
  } else {
10789
- console.log(chalk5.dim(" All synced! Your dotfiles are up to date."));
12755
+ console.log(chalk6.dim(" All synced! Your dotfiles are up to date."));
10790
12756
  console.log();
10791
- console.log(chalk5.cyan(" tuck scan") + chalk5.dim(" - Find more dotfiles to track"));
10792
- console.log(chalk5.cyan(" tuck list") + chalk5.dim(" - See tracked files"));
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(chalk5.yellow("Tuck directory exists but may be corrupted."));
10798
- console.log(chalk5.dim("Run `tuck init` to reinitialize."));
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
- if (!hasCommand && !process.argv.includes("--help") && !process.argv.includes("-h") && !process.argv.includes("--version") && !process.argv.includes("-v")) {
10808
- runDefaultAction().catch(handleError);
10809
- } else {
10810
- program.parseAsync(process.argv).catch(handleError);
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