@prnv/tuck 1.2.0 → 1.2.1

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
@@ -365,12 +365,22 @@ var init_ui = __esm({
365
365
 
366
366
  // src/constants.ts
367
367
  import { homedir } from "os";
368
- import { join } from "path";
369
- var VERSION, DESCRIPTION, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES, COMMON_DOTFILES;
368
+ import { join, dirname } from "path";
369
+ import { readFileSync } from "fs";
370
+ import { fileURLToPath } from "url";
371
+ var __dirname, packageJsonPath, VERSION_VALUE, VERSION, DESCRIPTION, HOME_DIR, DEFAULT_TUCK_DIR, MANIFEST_FILE, CONFIG_FILE, BACKUP_DIR, FILES_DIR, CATEGORIES;
370
372
  var init_constants = __esm({
371
373
  "src/constants.ts"() {
372
374
  "use strict";
373
- VERSION = "0.1.0";
375
+ __dirname = dirname(fileURLToPath(import.meta.url));
376
+ packageJsonPath = join(__dirname, "..", "package.json");
377
+ VERSION_VALUE = "1.0.0";
378
+ try {
379
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
380
+ VERSION_VALUE = pkg.version;
381
+ } catch {
382
+ }
383
+ VERSION = VERSION_VALUE;
374
384
  DESCRIPTION = "Modern dotfiles manager with a beautiful CLI";
375
385
  HOME_DIR = homedir();
376
386
  DEFAULT_TUCK_DIR = join(HOME_DIR, ".tuck");
@@ -430,23 +440,12 @@ var init_constants = __esm({
430
440
  icon: "-"
431
441
  }
432
442
  };
433
- COMMON_DOTFILES = [
434
- { path: "~/.zshrc", category: "shell" },
435
- { path: "~/.bashrc", category: "shell" },
436
- { path: "~/.bash_profile", category: "shell" },
437
- { path: "~/.gitconfig", category: "git" },
438
- { path: "~/.config/nvim", category: "editors" },
439
- { path: "~/.vimrc", category: "editors" },
440
- { path: "~/.tmux.conf", category: "terminal" },
441
- { path: "~/.ssh/config", category: "ssh" },
442
- { path: "~/.config/starship.toml", category: "terminal" }
443
- ];
444
443
  }
445
444
  });
446
445
 
447
446
  // src/lib/paths.ts
448
447
  import { homedir as homedir2 } from "os";
449
- import { join as join2, basename, dirname, relative, isAbsolute, resolve } from "path";
448
+ import { join as join2, basename, dirname as dirname2, relative, isAbsolute, resolve } from "path";
450
449
  import { stat, access } from "fs/promises";
451
450
  import { constants } from "fs";
452
451
  var expandPath, collapsePath, getTuckDir, getManifestPath, getConfigPath, getFilesDir, getCategoryDir, getDestinationPath, getRelativeDestination, sanitizeFilename, detectCategory, pathExists, isDirectory, isPathWithinHome, validateSafeSourcePath, generateFileId;
@@ -1160,9 +1159,9 @@ var init_git = __esm({
1160
1159
  const remote = options?.remote || "origin";
1161
1160
  const branch = options?.branch;
1162
1161
  if (branch) {
1163
- await git.push([remote, branch, ...args]);
1162
+ await git.push([...args, remote, branch]);
1164
1163
  } else {
1165
- await git.push([remote, ...args]);
1164
+ await git.push([...args, remote]);
1166
1165
  }
1167
1166
  } catch (error) {
1168
1167
  throw new GitError("Failed to push", String(error));
@@ -1234,9 +1233,15 @@ var init_git = __esm({
1234
1233
  try {
1235
1234
  const git = createGit(dir);
1236
1235
  const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
1237
- return branch;
1238
- } catch (error) {
1239
- throw new GitError("Failed to get current branch", String(error));
1236
+ return branch.trim();
1237
+ } catch {
1238
+ try {
1239
+ const git = createGit(dir);
1240
+ const ref = await git.raw(["symbolic-ref", "--short", "HEAD"]);
1241
+ return ref.trim();
1242
+ } catch {
1243
+ return "main";
1244
+ }
1240
1245
  }
1241
1246
  };
1242
1247
  hasRemote = async (dir, name = "origin") => {
@@ -1480,7 +1485,7 @@ var init_github = __esm({
1480
1485
  import { createHash } from "crypto";
1481
1486
  import { readFile as readFile5, stat as stat4, readdir as readdir3, copyFile, symlink, unlink, rm as rm3 } from "fs/promises";
1482
1487
  import { copy as copy3, ensureDir as ensureDir3 } from "fs-extra";
1483
- import { join as join7, dirname as dirname3 } from "path";
1488
+ import { join as join7, dirname as dirname4 } from "path";
1484
1489
  var getFileChecksum, getFileInfo, getDirectoryFiles, getDirectoryFileCount, copyFileOrDir, createSymlink, deleteFileOrDir;
1485
1490
  var init_files = __esm({
1486
1491
  "src/lib/files.ts"() {
@@ -1546,7 +1551,7 @@ var init_files = __esm({
1546
1551
  if (!await pathExists(expandedSource)) {
1547
1552
  throw new FileNotFoundError(source);
1548
1553
  }
1549
- await ensureDir3(dirname3(expandedDest));
1554
+ await ensureDir3(dirname4(expandedDest));
1550
1555
  const sourceIsDir = await isDirectory(expandedSource);
1551
1556
  try {
1552
1557
  if (sourceIsDir) {
@@ -1574,7 +1579,7 @@ var init_files = __esm({
1574
1579
  if (!await pathExists(expandedTarget)) {
1575
1580
  throw new FileNotFoundError(target);
1576
1581
  }
1577
- await ensureDir3(dirname3(expandedLink));
1582
+ await ensureDir3(dirname4(expandedLink));
1578
1583
  if (options?.overwrite && await pathExists(expandedLink)) {
1579
1584
  await unlink(expandedLink);
1580
1585
  }
@@ -2239,7 +2244,7 @@ var detectDotfiles = async () => {
2239
2244
  // src/lib/timemachine.ts
2240
2245
  init_paths();
2241
2246
  init_errors();
2242
- import { join as join5, dirname as dirname2 } from "path";
2247
+ import { join as join5, dirname as dirname3 } from "path";
2243
2248
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3, rm, stat as stat3 } from "fs/promises";
2244
2249
  import { copy, ensureDir, pathExists as pathExists2 } from "fs-extra";
2245
2250
  import { homedir as homedir3 } from "os";
@@ -2273,7 +2278,7 @@ var createSnapshot = async (filePaths, reason, profile) => {
2273
2278
  const backupPath = join5(snapshotPath, "files", backupRelativePath);
2274
2279
  const existed = await pathExists(expandedPath);
2275
2280
  if (existed) {
2276
- await ensureDir(dirname2(backupPath));
2281
+ await ensureDir(dirname3(backupPath));
2277
2282
  await copy(expandedPath, backupPath, { overwrite: true, preserveTimestamps: true });
2278
2283
  }
2279
2284
  files.push({
@@ -2382,7 +2387,7 @@ var restoreSnapshot = async (snapshotId) => {
2382
2387
  continue;
2383
2388
  }
2384
2389
  if (await pathExists2(file.backupPath)) {
2385
- await ensureDir(dirname2(file.originalPath));
2390
+ await ensureDir(dirname3(file.originalPath));
2386
2391
  await copy(file.backupPath, file.originalPath, { overwrite: true, preserveTimestamps: true });
2387
2392
  restoredFiles.push(file.originalPath);
2388
2393
  }
@@ -2410,7 +2415,7 @@ var restoreFileFromSnapshot = async (snapshotId, filePath) => {
2410
2415
  if (!await pathExists2(file.backupPath)) {
2411
2416
  throw new BackupError(`Backup file is missing: ${file.backupPath}`);
2412
2417
  }
2413
- await ensureDir(dirname2(file.originalPath));
2418
+ await ensureDir(dirname3(file.originalPath));
2414
2419
  await copy(file.backupPath, file.originalPath, { overwrite: true, preserveTimestamps: true });
2415
2420
  return true;
2416
2421
  };
@@ -3070,30 +3075,52 @@ var runInteractiveInit = async () => {
3070
3075
  prompts.log.info("Run `tuck restore --all` to restore all dotfiles");
3071
3076
  }
3072
3077
  } else {
3073
- const existingDotfiles = [];
3074
- for (const df of COMMON_DOTFILES) {
3075
- const fullPath = expandPath(df.path);
3076
- if (await pathExists(fullPath)) {
3077
- existingDotfiles.push({
3078
- path: df.path,
3079
- label: `${df.path} (${df.category})`
3080
- });
3081
- }
3082
- }
3083
3078
  await initFromScratch(tuckDir, {});
3084
- if (existingDotfiles.length > 0) {
3085
- const selectedFiles = await prompts.multiselect(
3086
- "Would you like to track some common dotfiles?",
3087
- existingDotfiles.map((f) => ({
3079
+ const scanSpinner = prompts.spinner();
3080
+ scanSpinner.start("Scanning for dotfiles...");
3081
+ const detectedFiles = await detectDotfiles();
3082
+ const nonSensitiveFiles = detectedFiles.filter((f) => !f.sensitive);
3083
+ scanSpinner.stop(`Found ${nonSensitiveFiles.length} dotfiles on your system`);
3084
+ if (nonSensitiveFiles.length > 0) {
3085
+ const grouped = {};
3086
+ for (const file of nonSensitiveFiles) {
3087
+ if (!grouped[file.category]) grouped[file.category] = [];
3088
+ grouped[file.category].push(file);
3089
+ }
3090
+ console.log();
3091
+ const categoryOrder = ["shell", "git", "editors", "terminal", "ssh", "misc"];
3092
+ const sortedCategories = Object.keys(grouped).sort((a, b) => {
3093
+ const aIdx = categoryOrder.indexOf(a);
3094
+ const bIdx = categoryOrder.indexOf(b);
3095
+ if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
3096
+ if (aIdx === -1) return 1;
3097
+ if (bIdx === -1) return -1;
3098
+ return aIdx - bIdx;
3099
+ });
3100
+ for (const category of sortedCategories) {
3101
+ const files = grouped[category];
3102
+ const config = DETECTION_CATEGORIES[category] || { icon: "-", name: category };
3103
+ console.log(` ${config.icon} ${config.name}: ${files.length} files`);
3104
+ }
3105
+ console.log();
3106
+ const trackNow = await prompts.confirm("Would you like to track some of these now?", true);
3107
+ if (trackNow) {
3108
+ const options = nonSensitiveFiles.slice(0, 25).map((f) => ({
3088
3109
  value: f.path,
3089
- label: f.label
3090
- }))
3091
- );
3092
- if (selectedFiles.length > 0) {
3093
- prompts.log.step(
3094
- `Run the following to track these files:
3095
- tuck add ${selectedFiles.join(" ")}`
3110
+ label: `${collapsePath(f.path)} (${f.category})`
3111
+ }));
3112
+ const selectedFiles = await prompts.multiselect(
3113
+ "Select files to track:",
3114
+ options
3096
3115
  );
3116
+ if (selectedFiles.length > 0) {
3117
+ prompts.log.step(
3118
+ `Run the following to track these files:
3119
+ tuck add ${selectedFiles.join(" ")}`
3120
+ );
3121
+ }
3122
+ } else {
3123
+ prompts.log.info("Run 'tuck scan' later to interactively add files");
3097
3124
  }
3098
3125
  }
3099
3126
  const wantsRemote = await prompts.confirm("Would you like to set up a remote repository?");
@@ -3618,13 +3645,32 @@ var runInteractivePush = async (tuckDir) => {
3618
3645
  return;
3619
3646
  }
3620
3647
  const needsUpstream = !status.tracking;
3621
- await withSpinner("Pushing...", async () => {
3622
- await push(tuckDir, {
3623
- setUpstream: needsUpstream,
3624
- branch: needsUpstream ? branch : void 0
3648
+ try {
3649
+ await withSpinner("Pushing...", async () => {
3650
+ await push(tuckDir, {
3651
+ setUpstream: needsUpstream,
3652
+ branch: needsUpstream ? branch : void 0
3653
+ });
3625
3654
  });
3626
- });
3627
- prompts.log.success("Pushed successfully!");
3655
+ prompts.log.success("Pushed successfully!");
3656
+ } catch (error) {
3657
+ const errorMsg = error instanceof Error ? error.message : String(error);
3658
+ if (errorMsg.includes("Permission denied") || errorMsg.includes("publickey")) {
3659
+ prompts.log.error("Authentication failed");
3660
+ prompts.log.info("Check your SSH keys with: ssh -T git@github.com");
3661
+ prompts.log.info("Or try switching to HTTPS: git remote set-url origin https://...");
3662
+ } else if (errorMsg.includes("Could not resolve host") || errorMsg.includes("Network")) {
3663
+ prompts.log.error("Network error - could not reach remote");
3664
+ prompts.log.info("Check your internet connection and try again");
3665
+ } else if (errorMsg.includes("rejected") || errorMsg.includes("non-fast-forward")) {
3666
+ prompts.log.error("Push rejected - remote has changes");
3667
+ prompts.log.info("Run 'tuck pull' first, then push again");
3668
+ prompts.log.info("Or use 'tuck push --force' to overwrite (use with caution)");
3669
+ } else {
3670
+ prompts.log.error(`Push failed: ${errorMsg}`);
3671
+ }
3672
+ return;
3673
+ }
3628
3674
  if (remoteUrl) {
3629
3675
  let viewUrl = remoteUrl;
3630
3676
  if (remoteUrl.startsWith("git@github.com:")) {
@@ -3651,14 +3697,27 @@ var runPush = async (options) => {
3651
3697
  throw new GitError("No remote configured", "Run 'tuck init -r <url>' or add a remote manually");
3652
3698
  }
3653
3699
  const branch = await getCurrentBranch(tuckDir);
3654
- await withSpinner("Pushing...", async () => {
3655
- await push(tuckDir, {
3656
- force: options.force,
3657
- setUpstream: Boolean(options.setUpstream),
3658
- branch: options.setUpstream || branch
3700
+ try {
3701
+ await withSpinner("Pushing...", async () => {
3702
+ await push(tuckDir, {
3703
+ force: options.force,
3704
+ setUpstream: Boolean(options.setUpstream),
3705
+ branch: options.setUpstream || branch
3706
+ });
3659
3707
  });
3660
- });
3661
- logger.success("Pushed successfully!");
3708
+ logger.success("Pushed successfully!");
3709
+ } catch (error) {
3710
+ const errorMsg = error instanceof Error ? error.message : String(error);
3711
+ if (errorMsg.includes("Permission denied") || errorMsg.includes("publickey")) {
3712
+ throw new GitError("Authentication failed", "Check your SSH keys: ssh -T git@github.com");
3713
+ } else if (errorMsg.includes("Could not resolve host") || errorMsg.includes("Network")) {
3714
+ throw new GitError("Network error", "Check your internet connection");
3715
+ } else if (errorMsg.includes("rejected") || errorMsg.includes("non-fast-forward")) {
3716
+ throw new GitError("Push rejected", "Run 'tuck pull' first, or use --force");
3717
+ } else {
3718
+ throw new GitError("Push failed", errorMsg);
3719
+ }
3720
+ }
3662
3721
  };
3663
3722
  var pushCommand = new Command5("push").description("Push changes to remote repository").option("-f, --force", "Force push").option("--set-upstream <name>", "Set upstream branch").action(async (options) => {
3664
3723
  await runPush(options);
@@ -3999,8 +4058,10 @@ init_manifest();
3999
4058
  init_git();
4000
4059
  init_files();
4001
4060
  init_errors();
4061
+ init_constants();
4002
4062
  import { Command as Command8 } from "commander";
4003
4063
  import chalk13 from "chalk";
4064
+ import boxen2 from "boxen";
4004
4065
  var detectFileChanges = async (tuckDir) => {
4005
4066
  const files = await getAllTrackedFiles(tuckDir);
4006
4067
  const changes = [];
@@ -4055,6 +4116,10 @@ var getFullStatus = async (tuckDir) => {
4055
4116
  }
4056
4117
  }
4057
4118
  const fileChanges = await detectFileChanges(tuckDir);
4119
+ const categoryCounts = {};
4120
+ for (const file of Object.values(manifest.files)) {
4121
+ categoryCounts[file.category] = (categoryCounts[file.category] || 0) + 1;
4122
+ }
4058
4123
  return {
4059
4124
  tuckDir,
4060
4125
  branch,
@@ -4063,6 +4128,7 @@ var getFullStatus = async (tuckDir) => {
4063
4128
  ahead: gitStatus.ahead,
4064
4129
  behind: gitStatus.behind,
4065
4130
  trackedCount: Object.keys(manifest.files).length,
4131
+ categoryCounts,
4066
4132
  changes: fileChanges,
4067
4133
  gitChanges: {
4068
4134
  staged: gitStatus.staged,
@@ -4072,33 +4138,58 @@ var getFullStatus = async (tuckDir) => {
4072
4138
  };
4073
4139
  };
4074
4140
  var printStatus = (status) => {
4075
- prompts.intro("tuck status");
4076
- console.log();
4077
- console.log(chalk13.dim("Repository:"), collapsePath(status.tuckDir));
4078
- console.log(chalk13.dim("Branch:"), chalk13.cyan(status.branch));
4141
+ const headerLines = [
4142
+ `${chalk13.bold.cyan("tuck")} ${chalk13.dim(`v${VERSION}`)}`,
4143
+ "",
4144
+ `${chalk13.dim("Repository:")} ${collapsePath(status.tuckDir)}`,
4145
+ `${chalk13.dim("Branch:")} ${chalk13.cyan(status.branch)}`
4146
+ ];
4147
+ if (status.remote) {
4148
+ const shortRemote = status.remote.length > 40 ? status.remote.replace(/^https?:\/\//, "").replace(/\.git$/, "") : status.remote;
4149
+ headerLines.push(`${chalk13.dim("Remote:")} ${shortRemote}`);
4150
+ } else {
4151
+ headerLines.push(`${chalk13.dim("Remote:")} ${chalk13.yellow("not configured")}`);
4152
+ }
4153
+ console.log(boxen2(headerLines.join("\n"), {
4154
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
4155
+ borderColor: "cyan",
4156
+ borderStyle: "round"
4157
+ }));
4079
4158
  if (status.remote) {
4080
- console.log(chalk13.dim("Remote:"), status.remote);
4081
4159
  let remoteInfo = "";
4082
4160
  switch (status.remoteStatus) {
4083
4161
  case "up-to-date":
4084
- remoteInfo = chalk13.green("up to date");
4162
+ remoteInfo = chalk13.green("\u2713 Up to date with remote");
4085
4163
  break;
4086
4164
  case "ahead":
4087
- remoteInfo = chalk13.yellow(`${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead`);
4165
+ remoteInfo = chalk13.yellow(`\u2191 ${status.ahead} commit${status.ahead > 1 ? "s" : ""} ahead of remote`);
4088
4166
  break;
4089
4167
  case "behind":
4090
- remoteInfo = chalk13.yellow(`${status.behind} commit${status.behind > 1 ? "s" : ""} behind`);
4168
+ remoteInfo = chalk13.yellow(`\u2193 ${status.behind} commit${status.behind > 1 ? "s" : ""} behind remote`);
4091
4169
  break;
4092
4170
  case "diverged":
4093
- remoteInfo = chalk13.red(`diverged (${status.ahead} ahead, ${status.behind} behind)`);
4171
+ remoteInfo = chalk13.red(`\u26A0 Diverged (${status.ahead} ahead, ${status.behind} behind)`);
4094
4172
  break;
4095
4173
  }
4096
- console.log(chalk13.dim("Status:"), remoteInfo);
4097
- } else {
4098
- console.log(chalk13.dim("Remote:"), chalk13.yellow("not configured"));
4174
+ console.log("\n" + remoteInfo);
4099
4175
  }
4100
4176
  console.log();
4101
- console.log(chalk13.dim("Tracked files:"), status.trackedCount);
4177
+ console.log(chalk13.bold(`Tracked Files: ${status.trackedCount}`));
4178
+ const categoryOrder = ["shell", "git", "editors", "terminal", "ssh", "misc"];
4179
+ const sortedCategories = Object.keys(status.categoryCounts).sort((a, b) => {
4180
+ const aIdx = categoryOrder.indexOf(a);
4181
+ const bIdx = categoryOrder.indexOf(b);
4182
+ if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
4183
+ if (aIdx === -1) return 1;
4184
+ if (bIdx === -1) return -1;
4185
+ return aIdx - bIdx;
4186
+ });
4187
+ if (sortedCategories.length > 0) {
4188
+ for (const category of sortedCategories) {
4189
+ const count = status.categoryCounts[category];
4190
+ console.log(chalk13.dim(` ${category}: ${count} file${count > 1 ? "s" : ""}`));
4191
+ }
4192
+ }
4102
4193
  if (status.changes.length > 0) {
4103
4194
  console.log();
4104
4195
  console.log(chalk13.bold("Changes detected:"));
@@ -4938,8 +5029,8 @@ var applyWithMerge = async (files, dryRun) => {
4938
5029
  } else {
4939
5030
  const { writeFile: writeFile5 } = await import("fs/promises");
4940
5031
  const { ensureDir: ensureDir6 } = await import("fs-extra");
4941
- const { dirname: dirname4 } = await import("path");
4942
- await ensureDir6(dirname4(file.destination));
5032
+ const { dirname: dirname5 } = await import("path");
5033
+ await ensureDir6(dirname5(file.destination));
4943
5034
  await writeFile5(file.destination, mergeResult.content, "utf-8");
4944
5035
  logger.file("merge", collapsePath(file.destination));
4945
5036
  }