@prnv/tuck 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,30 @@
1
1
  <div align="center">
2
- <img src="public/tuck.png" alt="tuck logo" width="200">
3
- </div>
4
-
5
- # tuck
6
-
7
- > Modern dotfiles manager with a beautiful CLI
2
+ <img src="public/tuck.png" alt="tuck logo" width="180">
3
+
4
+ # tuck
5
+
6
+ **The modern dotfiles manager**
7
+
8
+ Simple, fast, and beautiful. Manage your dotfiles with Git, sync across machines, and never lose your configs again.
8
9
 
9
10
  [![npm version](https://img.shields.io/npm/v/@prnv/tuck.svg)](https://www.npmjs.com/package/@prnv/tuck)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
12
  [![CI](https://github.com/Pranav-Karra-3301/tuck/actions/workflows/ci.yml/badge.svg)](https://github.com/Pranav-Karra-3301/tuck/actions/workflows/ci.yml)
12
13
 
13
- ## Features
14
+ [Website](https://tuck.sh) · [Install](#installation) · [Quick Start](#quick-start) · [Commands](#commands)
15
+
16
+ </div>
17
+
18
+ ---
14
19
 
15
- - **Beautiful CLI** - Gorgeous prompts, spinners, and colors powered by @clack/prompts
16
- - **Git-native** - Uses git under the hood but abstracts complexity
17
- - **Organized** - Auto-categorizes your dotfiles (shell, git, editors, terminal, etc.)
18
- - **Safe** - Never overwrites without confirmation, always creates backups
19
- - **Fast** - Written in TypeScript, runs on Node.js 18+
20
- - **Cross-platform** - Works on macOS and Linux
20
+ ## Why tuck?
21
+
22
+ - **One command to rule them all** `tuck init` scans your system, lets you pick what to track, and syncs to GitHub
23
+ - **Smart detection** — Auto-categorizes dotfiles (shell, git, editors, terminal, ssh, etc.)
24
+ - **Beautiful CLI** Gorgeous prompts, spinners, and progress bars powered by @clack/prompts
25
+ - **Safe by default** Creates backups before every operation, never overwrites without asking
26
+ - **Git-native** — Uses Git under the hood but hides the complexity
27
+ - **Cross-platform** — Works on macOS and Linux
21
28
 
22
29
  ## Installation
23
30
 
@@ -25,133 +32,166 @@
25
32
  # npm
26
33
  npm install -g @prnv/tuck
27
34
 
35
+ # Homebrew (macOS/Linux)
36
+ brew install prnv/tap/tuck
37
+
28
38
  # pnpm
29
39
  pnpm add -g @prnv/tuck
30
40
 
31
41
  # yarn
32
42
  yarn global add @prnv/tuck
33
-
34
- # Homebrew (macOS)
35
- brew tap pranav-karra-3301/tuck
36
- brew install tuck
37
43
  ```
38
44
 
39
45
  ## Quick Start
40
46
 
47
+ ### First time setup
48
+
41
49
  ```bash
42
- # Initialize tuck (interactive)
50
+ # Interactive setup - scans your system, pick what to track, syncs to GitHub
43
51
  tuck init
52
+ ```
53
+
54
+ That's it! `tuck init` does everything:
44
55
 
45
- # Or initialize with a remote repository
46
- tuck init --from git@github.com:username/dotfiles.git
56
+ 1. Creates `~/.tuck` repository
57
+ 2. Scans your system for dotfiles
58
+ 3. Lets you select which to track
59
+ 4. Creates a GitHub repo (optional)
60
+ 5. Commits and pushes
47
61
 
48
- # Add your dotfiles
49
- tuck add ~/.zshrc ~/.gitconfig ~/.config/nvim
62
+ ### Ongoing workflow
50
63
 
51
- # Sync changes to repository
64
+ ```bash
65
+ # Detect changes, find new dotfiles, commit, and push - all in one
52
66
  tuck sync
67
+ ```
68
+
69
+ ### On a new machine
53
70
 
54
- # Push to remote
55
- tuck push
71
+ ```bash
72
+ # Apply dotfiles from any GitHub user
73
+ tuck apply username
74
+
75
+ # Or clone your own and restore
76
+ tuck init --from github.com/you/dotfiles
77
+ tuck restore --all
56
78
  ```
57
79
 
58
80
  ## Commands
59
81
 
60
- | Command | Description |
61
- |---------|-------------|
62
- | `tuck init` | Initialize tuck repository |
63
- | `tuck add <paths>` | Track new dotfiles |
64
- | `tuck remove <paths>` | Stop tracking dotfiles |
65
- | `tuck sync` | Sync changes to repository |
66
- | `tuck push` | Push to remote |
82
+ ### Essential (what you'll use 99% of the time)
83
+
84
+ | Command | Description |
85
+ | ------------- | ----------------------------------------------------------------------- |
86
+ | `tuck init` | Set up tuck - scans for dotfiles, select what to track, syncs to GitHub |
87
+ | `tuck sync` | Detect changes + new files, commit, and push (pulls first if behind) |
88
+ | `tuck status` | See what's tracked, what's changed, and sync status |
89
+
90
+ ### Managing Files
91
+
92
+ | Command | Description |
93
+ | --------------------- | ---------------------------------- |
94
+ | `tuck add <paths>` | Manually track specific files |
95
+ | `tuck remove <paths>` | Stop tracking files |
96
+ | `tuck scan` | Discover dotfiles without syncing |
97
+ | `tuck list` | List all tracked files by category |
98
+ | `tuck diff [file]` | Show what's changed |
99
+
100
+ ### Syncing
101
+
102
+ | Command | Description |
103
+ | ----------- | ---------------- |
104
+ | `tuck push` | Push to remote |
67
105
  | `tuck pull` | Pull from remote |
68
- | `tuck restore` | Restore dotfiles to system |
69
- | `tuck status` | Show tracking status |
70
- | `tuck list` | List tracked files |
71
- | `tuck diff` | Show changes |
72
- | `tuck config` | Manage configuration |
106
+
107
+ ### Restoring
108
+
109
+ | Command | Description |
110
+ | ------------------- | ------------------------------------------------------ |
111
+ | `tuck apply <user>` | Apply dotfiles from a GitHub user (with smart merging) |
112
+ | `tuck restore` | Restore dotfiles from repo to system |
113
+ | `tuck undo` | Restore from Time Machine backup snapshots |
114
+
115
+ ### Configuration
116
+
117
+ | Command | Description |
118
+ | -------------------- | ------------------------------- |
119
+ | `tuck config` | View/edit configuration |
120
+ | `tuck config wizard` | Interactive configuration setup |
73
121
 
74
122
  ## How It Works
75
123
 
76
- Tuck stores your dotfiles in `~/.tuck` (configurable), organized by category:
124
+ tuck stores your dotfiles in `~/.tuck`, organized by category:
77
125
 
78
126
  ```
79
127
  ~/.tuck/
80
128
  ├── files/
81
- │ ├── shell/ # .zshrc, .bashrc, etc.
129
+ │ ├── shell/ # .zshrc, .bashrc, .profile
82
130
  │ ├── git/ # .gitconfig, .gitignore_global
83
- │ ├── editors/ # .vimrc, nvim config
84
- │ ├── terminal/ # .tmux.conf, alacritty config
85
- │ ├── ssh/ # ssh config
131
+ │ ├── editors/ # .vimrc, nvim, VS Code settings
132
+ │ ├── terminal/ # .tmux.conf, alacritty, kitty
133
+ │ ├── ssh/ # ssh config (never keys!)
86
134
  │ └── misc/ # everything else
87
- ├── .tuckmanifest.json # Tracks all managed files
88
- ├── .tuckrc.json # Tuck configuration
89
- └── README.md
135
+ ├── .tuckmanifest.json
136
+ └── .tuckrc.json
90
137
  ```
91
138
 
92
- When you run `tuck add ~/.zshrc`:
93
- 1. The file is copied to `~/.tuck/files/shell/zshrc`
94
- 2. An entry is added to the manifest with the source path and checksum
95
- 3. Run `tuck sync` to commit and `tuck push` to upload
139
+ **The flow:**
96
140
 
97
- When setting up a new machine:
98
- ```bash
99
- tuck init --from git@github.com:username/dotfiles.git
100
- tuck restore --all
141
+ ```
142
+ ~/.zshrc → ~/.tuck/files/shell/zshrc
143
+ ~/.gitconfig → ~/.tuck/files/git/gitconfig
144
+ ~/.config/nvim → ~/.tuck/files/editors/nvim
101
145
  ```
102
146
 
147
+ Run `tuck sync` anytime to detect changes and push. On a new machine, run `tuck apply username` to grab anyone's dotfiles.
148
+
103
149
  ## Configuration
104
150
 
105
- Tuck can be configured via `~/.tuck/.tuckrc.json`:
151
+ Configure tuck via `~/.tuck/.tuckrc.json` or `tuck config wizard`:
106
152
 
107
153
  ```json
108
154
  {
109
155
  "repository": {
110
- "path": "~/.tuck",
111
- "defaultBranch": "main",
112
156
  "autoCommit": true,
113
157
  "autoPush": false
114
158
  },
115
159
  "files": {
116
160
  "strategy": "copy",
117
- "backupOnRestore": true,
118
- "backupDir": "~/.tuck-backups"
119
- },
120
- "ui": {
121
- "colors": true,
122
- "emoji": true,
123
- "verbose": false
161
+ "backupOnRestore": true
124
162
  }
125
163
  }
126
164
  ```
127
165
 
128
166
  ### File Strategies
129
167
 
130
- - **copy** (default): Files are copied to the repository. Changes in your system don't affect the repo until you run `tuck sync`.
131
- - **symlink**: Files in your system are replaced with symlinks to the repository. Changes are immediate.
168
+ - **copy** (default) Files are copied. Run `tuck sync` to update the repo.
169
+ - **symlink** Files are symlinked. Changes are instant but require more care.
170
+
171
+ ## Security
132
172
 
133
- ## Restoring on a New Machine
173
+ tuck is designed with security in mind:
174
+
175
+ - **Never tracks private keys** — SSH keys, `.env` files, and credentials are blocked by default
176
+ - **Secret scanning** — Warns if files contain API keys or tokens
177
+ - **Placeholder support** — Replace secrets with `{{PLACEHOLDER}}` syntax
178
+ - **Local secrets** — Store actual values in `secrets.local.json` (never committed)
134
179
 
135
180
  ```bash
136
- # Option 1: Clone and restore in one step
137
- tuck init --from git@github.com:username/dotfiles.git
138
- tuck restore --all
181
+ # Scan tracked files for secrets
182
+ tuck secrets scan
139
183
 
140
- # Option 2: Clone manually
141
- git clone git@github.com:username/dotfiles.git ~/.tuck
142
- tuck restore --all
184
+ # Set a secret value locally
185
+ tuck secrets set API_KEY "your-actual-key"
143
186
  ```
144
187
 
145
188
  ## Hooks
146
189
 
147
- Run custom commands before/after sync or restore:
190
+ Run custom commands before/after operations:
148
191
 
149
192
  ```json
150
193
  {
151
194
  "hooks": {
152
- "preSync": "echo 'About to sync...'",
153
- "postSync": "echo 'Sync complete!'",
154
- "preRestore": "echo 'Backing up...'",
155
195
  "postRestore": "source ~/.zshrc"
156
196
  }
157
197
  }
@@ -160,26 +200,23 @@ Run custom commands before/after sync or restore:
160
200
  ## Development
161
201
 
162
202
  ```bash
163
- # Clone the repository
164
203
  git clone https://github.com/Pranav-Karra-3301/tuck.git
165
204
  cd tuck
166
-
167
- # Install dependencies
168
205
  pnpm install
169
-
170
- # Build
171
206
  pnpm build
172
-
173
- # Run locally
174
- node dist/index.js --help
175
-
176
- # Run tests
177
207
  pnpm test
178
-
179
- # Lint
180
- pnpm lint
181
208
  ```
182
209
 
210
+ ## Contributing
211
+
212
+ Contributions are welcome! Please read our contributing guidelines and submit PRs to the `main` branch.
213
+
183
214
  ## License
184
215
 
185
- MIT - see [LICENSE](LICENSE)
216
+ MIT see [LICENSE](LICENSE)
217
+
218
+ ---
219
+
220
+ <div align="center">
221
+ <sub>Made with love in San Francisco and State College</sub>
222
+ </div>
package/dist/index.js CHANGED
@@ -8674,6 +8674,7 @@ var listCommand = new Command10("list").description("List all tracked files").op
8674
8674
 
8675
8675
  // src/commands/diff.ts
8676
8676
  init_ui();
8677
+ init_theme();
8677
8678
  init_paths();
8678
8679
  init_manifest();
8679
8680
  init_git();
@@ -8701,26 +8702,60 @@ var getFileDiff = async (tuckDir, source) => {
8701
8702
  destination: tracked.file.destination,
8702
8703
  hasChanges: false
8703
8704
  };
8704
- if (!await pathExists(systemPath)) {
8705
+ const systemExists = await pathExists(systemPath);
8706
+ const repoExists2 = await pathExists(repoPath);
8707
+ if (!systemExists) {
8705
8708
  diff.hasChanges = true;
8706
- if (await pathExists(repoPath)) {
8707
- const repoContent = await readFile9(repoPath, "utf-8");
8708
- diff.repoContent = repoContent;
8709
- diff.repoSize = repoContent.length;
8709
+ if (repoExists2) {
8710
+ if (await isDirectory(repoPath)) {
8711
+ diff.isDirectory = true;
8712
+ const files = await getDirectoryFiles(repoPath);
8713
+ diff.fileCount = files.length;
8714
+ } else {
8715
+ const repoContent = await readFile9(repoPath, "utf-8");
8716
+ diff.repoContent = repoContent;
8717
+ diff.repoSize = repoContent.length;
8718
+ }
8710
8719
  }
8711
8720
  return diff;
8712
8721
  }
8713
- if (!await pathExists(repoPath)) {
8722
+ if (!repoExists2) {
8714
8723
  diff.hasChanges = true;
8715
- const systemContent = await readFile9(systemPath, "utf-8");
8716
- diff.systemContent = systemContent;
8717
- diff.systemSize = systemContent.length;
8724
+ if (await isDirectory(systemPath)) {
8725
+ diff.isDirectory = true;
8726
+ const files = await getDirectoryFiles(systemPath);
8727
+ diff.fileCount = files.length;
8728
+ } else {
8729
+ const systemContent = await readFile9(systemPath, "utf-8");
8730
+ diff.systemContent = systemContent;
8731
+ diff.systemSize = systemContent.length;
8732
+ }
8733
+ return diff;
8734
+ }
8735
+ const systemIsDir = await isDirectory(systemPath);
8736
+ const repoIsDir = await isDirectory(repoPath);
8737
+ if (systemIsDir || repoIsDir) {
8738
+ diff.isDirectory = true;
8739
+ if (systemIsDir) {
8740
+ const files = await getDirectoryFiles(systemPath);
8741
+ diff.fileCount = files.length;
8742
+ }
8743
+ if (repoIsDir) {
8744
+ const files = await getDirectoryFiles(repoPath);
8745
+ diff.fileCount = (diff.fileCount || 0) + files.length;
8746
+ }
8747
+ const systemChecksum2 = await getFileChecksum(systemPath);
8748
+ const repoChecksum2 = await getFileChecksum(repoPath);
8749
+ diff.hasChanges = systemChecksum2 !== repoChecksum2;
8718
8750
  return diff;
8719
8751
  }
8720
8752
  const systemIsBinary = await isBinary(systemPath);
8721
8753
  const repoIsBinary = await isBinary(repoPath);
8722
8754
  if (systemIsBinary || repoIsBinary) {
8723
8755
  diff.isBinary = true;
8756
+ const systemChecksum2 = await getFileChecksum(systemPath);
8757
+ const repoChecksum2 = await getFileChecksum(repoPath);
8758
+ diff.hasChanges = systemChecksum2 !== repoChecksum2;
8724
8759
  try {
8725
8760
  const systemBuffer = await readFile9(systemPath);
8726
8761
  diff.systemSize = systemBuffer.length;
@@ -8731,24 +8766,6 @@ var getFileDiff = async (tuckDir, source) => {
8731
8766
  diff.repoSize = repoBuffer.length;
8732
8767
  } catch {
8733
8768
  }
8734
- const systemChecksum2 = await getFileChecksum(systemPath);
8735
- const repoChecksum2 = await getFileChecksum(repoPath);
8736
- diff.hasChanges = systemChecksum2 !== repoChecksum2;
8737
- return diff;
8738
- }
8739
- const systemIsDir = await isDirectory(systemPath);
8740
- const repoIsDir = await isDirectory(repoPath);
8741
- if (systemIsDir || repoIsDir) {
8742
- diff.isDirectory = true;
8743
- diff.hasChanges = true;
8744
- if (systemIsDir) {
8745
- const files = await getDirectoryFiles(systemPath);
8746
- diff.fileCount = files.length;
8747
- }
8748
- if (repoIsDir) {
8749
- const files = await getDirectoryFiles(repoPath);
8750
- diff.fileCount = (diff.fileCount || 0) + files.length;
8751
- }
8752
8769
  return diff;
8753
8770
  }
8754
8771
  try {
@@ -8786,26 +8803,27 @@ var formatUnifiedDiff = (diff) => {
8786
8803
  return lines.join("\n");
8787
8804
  }
8788
8805
  const { systemContent, repoContent } = diff;
8789
- if (!systemContent && repoContent) {
8806
+ const systemMissing = systemContent === void 0;
8807
+ const repoMissing = repoContent === void 0;
8808
+ if (systemMissing && !repoMissing) {
8790
8809
  lines.push(colors.red("File missing on system"));
8791
8810
  lines.push(colors.dim("Repository content:"));
8792
8811
  repoContent.split("\n").forEach((line) => {
8793
8812
  lines.push(colors.green(`+ ${line}`));
8794
8813
  });
8795
- } else if (systemContent && !repoContent) {
8814
+ } else if (!systemMissing && repoMissing) {
8796
8815
  lines.push(colors.yellow("File not yet synced to repository"));
8797
8816
  lines.push(colors.dim("System content:"));
8798
8817
  systemContent.split("\n").forEach((line) => {
8799
8818
  lines.push(colors.red(`- ${line}`));
8800
8819
  });
8801
- } else if (systemContent && repoContent) {
8820
+ } else if (!systemMissing && !repoMissing) {
8802
8821
  const CONTEXT_LINES = 3;
8803
8822
  const systemLines = systemContent.split("\n");
8804
8823
  const repoLines = repoContent.split("\n");
8805
8824
  const maxLines = Math.max(systemLines.length, repoLines.length);
8806
8825
  let inDiff = false;
8807
8826
  let diffStart = 0;
8808
- let contextCount = 0;
8809
8827
  for (let i = 0; i < maxLines; i++) {
8810
8828
  const sysLine = systemLines[i];
8811
8829
  const repoLine = repoLines[i];
@@ -8814,12 +8832,19 @@ var formatUnifiedDiff = (diff) => {
8814
8832
  inDiff = true;
8815
8833
  diffStart = i;
8816
8834
  const startLine = Math.max(0, diffStart - CONTEXT_LINES + 1);
8817
- const endLine = Math.min(maxLines, diffStart + CONTEXT_LINES);
8835
+ const contextLineCount = Math.min(diffStart, CONTEXT_LINES);
8836
+ const endLine = Math.min(maxLines, diffStart + CONTEXT_LINES + 1);
8818
8837
  lines.push(
8819
8838
  colors.cyan(
8820
- `@@ -${startLine + 1},${endLine - startLine} +${startLine + 1},${endLine - startLine} @@`
8839
+ `@@ -${startLine + 1},${contextLineCount + 1} +${startLine + 1},${endLine - startLine} @@`
8821
8840
  )
8822
8841
  );
8842
+ for (let j = startLine; j < i; j++) {
8843
+ const ctxLine = systemLines[j];
8844
+ if (ctxLine !== void 0) {
8845
+ lines.push(colors.dim(` ${ctxLine}`));
8846
+ }
8847
+ }
8823
8848
  }
8824
8849
  if (sysLine !== void 0) {
8825
8850
  lines.push(colors.red(`- ${sysLine}`));
@@ -8827,13 +8852,12 @@ var formatUnifiedDiff = (diff) => {
8827
8852
  if (repoLine !== void 0) {
8828
8853
  lines.push(colors.green(`+ ${repoLine}`));
8829
8854
  }
8830
- contextCount = 0;
8831
8855
  } else if (inDiff) {
8832
- lines.push(colors.dim(` ${sysLine || ""}`));
8833
- contextCount++;
8834
- if (contextCount > CONTEXT_LINES * 2) {
8835
- inDiff = false;
8856
+ if (sysLine === repoLine && sysLine !== void 0) {
8857
+ lines.push(colors.dim(` ${sysLine}`));
8836
8858
  }
8859
+ } else {
8860
+ inDiff = false;
8837
8861
  }
8838
8862
  }
8839
8863
  }
@@ -10706,10 +10730,7 @@ init_git();
10706
10730
  var program = new Command16();
10707
10731
  program.name("tuck").description(DESCRIPTION).version(VERSION, "-v, --version", "Display version number").configureOutput({
10708
10732
  outputError: (str, write) => write(chalk5.red(str))
10709
- }).addHelpText("beforeAll", customHelp(VERSION)).helpOption("-h, --help", "Display this help message").showHelpAfterError(false);
10710
- program.configureHelp({
10711
- formatHelp: () => ""
10712
- });
10733
+ }).addHelpText("before", customHelp(VERSION)).helpOption("-h, --help", "Display this help message").showHelpAfterError(false);
10713
10734
  program.addCommand(initCommand);
10714
10735
  program.addCommand(addCommand);
10715
10736
  program.addCommand(removeCommand);
@@ -10778,9 +10799,7 @@ var runDefaultAction = async () => {
10778
10799
  console.log();
10779
10800
  }
10780
10801
  };
10781
- var hasCommand = process.argv.slice(2).some(
10782
- (arg) => !arg.startsWith("-") && arg !== "--help" && arg !== "-h"
10783
- );
10802
+ var hasCommand = process.argv.slice(2).some((arg) => !arg.startsWith("-") && arg !== "--help" && arg !== "-h");
10784
10803
  process.on("uncaughtException", handleError);
10785
10804
  process.on("unhandledRejection", (reason) => {
10786
10805
  handleError(reason instanceof Error ? reason : new Error(String(reason)));