@heyitsiveen/dotfiles 1.0.4 → 1.1.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.mjs CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner } from "@clack/prompts";
2
+ import { log } from "@clack/prompts";
3
3
  import { defineCommand, runMain } from "citty";
4
4
  import pc from "picocolors";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { basename, dirname, join, relative } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import fse from "fs-extra";
9
- import { readFile, readdir, stat, writeFile } from "node:fs/promises";
9
+ import { readdir, stat, writeFile } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
- import { execSync } from "node:child_process";
12
11
  //#region src/constants.ts
13
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
13
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
@@ -36,7 +35,7 @@ const ASCII_BANNER = ` ██╗ ██╗███████╗██╗
36
35
  ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═══╝`;
37
36
  //#endregion
38
37
  //#region src/installer.ts
39
- const { copy: copy$1, ensureDir: ensureDir$1, pathExists: pathExists$2, readJson, remove, writeJson } = fse;
38
+ const { copy, ensureDir, pathExists, readJson, remove, writeJson } = fse;
40
39
  function copyFilter(src) {
41
40
  return !COPY_EXCLUDE.has(basename(src));
42
41
  }
@@ -72,7 +71,7 @@ async function install(options) {
72
71
  const groupDir = join(home, BACKUP_DIR, group.name);
73
72
  let backupName = baseName;
74
73
  let counter = 2;
75
- while (await pathExists$2(join(groupDir, backupName))) {
74
+ while (await pathExists(join(groupDir, backupName))) {
76
75
  backupName = `${baseName}-${counter}`;
77
76
  counter++;
78
77
  }
@@ -80,7 +79,7 @@ async function install(options) {
80
79
  }
81
80
  for (const source of sources) {
82
81
  const sourcePath = join(platformDir, source);
83
- if (!await pathExists$2(sourcePath)) {
82
+ if (!await pathExists(sourcePath)) {
84
83
  result.errors.push({
85
84
  file: source,
86
85
  error: "Source not found"
@@ -88,11 +87,11 @@ async function install(options) {
88
87
  continue;
89
88
  }
90
89
  const targetPath = isMultiSource ? join(group.target, basename(source)) : group.target;
91
- if (backup && groupBackupDir && await pathExists$2(targetPath)) {
90
+ if (backup && groupBackupDir && await pathExists(targetPath)) {
92
91
  const backupTarget = group.extraBackupPaths && group.extraBackupPaths.length > 0 ? join(groupBackupDir, "config") : join(groupBackupDir, basename(source));
93
92
  if (!dryRun) try {
94
- await ensureDir$1(groupBackupDir);
95
- await copy$1(targetPath, backupTarget, { filter: copyFilter });
93
+ await ensureDir(groupBackupDir);
94
+ await copy(targetPath, backupTarget, { filter: copyFilter });
96
95
  result.backedUp.push(group.name);
97
96
  } catch (err) {
98
97
  result.errors.push({
@@ -104,12 +103,12 @@ async function install(options) {
104
103
  }
105
104
  if (!dryRun) try {
106
105
  const sourceIsDir = (await stat(sourcePath)).isDirectory();
107
- if (sourceIsDir) await ensureDir$1(targetPath);
108
- else await ensureDir$1(join(targetPath, ".."));
109
- if (await pathExists$2(targetPath)) {
106
+ if (sourceIsDir) await ensureDir(targetPath);
107
+ else await ensureDir(join(targetPath, ".."));
108
+ if (await pathExists(targetPath)) {
110
109
  if (sourceIsDir !== (await stat(targetPath)).isDirectory()) await remove(targetPath);
111
110
  }
112
- await copy$1(sourcePath, targetPath, {
111
+ await copy(sourcePath, targetPath, {
113
112
  filter: copyFilter,
114
113
  overwrite: true
115
114
  });
@@ -126,11 +125,11 @@ async function install(options) {
126
125
  else installedFiles.push(`${sourcePath} → ${targetPath}`);
127
126
  }
128
127
  if (backup && groupBackupDir && group.extraBackupPaths) {
129
- for (const extra of group.extraBackupPaths) if (await pathExists$2(extra.path)) {
128
+ for (const extra of group.extraBackupPaths) if (await pathExists(extra.path)) {
130
129
  const extraTarget = join(groupBackupDir, extra.label);
131
130
  if (!dryRun) try {
132
- await ensureDir$1(extraTarget);
133
- await copy$1(extra.path, extraTarget, { filter: copyFilter });
131
+ await ensureDir(extraTarget);
132
+ await copy(extra.path, extraTarget, { filter: copyFilter });
134
133
  } catch (err) {
135
134
  result.errors.push({
136
135
  file: `backup:${group.name}/${extra.label}`,
@@ -154,13 +153,18 @@ function getManifestPath() {
154
153
  }
155
154
  async function readManifest() {
156
155
  const manifestPath = getManifestPath();
157
- if (await pathExists$2(manifestPath)) try {
156
+ if (await pathExists(manifestPath)) try {
158
157
  return await readJson(manifestPath);
159
158
  } catch {
160
159
  return null;
161
160
  }
162
161
  return null;
163
162
  }
163
+ async function writeManifestAtomic(manifestPath, manifest) {
164
+ const tmpPath = `${manifestPath}.tmp`;
165
+ await writeJson(tmpPath, manifest, { spaces: 2 });
166
+ await fse.rename(tmpPath, manifestPath);
167
+ }
164
168
  async function createManifest(result, options) {
165
169
  const manifest = {
166
170
  version: VERSION,
@@ -170,8 +174,8 @@ async function createManifest(result, options) {
170
174
  groups: result.installedGroups
171
175
  };
172
176
  const manifestPath = getManifestPath();
173
- await ensureDir$1(join(manifestPath, ".."));
174
- await writeJson(manifestPath, manifest, { spaces: 2 });
177
+ await ensureDir(join(manifestPath, ".."));
178
+ await writeManifestAtomic(manifestPath, manifest);
175
179
  }
176
180
  async function updateManifest(updates) {
177
181
  const manifest = await readManifest();
@@ -180,11 +184,11 @@ async function updateManifest(updates) {
180
184
  ...manifest,
181
185
  ...updates
182
186
  };
183
- await writeJson(getManifestPath(), updated, { spaces: 2 });
187
+ await writeManifestAtomic(getManifestPath(), updated);
184
188
  }
185
189
  async function deleteManifest() {
186
190
  const manifestPath = getManifestPath();
187
- if (await pathExists$2(manifestPath)) await remove(manifestPath);
191
+ if (await pathExists(manifestPath)) await remove(manifestPath);
188
192
  }
189
193
  const TERMINAL_DEFAULTS = {
190
194
  WezTerm: {
@@ -203,9 +207,9 @@ async function uninstallGroups(groups) {
203
207
  try {
204
208
  if (termDefault) {
205
209
  const configPath = group.files.find((f) => f.endsWith(termDefault.file));
206
- if (configPath && await pathExists$2(configPath)) await writeFile(configPath, termDefault.content, "utf-8");
207
- for (const file of group.files) if (file !== configPath && await pathExists$2(file)) await remove(file);
208
- } else if (await pathExists$2(group.target)) await remove(group.target);
210
+ if (configPath && await pathExists(configPath)) await writeFile(configPath, termDefault.content, "utf-8");
211
+ for (const file of group.files) if (file !== configPath && await pathExists(file)) await remove(file);
212
+ } else if (await pathExists(group.target)) await remove(group.target);
209
213
  } catch (err) {
210
214
  errors.push(`${group.name}: ${err instanceof Error ? err.message : String(err)}`);
211
215
  }
@@ -216,7 +220,7 @@ async function uninstallGroups(groups) {
216
220
  async function findAllBackups() {
217
221
  const backupRoot = join(homedir(), BACKUP_DIR);
218
222
  const result = /* @__PURE__ */ new Map();
219
- if (!await pathExists$2(backupRoot)) return result;
223
+ if (!await pathExists(backupRoot)) return result;
220
224
  const groups = await readdir(backupRoot, { withFileTypes: true });
221
225
  for (const group of groups) {
222
226
  if (!group.isDirectory()) continue;
@@ -231,1151 +235,6 @@ async function findAllBackups() {
231
235
  return result;
232
236
  }
233
237
  //#endregion
234
- //#region src/platform.ts
235
- function getDependencyTools(platform) {
236
- if (platform === "macos") return [
237
- {
238
- name: "Git",
239
- binary: "git",
240
- description: "Version control + Neovim plugins",
241
- installCmd: "brew install git",
242
- required: true
243
- },
244
- {
245
- name: "fd",
246
- binary: "fd",
247
- description: "File finder used by FZF",
248
- installCmd: "brew install fd",
249
- required: false
250
- },
251
- {
252
- name: "eza",
253
- binary: "eza",
254
- description: "Modern ls for FZF tree preview",
255
- installCmd: "brew install eza",
256
- required: false
257
- },
258
- {
259
- name: "fastfetch",
260
- binary: "fastfetch",
261
- description: "System info tool",
262
- installCmd: "brew install fastfetch",
263
- required: false
264
- },
265
- {
266
- name: "tree-sitter-cli",
267
- binary: "tree-sitter",
268
- description: "LazyVim parser compiler (requires C compiler — included in Xcode CLT)",
269
- installCmd: "brew install tree-sitter-cli",
270
- required: true,
271
- forGroup: "Neovim"
272
- }
273
- ];
274
- return [
275
- {
276
- name: "Git",
277
- binary: "git",
278
- description: "Version control + Neovim plugins",
279
- installCmd: "winget install Git.Git",
280
- required: true
281
- },
282
- {
283
- name: "fd",
284
- binary: "fd",
285
- description: "File finder used by FZF",
286
- installCmd: "winget install sharkdp.fd",
287
- required: false
288
- },
289
- {
290
- name: "eza",
291
- binary: "eza",
292
- description: "Modern ls for FZF tree preview",
293
- installCmd: "winget install eza-community.eza",
294
- required: false
295
- },
296
- {
297
- name: "fastfetch",
298
- binary: "fastfetch",
299
- description: "System info tool",
300
- installCmd: "winget install Fastfetch-cli.Fastfetch",
301
- required: false
302
- },
303
- {
304
- name: "tree-sitter-cli",
305
- binary: "tree-sitter",
306
- description: "LazyVim parser compiler (requires C compiler — VS Build Tools or scoop install gcc)",
307
- installCmd: "npm i -g tree-sitter-cli",
308
- required: true,
309
- forGroup: "Neovim"
310
- }
311
- ];
312
- }
313
- function detectPlatform() {
314
- switch (process.platform) {
315
- case "darwin": return "macos";
316
- case "win32": return "windows";
317
- default: return null;
318
- }
319
- }
320
- function getHomedir() {
321
- return homedir();
322
- }
323
- function detectTool(binary) {
324
- try {
325
- execSync(process.platform === "win32" ? `where.exe ${binary}` : `which ${binary}`, { stdio: "ignore" });
326
- return true;
327
- } catch {
328
- return false;
329
- }
330
- }
331
- function getDotfileGroups(platform) {
332
- const home = getHomedir();
333
- const config = join(home, ".config");
334
- if (platform === "macos") return [
335
- {
336
- name: "Fish Shell",
337
- source: ".config/fish",
338
- target: join(config, "fish"),
339
- description: "Config, 8 modules, Tide palettes",
340
- toolBinary: "fish",
341
- toolDescription: "Modern shell with autosuggestions",
342
- installCmd: "brew install fish",
343
- required: true,
344
- themeSupport: true
345
- },
346
- {
347
- name: "Ghostty",
348
- source: ".config/ghostty",
349
- target: join(config, "ghostty"),
350
- description: "Terminal emulator",
351
- toolBinary: "ghostty",
352
- toolDescription: "GPU-accelerated terminal emulator",
353
- installCmd: "brew install --cask ghostty",
354
- required: true,
355
- themeSupport: true
356
- },
357
- {
358
- name: "WezTerm",
359
- source: ".config/wezterm",
360
- target: join(config, "wezterm"),
361
- description: "Cross-platform terminal",
362
- toolBinary: "wezterm",
363
- toolDescription: "Cross-platform terminal emulator",
364
- installCmd: "brew install --cask wezterm",
365
- required: true,
366
- themeSupport: true
367
- },
368
- {
369
- name: "tmux",
370
- source: ".config/tmux",
371
- target: join(config, "tmux"),
372
- description: "7 config files + keybinds",
373
- toolBinary: "tmux",
374
- toolDescription: "Terminal multiplexer",
375
- installCmd: "brew install tmux",
376
- required: false,
377
- themeSupport: true
378
- },
379
- {
380
- name: "Neovim",
381
- source: ".config/nvim",
382
- target: join(config, "nvim"),
383
- description: "LazyVim + solarized-osaka",
384
- toolBinary: "nvim",
385
- toolDescription: "Hyperextensible text editor",
386
- installCmd: "brew install neovim",
387
- required: true,
388
- themeSupport: true,
389
- extraBackupPaths: [{
390
- label: "data",
391
- path: join(home, ".local", "share", "nvim")
392
- }]
393
- },
394
- {
395
- name: "bat",
396
- source: ".config/bat",
397
- target: join(config, "bat"),
398
- description: "Config + custom themes",
399
- toolBinary: "bat",
400
- toolDescription: "Cat clone with syntax highlighting",
401
- installCmd: "brew install bat",
402
- required: false,
403
- themeSupport: true
404
- },
405
- {
406
- name: "btop",
407
- source: ".config/btop",
408
- target: join(config, "btop"),
409
- description: "System monitor + themes",
410
- toolBinary: "btop",
411
- toolDescription: "System resource monitor",
412
- installCmd: "brew install btop",
413
- required: false,
414
- themeSupport: true
415
- },
416
- {
417
- name: "ripgrep",
418
- source: ".config/ripgrep",
419
- target: join(config, "ripgrep"),
420
- description: "Search config",
421
- toolBinary: "rg",
422
- toolDescription: "Fast search tool",
423
- installCmd: "brew install ripgrep",
424
- required: false,
425
- themeSupport: false
426
- },
427
- {
428
- name: "Claude Code",
429
- source: [".claude.json", ".claude"],
430
- target: home,
431
- description: "MCP servers + settings",
432
- required: false,
433
- themeSupport: false
434
- }
435
- ];
436
- return [
437
- {
438
- name: "PowerShell",
439
- source: "powershell",
440
- target: join(home, "Documents", "PowerShell"),
441
- description: "Profile, 5 modules, 3 functions",
442
- toolBinary: "pwsh",
443
- toolDescription: "Modern cross-platform shell",
444
- installCmd: "winget install Microsoft.PowerShell",
445
- required: true,
446
- themeSupport: true
447
- },
448
- {
449
- name: "oh-my-posh",
450
- source: ".config/omp-themes",
451
- target: join(config, "omp-themes"),
452
- description: "3 TOML prompt themes",
453
- toolBinary: "oh-my-posh",
454
- toolDescription: "Prompt theme engine",
455
- installCmd: "winget install JanDeDobbeleer.OhMyPosh",
456
- required: false,
457
- themeSupport: true
458
- },
459
- {
460
- name: "WezTerm",
461
- source: ".config/wezterm",
462
- target: join(config, "wezterm"),
463
- description: "Terminal config",
464
- toolBinary: "wezterm",
465
- toolDescription: "Cross-platform terminal emulator",
466
- installCmd: "winget install wez.wezterm",
467
- required: true,
468
- themeSupport: true
469
- },
470
- {
471
- name: "Neovim",
472
- source: ".config/nvim",
473
- target: join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "nvim"),
474
- description: "LazyVim + solarized-osaka",
475
- toolBinary: "nvim",
476
- toolDescription: "Hyperextensible text editor",
477
- installCmd: "winget install Neovim.Neovim",
478
- required: true,
479
- themeSupport: true,
480
- extraBackupPaths: [{
481
- label: "data",
482
- path: join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "nvim-data")
483
- }]
484
- },
485
- {
486
- name: "bat",
487
- source: ".config/bat",
488
- target: join(process.env.APPDATA || join(home, "AppData", "Roaming"), "bat"),
489
- description: "Config + custom themes",
490
- toolBinary: "bat",
491
- toolDescription: "Cat clone with syntax highlighting",
492
- installCmd: "winget install sharkdp.bat",
493
- required: false,
494
- themeSupport: true
495
- },
496
- {
497
- name: "btop",
498
- source: ".config/btop",
499
- target: join(process.env.APPDATA || join(home, "AppData", "Roaming"), "btop"),
500
- description: "System monitor + themes",
501
- toolBinary: "btop",
502
- toolDescription: "System resource monitor",
503
- installCmd: "winget install aristocratos.btop4win",
504
- required: false,
505
- themeSupport: true
506
- },
507
- {
508
- name: "ripgrep",
509
- source: ".config/ripgrep",
510
- target: join(config, "ripgrep"),
511
- description: "Search config",
512
- toolBinary: "rg",
513
- toolDescription: "Fast search tool",
514
- installCmd: "winget install BurntSushi.ripgrep.MSVC",
515
- required: false,
516
- themeSupport: false
517
- },
518
- {
519
- name: "Claude Code",
520
- source: [".claude.json", ".claude"],
521
- target: home,
522
- description: "MCP servers + settings",
523
- required: false,
524
- themeSupport: false
525
- }
526
- ];
527
- }
528
- //#endregion
529
- //#region src/theme.ts
530
- const { ensureDir, pathExists: pathExists$1 } = fse;
531
- const ghosttyThemes = {
532
- "solarized-dark": "Solarized Dark Patched",
533
- vercel: "Vercel",
534
- vesper: "Vesper"
535
- };
536
- const batThemes = {
537
- "solarized-dark": "Solarized (dark)",
538
- vercel: "Vercel",
539
- vesper: "Vesper"
540
- };
541
- const btopThemes = {
542
- "solarized-dark": "Solarized_Dark",
543
- vercel: "Vercel",
544
- vesper: "Vesper"
545
- };
546
- const weztermThemes = {
547
- "solarized-dark": "Solarized Dark (Gogh)",
548
- vercel: "Vercel",
549
- vesper: "Vesper"
550
- };
551
- const nvimPlugins = {
552
- "solarized-dark": "craftzdog/solarized-osaka.nvim",
553
- vercel: "tiesen243/vercel.nvim",
554
- vesper: "datsfilipe/vesper.nvim"
555
- };
556
- const tidePalettes = {
557
- "solarized-dark": "heyitsiveen",
558
- vercel: "vercel",
559
- vesper: "vesper"
560
- };
561
- const themeHeaderPatterns = {
562
- "solarized-dark": "SOLARIZED",
563
- vercel: "VERCEL",
564
- vesper: "VESPER"
565
- };
566
- async function switchTheme(theme, installedGroups, platform, dryRun) {
567
- const results = [];
568
- for (const group of installedGroups) try {
569
- switch (group.name) {
570
- case "Ghostty":
571
- if (!dryRun) await switchGhosttyTheme(group.target, theme);
572
- results.push(`Ghostty → ${ghosttyThemes[theme]}`);
573
- break;
574
- case "bat":
575
- if (!dryRun) await switchBatTheme(group.target, theme);
576
- results.push(`bat → ${batThemes[theme]}`);
577
- break;
578
- case "btop":
579
- if (!dryRun) await switchSingleLine(join(group.target, "btop.conf"), /^color_theme = .*$/m, `color_theme = "${btopThemes[theme]}"`);
580
- results.push(`btop → ${btopThemes[theme]}`);
581
- break;
582
- case "WezTerm":
583
- if (!dryRun) await switchSingleLine(join(group.target, "wezterm.lua"), /^config\.color_scheme = .*$/m, `config.color_scheme = '${weztermThemes[theme]}'`);
584
- results.push(`WezTerm → ${weztermThemes[theme]}`);
585
- break;
586
- case "Neovim":
587
- if (!dryRun) await switchNeovimTheme(group.target, theme);
588
- results.push(`Neovim → ${nvimPlugins[theme]}`);
589
- break;
590
- case "Fish Shell":
591
- if (!dryRun) {
592
- await switchTideTheme(group.target, theme);
593
- await switchFzfTheme(join(group.target, "conf.d", "40-fzf.fish"), theme, /^set -gx FZF_DEFAULT_OPTS/);
594
- }
595
- results.push(`Fish/Tide palette → ${tidePalettes[theme]}`);
596
- results.push(`FZF → ${theme}`);
597
- break;
598
- case "tmux":
599
- if (!dryRun) await switchTmuxTheme(group.target, theme);
600
- results.push(`tmux statusbar → ${theme}`);
601
- break;
602
- case "PowerShell":
603
- if (!dryRun) await switchFzfTheme(join(group.target, "modules", "fzf.ps1"), theme, /^\$env:FZF_DEFAULT_OPTS/);
604
- results.push(`FZF (PowerShell) → ${theme}`);
605
- break;
606
- case "oh-my-posh":
607
- if (!dryRun) await switchOmpTheme(theme);
608
- results.push(`oh-my-posh → ${theme}`);
609
- break;
610
- }
611
- } catch (err) {
612
- results.push(`${group.name} — failed: ${err instanceof Error ? err.message : String(err)}`);
613
- }
614
- return results;
615
- }
616
- async function switchGhosttyTheme(targetDir, theme) {
617
- const filePath = join(targetDir, "config");
618
- if (!await pathExists$1(filePath)) return;
619
- let content = await readFile(filePath, "utf-8");
620
- content = content.replace(/^theme = .*$/m, `theme = "${ghosttyThemes[theme]}"`);
621
- if (theme === "solarized-dark") {
622
- if (!/^background = /m.test(content)) content = content.replace(/^(theme = .*)$/m, "$1\n\n# Window\nbackground = #031219");
623
- } else content = content.replace(/^background = #031219\n/m, "");
624
- await writeFile(filePath, content, "utf-8");
625
- }
626
- async function switchSingleLine(filePath, pattern, replacement) {
627
- if (!await pathExists$1(filePath)) return;
628
- await writeFile(filePath, (await readFile(filePath, "utf-8")).replace(pattern, replacement), "utf-8");
629
- }
630
- async function switchBatTheme(targetDir, theme) {
631
- const filePath = join(targetDir, "config");
632
- if (!await pathExists$1(filePath)) return;
633
- let content = await readFile(filePath, "utf-8");
634
- content = content.replace(/^(--theme=.*)$/m, "# $1");
635
- const targetValue = batThemes[theme];
636
- content = content.replace(new RegExp(`^# (--theme="${targetValue.replace(/[()]/g, "\\$&")}")$`, "m"), "$1");
637
- await writeFile(filePath, content, "utf-8");
638
- }
639
- async function switchNeovimTheme(targetDir, theme) {
640
- const filePath = join(targetDir, "lua", "plugins", "colorscheme.lua");
641
- if (!await pathExists$1(filePath)) return;
642
- const content = await readFile(filePath, "utf-8");
643
- const allPlugins = Object.values(nvimPlugins);
644
- const pluginPattern = new RegExp(`"(${allPlugins.map((p) => p.replace(/[/.]/g, "\\$&")).join("|")})"`);
645
- await writeFile(filePath, content.replace(pluginPattern, `"${nvimPlugins[theme]}"`), "utf-8");
646
- }
647
- async function switchTideTheme(targetDir, theme) {
648
- const filePath = join(targetDir, "conf.d", "70-tide.fish");
649
- if (!await pathExists$1(filePath)) return;
650
- await writeFile(filePath, (await readFile(filePath, "utf-8")).replace(/^(set -l tide_default_palette )\S+$/m, `$1${tidePalettes[theme]}`), "utf-8");
651
- }
652
- async function switchOmpTheme(theme) {
653
- const themeDir = join(homedir(), MANIFEST_DIR, "oh-my-posh");
654
- await ensureDir(themeDir);
655
- await writeFile(join(themeDir, "prompt-theme.txt"), theme, "utf-8");
656
- }
657
- async function switchFzfTheme(filePath, theme, commandStart) {
658
- if (!await pathExists$1(filePath)) return;
659
- await writeFile(filePath, toggleThemeBlocks(await readFile(filePath, "utf-8"), theme, commandStart), "utf-8");
660
- }
661
- async function switchTmuxTheme(targetDir, theme) {
662
- const filePath = join(targetDir, "statusbar.conf");
663
- if (!await pathExists$1(filePath)) return;
664
- await writeFile(filePath, toggleThemeBlocks(await readFile(filePath, "utf-8"), theme, /^(set|setw) -g /), "utf-8");
665
- }
666
- function toggleThemeBlocks(content, theme, commandStart) {
667
- const lines = content.split("\n");
668
- const result = [];
669
- const targetHeader = themeHeaderPatterns[theme];
670
- let currentSection = null;
671
- let inMultiline = false;
672
- let inHereString = false;
673
- for (let i = 0; i < lines.length; i++) {
674
- const line = lines[i];
675
- const headerMatch = line.match(/║\s+(\S+)/);
676
- if (headerMatch) {
677
- const headerName = headerMatch[1];
678
- for (const [, pattern] of Object.entries(themeHeaderPatterns)) if (headerName.startsWith(pattern)) {
679
- currentSection = pattern;
680
- break;
681
- }
682
- if (headerName === "STATUS" || headerName === "WINDOW" || headerName === "FZF") {}
683
- }
684
- if (currentSection === null) {
685
- result.push(line);
686
- inMultiline = line.replace(/^# /, "").endsWith("\\");
687
- continue;
688
- }
689
- const shouldBeActive = currentSection === targetHeader;
690
- const stripped = line.replace(/^# /, "");
691
- const isCommented = line !== stripped && line.startsWith("# ");
692
- const isCommandStart = commandStart.test(stripped);
693
- if (isCommandStart || inMultiline) if (shouldBeActive && isCommented) result.push(stripped);
694
- else if (!shouldBeActive && !isCommented && !line.match(/^\s*$/)) result.push(`# ${line}`);
695
- else result.push(line);
696
- else result.push(line);
697
- const contentForState = isCommented ? stripped : line;
698
- if (isCommandStart || inMultiline) if (!inHereString && contentForState.trimEnd().endsWith("\\")) inMultiline = true;
699
- else if (contentForState.trim() === "\"@") {
700
- inMultiline = false;
701
- inHereString = false;
702
- } else if (contentForState.includes("@\"")) {
703
- inMultiline = true;
704
- inHereString = true;
705
- } else if (inHereString) inMultiline = true;
706
- else inMultiline = false;
707
- }
708
- return result.join("\n");
709
- }
710
- //#endregion
711
- //#region src/prompts.ts
712
- const { copy, pathExists } = fse;
713
- function handleCancel(value) {
714
- if (isCancel(value)) {
715
- cancel("Operation cancelled.");
716
- process.exit(0);
717
- }
718
- }
719
- function showBanner() {
720
- console.log(pc.cyan(ASCII_BANNER));
721
- console.log(pc.dim(` v${VERSION}`));
722
- console.log();
723
- }
724
- function showPrerequisites(platform) {
725
- const items = [{
726
- name: "Nerd Font",
727
- description: "required for icons (Tide, oh-my-posh, Neovim)",
728
- link: "https://www.nerdfonts.com/",
729
- ok: true
730
- }];
731
- if (platform === "macos") items.push({
732
- name: "Homebrew",
733
- description: "package manager for macOS",
734
- link: "https://brew.sh/",
735
- ok: detectTool("brew")
736
- });
737
- else items.push({
738
- name: "winget",
739
- description: "package manager for Windows",
740
- link: "https://aka.ms/getwinget",
741
- ok: detectTool("winget")
742
- });
743
- log.info("Prerequisites:");
744
- for (const item of items) {
745
- const marker = item.ok ? pc.green("◆") : pc.yellow("⚠");
746
- const label = item.ok ? item.name : pc.yellow(item.name);
747
- log.message(` ${marker} ${pc.bold(label)} — ${item.description}`);
748
- log.message(` ${pc.cyan(item.link)}`);
749
- }
750
- }
751
- /** Detect tools and show status. Returns missing tools (no install). */
752
- async function showToolStatus(groups, platform) {
753
- const allTools = [];
754
- const groupNames = new Set(groups.map((g) => g.name));
755
- const subTools = /* @__PURE__ */ new Map();
756
- for (const dep of getDependencyTools(platform)) if (dep.forGroup) {
757
- if (!groupNames.has(dep.forGroup)) continue;
758
- const ok = detectTool(dep.binary);
759
- if (!subTools.has(dep.forGroup)) subTools.set(dep.forGroup, []);
760
- subTools.get(dep.forGroup).push({
761
- ...dep,
762
- ok
763
- });
764
- allTools.push(dep);
765
- } else allTools.push(dep);
766
- for (const g of groups) if (g.toolBinary && g.installCmd) allTools.push({
767
- name: g.name,
768
- binary: g.toolBinary,
769
- description: g.toolDescription,
770
- installCmd: g.installCmd,
771
- required: g.required
772
- });
773
- if (allTools.length === 0) return [];
774
- const forGroupNames = new Set([...subTools.values()].flat().map((t) => t.name));
775
- const requiredTools = [];
776
- const optionalTools = [];
777
- const missing = [];
778
- for (const t of allTools) {
779
- const ok = detectTool(t.binary);
780
- const entry = {
781
- ...t,
782
- ok
783
- };
784
- if (!forGroupNames.has(t.name)) if (t.required) requiredTools.push(entry);
785
- else optionalTools.push(entry);
786
- if (!ok) missing.push(t);
787
- }
788
- const showTool = (t) => {
789
- const marker = t.ok ? pc.green("◆") : pc.yellow("⚠");
790
- const label = t.ok ? t.name : pc.yellow(t.name);
791
- log.message(` ${marker} ${pc.bold(label)} — ${t.description}`);
792
- };
793
- const showSubTool = (t) => {
794
- const marker = t.ok ? pc.green("◆") : pc.yellow("⚠");
795
- const label = t.ok ? t.name : pc.yellow(t.name);
796
- log.message(` ${marker} ${label} — ${pc.dim(t.description)}`);
797
- };
798
- log.info("Tools:");
799
- log.message(` ${pc.bold("Required")}`);
800
- for (const t of requiredTools) {
801
- showTool(t);
802
- const subs = subTools.get(t.name);
803
- if (subs) for (const st of subs) showSubTool(st);
804
- }
805
- log.message("");
806
- log.message(` ${pc.bold("Optional")}`);
807
- for (const t of optionalTools) {
808
- showTool(t);
809
- const subs = subTools.get(t.name);
810
- if (subs) for (const st of subs) showSubTool(st);
811
- }
812
- if (missing.length === 0) log.success("All tools installed!");
813
- return missing;
814
- }
815
- /** Show multiselect picker for missing tools. Returns tools to install (no execution). */
816
- async function pickMissingTools(missing) {
817
- const selectedNames = await multiselect({
818
- message: "Install missing tools?",
819
- options: missing.map((t) => ({
820
- value: t.name,
821
- label: t.name,
822
- hint: t.installCmd
823
- })),
824
- initialValues: missing.map((t) => t.name),
825
- required: false
826
- });
827
- handleCancel(selectedNames);
828
- const toInstall = missing.filter((t) => selectedNames.includes(t.name));
829
- const brewFirst = toInstall.filter((t) => t.name === "Homebrew");
830
- const rest = toInstall.filter((t) => t.name !== "Homebrew");
831
- return [...brewFirst, ...rest];
832
- }
833
- const groupCategories = {
834
- "Shell & Terminal": [
835
- "Fish Shell",
836
- "PowerShell",
837
- "Ghostty",
838
- "WezTerm",
839
- "tmux"
840
- ],
841
- Editor: ["Neovim"],
842
- "CLI Tools": [
843
- "bat",
844
- "btop",
845
- "ripgrep",
846
- "oh-my-posh"
847
- ],
848
- Other: ["Claude Code"]
849
- };
850
- function showOverview(groups) {
851
- log.info("Available configurations:");
852
- for (const [category, names] of Object.entries(groupCategories)) {
853
- const categoryGroups = groups.filter((g) => names.includes(g.name));
854
- if (categoryGroups.length === 0) continue;
855
- log.message(` ${pc.bold(category)}`);
856
- for (const g of categoryGroups) {
857
- log.message(` ${pc.green("◆")} ${pc.bold(g.name)} — ${pc.dim(g.description)}`);
858
- log.message(` ${pc.dim("→")} ${pc.dim(g.target)}`);
859
- }
860
- log.message("");
861
- }
862
- }
863
- function showReloadCommands(groups) {
864
- const reloads = [];
865
- for (const g of groups) switch (g.name) {
866
- case "Fish Shell":
867
- reloads.push({
868
- name: "Fish Shell",
869
- cmd: "exec fish"
870
- });
871
- break;
872
- case "tmux":
873
- reloads.push({
874
- name: "tmux",
875
- cmd: "tmux source ~/.config/tmux/tmux.conf"
876
- });
877
- break;
878
- case "Neovim":
879
- reloads.push({
880
- name: "Neovim",
881
- cmd: "restart nvim, then run :Lazy sync"
882
- });
883
- break;
884
- }
885
- if (reloads.length > 0) {
886
- log.info("Reload your configs:");
887
- for (const r of reloads) log.message(` ${r.name.padEnd(12)} → ${pc.cyan(r.cmd)}`);
888
- }
889
- const restartMsg = "Restart your terminal for all changes to take effect.";
890
- const w = 57;
891
- log.message([
892
- "",
893
- ` ${pc.dim("╭" + "─".repeat(w) + "╮")}`,
894
- ` ${pc.dim("│")} ${pc.bold(restartMsg)} ${pc.dim("│")}`,
895
- ` ${pc.dim("╰" + "─".repeat(w) + "╯")}`
896
- ].join("\n"));
897
- }
898
- async function resolvePlatform(flagPlatform) {
899
- if (flagPlatform === "macos" || flagPlatform === "windows") return flagPlatform;
900
- const detected = detectPlatform();
901
- if (detected) {
902
- log.info(`Detected platform: ${pc.bold(detected === "macos" ? "macOS" : "Windows 11")}`);
903
- return detected;
904
- }
905
- if (process.platform === "linux") {
906
- log.info("Linux detected — macOS configs will be used (most tools are shared).");
907
- return "macos";
908
- }
909
- const chosen = await select({
910
- message: "Which platform?",
911
- options: [{
912
- value: "macos",
913
- label: "macOS"
914
- }, {
915
- value: "windows",
916
- label: "Windows 11"
917
- }]
918
- });
919
- handleCancel(chosen);
920
- return chosen;
921
- }
922
- async function showUpdateNotification(updatePromise) {
923
- if (!updatePromise) return;
924
- const latest = await updatePromise;
925
- if (!latest) return;
926
- const msg = `Update available: ${VERSION} → ${latest}`;
927
- const cmd = "Run: bunx @heyitsiveen/dotfiles@latest";
928
- const w = Math.max(msg.length, 38) + 4;
929
- log.message([
930
- ` ${pc.dim("╭" + "─".repeat(w) + "╮")}`,
931
- ` ${pc.dim("│")} ${pc.yellow(msg.padEnd(w - 2))}${pc.dim("│")}`,
932
- ` ${pc.dim("│")} ${pc.cyan(cmd.padEnd(w - 2))}${pc.dim("│")}`,
933
- ` ${pc.dim("╰" + "─".repeat(w) + "╯")}`
934
- ].join("\n"));
935
- }
936
- async function firstRunFlow(flagPlatform, dryRun = false, updatePromise) {
937
- showBanner();
938
- intro(pc.bold("heyitsiveen"));
939
- await showUpdateNotification(updatePromise);
940
- const platform = await resolvePlatform(flagPlatform);
941
- showPrerequisites(platform);
942
- const allGroups = getDotfileGroups(platform);
943
- const missingTools = await showToolStatus(allGroups, platform);
944
- const toolsToInstall = missingTools.length > 0 ? await pickMissingTools(missingTools) : [];
945
- showOverview(allGroups);
946
- const depTools = getDependencyTools(platform);
947
- const selectedNames = await multiselect({
948
- message: "Which dotfiles would you like to install?",
949
- options: allGroups.map((g) => {
950
- const toolMissing = g.toolBinary && !detectTool(g.toolBinary);
951
- const missingDeps = depTools.filter((d) => d.forGroup === g.name && !detectTool(d.binary)).map((d) => d.name);
952
- const warnings = [];
953
- if (toolMissing) warnings.push("not installed");
954
- if (missingDeps.length > 0) warnings.push(`${missingDeps.join(", ")} missing`);
955
- const hint = warnings.length > 0 ? `⚠ ${warnings.join(", ")}` : g.description;
956
- return {
957
- value: g.name,
958
- label: g.name,
959
- hint
960
- };
961
- }),
962
- initialValues: allGroups.map((g) => g.name),
963
- required: true
964
- });
965
- handleCancel(selectedNames);
966
- const selectedGroups = allGroups.filter((g) => selectedNames.includes(g.name));
967
- const theme = await select({
968
- message: "Which color theme would you like?",
969
- options: [
970
- {
971
- value: "solarized-dark",
972
- label: "Solarized Dark",
973
- hint: "default"
974
- },
975
- {
976
- value: "vercel",
977
- label: "Vercel"
978
- },
979
- {
980
- value: "vesper",
981
- label: "Vesper"
982
- }
983
- ]
984
- });
985
- handleCancel(theme);
986
- let shouldBackup = false;
987
- for (const g of selectedGroups) {
988
- const sources = Array.isArray(g.source) ? g.source : [g.source];
989
- const isMultiSource = Array.isArray(g.source);
990
- for (const source of sources) if (await pathExists(isMultiSource ? join(g.target, source.split("/").pop()) : g.target)) {
991
- shouldBackup = true;
992
- break;
993
- }
994
- if (shouldBackup) break;
995
- }
996
- if (shouldBackup) {
997
- const doBackup = await confirm({ message: "Existing dotfiles found. Create backup?" });
998
- handleCancel(doBackup);
999
- shouldBackup = doBackup;
1000
- }
1001
- if (dryRun) log.info(pc.yellow("Dry run — showing planned operations:"));
1002
- if (toolsToInstall.length > 0) {
1003
- log.info("Installing tools...");
1004
- let toolsInstalled = 0;
1005
- for (const t of toolsToInstall) {
1006
- log.message(` ${pc.dim("○")} ${t.name}...`);
1007
- try {
1008
- execSync(t.installCmd, {
1009
- stdio: "pipe",
1010
- encoding: "utf-8",
1011
- timeout: 3e5
1012
- });
1013
- toolsInstalled++;
1014
- log.message(` ${pc.green("◆")} ${t.name} installed`);
1015
- } catch (err) {
1016
- log.message(` ${pc.yellow("⚠")} ${t.name} failed`);
1017
- log.message(` ${pc.dim(t.installCmd)} — ${pc.dim(err instanceof Error ? err.message : String(err))}`);
1018
- }
1019
- }
1020
- log.message(` ${pc.green("◆")} ${toolsInstalled}/${toolsToInstall.length} tools installed`);
1021
- }
1022
- log.info("Installing dotfiles...");
1023
- const result = await install({
1024
- platform,
1025
- selectedGroups,
1026
- theme,
1027
- backup: shouldBackup,
1028
- dryRun
1029
- });
1030
- for (const ig of result.installedGroups) log.message(` ${pc.green("◆")} ${ig.name} → ${pc.dim(ig.target)}`);
1031
- log.message(` ${pc.green("◆")} ${result.installedGroups.length}/${selectedGroups.length} configs installed`);
1032
- const themeGroups = selectedGroups.filter((g) => g.themeSupport);
1033
- if (themeGroups.length > 0) {
1034
- log.info("Applying theme...");
1035
- const themeResults = await switchTheme(theme, result.installedGroups.filter((ig) => themeGroups.some((sg) => sg.name === ig.name)), platform, dryRun);
1036
- for (const r of themeResults) log.message(` ${pc.green("◆")} ${r}`);
1037
- log.message(` ${pc.green("◆")} Theme: ${theme} activated`);
1038
- }
1039
- if (result.backedUp.length > 0) {
1040
- const unique = [...new Set(result.backedUp)];
1041
- log.info("Backed up existing configs:");
1042
- for (const name of unique) log.message(` ${pc.green("◆")} ${name}`);
1043
- log.message(` ${pc.green("◆")} ${unique.length} configs → ${pc.dim("~/.config/heyitsiveen/dotfiles/backup/")}`);
1044
- }
1045
- if (!dryRun) await createManifest(result, {
1046
- platform,
1047
- selectedGroups,
1048
- theme,
1049
- backup: shouldBackup,
1050
- dryRun
1051
- });
1052
- if (result.errors.length > 0) for (const err of result.errors) log.message(` ${pc.yellow("⚠")} ${err.file}: ${pc.dim(err.error)}`);
1053
- showReloadCommands(selectedGroups);
1054
- outro("Done! Your dotfiles are installed.");
1055
- }
1056
- async function reRunFlow(manifest, flagPlatform, dryRun = false, updatePromise) {
1057
- showBanner();
1058
- intro(pc.bold("heyitsiveen"));
1059
- await showUpdateNotification(updatePromise);
1060
- showPrerequisites(manifest.platform);
1061
- log.info(`Existing installation detected (v${manifest.version}, installed ${manifest.installedAt.split("T")[0]})`);
1062
- const mode = await select({
1063
- message: "What would you like to do?",
1064
- options: [
1065
- {
1066
- value: "fresh",
1067
- label: "Fresh install (backup + overwrite all)"
1068
- },
1069
- {
1070
- value: "update",
1071
- label: "Update (apply changes from new package version)"
1072
- },
1073
- {
1074
- value: "theme",
1075
- label: "Change theme"
1076
- },
1077
- {
1078
- value: "uninstall",
1079
- label: "Uninstall"
1080
- },
1081
- {
1082
- value: "restore",
1083
- label: "Restore from backup"
1084
- }
1085
- ]
1086
- });
1087
- handleCancel(mode);
1088
- switch (mode) {
1089
- case "fresh":
1090
- await firstRunFlow(flagPlatform, dryRun);
1091
- break;
1092
- case "update":
1093
- await updateFlow(manifest, dryRun);
1094
- break;
1095
- case "theme":
1096
- await themeFlow(manifest, dryRun);
1097
- break;
1098
- case "uninstall":
1099
- await uninstallFlow(manifest, dryRun);
1100
- break;
1101
- case "restore":
1102
- await restoreFlow(dryRun);
1103
- break;
1104
- }
1105
- }
1106
- async function themeFlow(manifest, dryRun = false, flagTheme) {
1107
- let theme;
1108
- if (flagTheme && THEMES.includes(flagTheme)) theme = flagTheme;
1109
- else {
1110
- if (!flagTheme) {
1111
- showBanner();
1112
- intro(pc.bold("heyitsiveen"));
1113
- }
1114
- const chosen = await select({
1115
- message: "Which color theme would you like?",
1116
- options: [
1117
- {
1118
- value: "solarized-dark",
1119
- label: "Solarized Dark"
1120
- },
1121
- {
1122
- value: "vercel",
1123
- label: "Vercel"
1124
- },
1125
- {
1126
- value: "vesper",
1127
- label: "Vesper"
1128
- }
1129
- ]
1130
- });
1131
- handleCancel(chosen);
1132
- theme = chosen;
1133
- }
1134
- const s = spinner();
1135
- s.start(`Switching theme → ${theme}...`);
1136
- const results = await switchTheme(theme, manifest.groups, manifest.platform, dryRun);
1137
- if (!dryRun) await updateManifest({ theme });
1138
- s.stop("Theme updated!");
1139
- for (const r of results) log.success(r);
1140
- outro(`Theme switched to ${theme}. Restart your terminal to apply.`);
1141
- }
1142
- async function uninstallFlow(manifest, dryRun = false) {
1143
- const mode = await select({
1144
- message: "What would you like to uninstall?",
1145
- options: [
1146
- {
1147
- value: "configs",
1148
- label: "Configs only",
1149
- hint: "remove installed dotfiles"
1150
- },
1151
- {
1152
- value: "tools",
1153
- label: "Tools only",
1154
- hint: "uninstall via brew/winget"
1155
- },
1156
- {
1157
- value: "both",
1158
- label: "Both",
1159
- hint: "remove configs and uninstall tools"
1160
- }
1161
- ]
1162
- });
1163
- handleCancel(mode);
1164
- const uninstallConfigs = mode === "configs" || mode === "both";
1165
- const uninstallTools = mode === "tools" || mode === "both";
1166
- let selectedConfigGroups = manifest.groups;
1167
- let configNames = manifest.groups.map((g) => g.name);
1168
- if (uninstallConfigs) {
1169
- const selected = await multiselect({
1170
- message: "Which configs to remove?",
1171
- options: manifest.groups.map((g) => ({
1172
- value: g.name,
1173
- label: g.name,
1174
- hint: g.target
1175
- })),
1176
- initialValues: manifest.groups.map((g) => g.name),
1177
- required: true
1178
- });
1179
- handleCancel(selected);
1180
- configNames = selected;
1181
- selectedConfigGroups = manifest.groups.filter((g) => configNames.includes(g.name));
1182
- }
1183
- let selectedTools = [];
1184
- if (uninstallTools) {
1185
- const allGroups = getDotfileGroups(manifest.platform);
1186
- const allDeps = getDependencyTools(manifest.platform);
1187
- const toolOptions = [];
1188
- for (const g of allGroups) if (g.installCmd && g.toolBinary && detectTool(g.toolBinary)) {
1189
- const cmd = g.installCmd.replace(/\binstall\b/, "uninstall");
1190
- toolOptions.push({
1191
- value: {
1192
- name: g.name,
1193
- uninstallCmd: cmd
1194
- },
1195
- label: g.name,
1196
- hint: cmd
1197
- });
1198
- }
1199
- for (const d of allDeps) if (d.installCmd && detectTool(d.binary)) {
1200
- const cmd = d.installCmd.replace(/\binstall\b/, "uninstall");
1201
- toolOptions.push({
1202
- value: {
1203
- name: d.name,
1204
- uninstallCmd: cmd
1205
- },
1206
- label: d.name,
1207
- hint: cmd
1208
- });
1209
- }
1210
- if (toolOptions.length === 0) log.info("No installed tools found to uninstall.");
1211
- else {
1212
- const selected = await multiselect({
1213
- message: "Which tools to uninstall?",
1214
- options: toolOptions,
1215
- required: false
1216
- });
1217
- handleCancel(selected);
1218
- selectedTools = selected;
1219
- }
1220
- }
1221
- if (dryRun) {
1222
- log.info(pc.yellow("Dry run — would remove:"));
1223
- if (uninstallConfigs) for (const g of selectedConfigGroups) log.info(` ${g.name}: ${g.files.length} config files`);
1224
- for (const t of selectedTools) log.info(` ${t.name}: ${t.uninstallCmd}`);
1225
- return;
1226
- }
1227
- if (uninstallConfigs) {
1228
- const s = spinner();
1229
- s.start("Removing configs...");
1230
- const errors = await uninstallGroups(selectedConfigGroups);
1231
- s.stop("Configs removed!");
1232
- for (const g of selectedConfigGroups) log.success(`Removed ${g.name} config`);
1233
- for (const err of errors) log.warn(err);
1234
- }
1235
- if (selectedTools.length > 0) for (const t of selectedTools) {
1236
- log.message(` ${pc.dim("○")} Uninstalling ${t.name}...`);
1237
- try {
1238
- execSync(t.uninstallCmd, {
1239
- stdio: "pipe",
1240
- encoding: "utf-8",
1241
- timeout: 3e5
1242
- });
1243
- log.message(` ${pc.green("◆")} ${t.name} uninstalled`);
1244
- } catch (err) {
1245
- log.message(` ${pc.yellow("⚠")} ${t.name} failed`);
1246
- log.message(` ${pc.dim(t.uninstallCmd)} — ${pc.dim(err instanceof Error ? err.message : String(err))}`);
1247
- }
1248
- }
1249
- if (uninstallConfigs) {
1250
- if ((await findAllBackups()).size > 0) {
1251
- const doRestore = await confirm({ message: "Restore from backup?" });
1252
- handleCancel(doRestore);
1253
- if (doRestore) await restoreFlow(dryRun);
1254
- }
1255
- if (configNames.length === manifest.groups.length) await deleteManifest();
1256
- else await updateManifest({ groups: manifest.groups.filter((g) => !configNames.includes(g.name)) });
1257
- }
1258
- outro("Uninstall complete.");
1259
- }
1260
- async function updateFlow(manifest, dryRun = false) {
1261
- log.info(`Installed: v${manifest.version} → Current: v${VERSION}`);
1262
- const allGroups = getDotfileGroups(manifest.platform);
1263
- const installedNames = manifest.groups.map((g) => g.name);
1264
- const updatableGroups = allGroups.filter((g) => installedNames.includes(g.name));
1265
- if (updatableGroups.length === 0) {
1266
- log.info("No groups to update.");
1267
- return;
1268
- }
1269
- const selectedNames = await multiselect({
1270
- message: "Which groups would you like to update?",
1271
- options: updatableGroups.map((g) => ({
1272
- value: g.name,
1273
- label: g.name
1274
- })),
1275
- initialValues: updatableGroups.map((g) => g.name),
1276
- required: true
1277
- });
1278
- handleCancel(selectedNames);
1279
- const selected = updatableGroups.filter((g) => selectedNames.includes(g.name));
1280
- if (dryRun) {
1281
- log.info(pc.yellow("Dry run — would update:"));
1282
- for (const g of selected) log.info(` ${g.name} → ${g.target}`);
1283
- return;
1284
- }
1285
- const s = spinner();
1286
- s.start("Updating...");
1287
- const result = await install({
1288
- platform: manifest.platform,
1289
- selectedGroups: selected,
1290
- theme: manifest.theme,
1291
- backup: true,
1292
- dryRun: false
1293
- });
1294
- const themeGroups = selected.filter((g) => g.themeSupport);
1295
- if (themeGroups.length > 0) {
1296
- const themeInstalledGroups = result.installedGroups.filter((ig) => themeGroups.some((sg) => sg.name === ig.name));
1297
- await switchTheme(manifest.theme, themeInstalledGroups, manifest.platform, false);
1298
- }
1299
- await updateManifest({
1300
- version: VERSION,
1301
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1302
- groups: result.installedGroups
1303
- });
1304
- s.stop("Update complete!");
1305
- for (const g of result.installedGroups) log.success(`Updated ${g.name} → ${g.target}`);
1306
- if (result.errors.length > 0) for (const err of result.errors) log.warn(`${err.file}: ${err.error}`);
1307
- outro("Update complete.");
1308
- }
1309
- async function restoreFlow(dryRun = false) {
1310
- const allBackups = await findAllBackups();
1311
- if (allBackups.size === 0) {
1312
- log.info("No backups found. Nothing to restore.");
1313
- process.exit(0);
1314
- }
1315
- const manifest = await readManifest();
1316
- if (!manifest) log.warn("No installation record found. Restore may overwrite untracked files.");
1317
- const selectedGroups = await multiselect({
1318
- message: "Which configs to restore?",
1319
- options: [...allBackups.entries()].map(([group, entries]) => ({
1320
- value: group,
1321
- label: `${group.padEnd(16)} (${entries.length} backup${entries.length > 1 ? "s" : ""})`,
1322
- hint: `★ latest: ${entries[0].name}`
1323
- })),
1324
- initialValues: [...allBackups.keys()],
1325
- required: true
1326
- });
1327
- handleCancel(selectedGroups);
1328
- const toRestore = [];
1329
- for (const group of selectedGroups) {
1330
- const entries = allBackups.get(group);
1331
- const chosen = await select({
1332
- message: `Which ${group} backup to restore?`,
1333
- options: entries.map((e, i) => ({
1334
- value: e,
1335
- label: e.name,
1336
- hint: i === 0 ? "★ latest" : void 0
1337
- }))
1338
- });
1339
- handleCancel(chosen);
1340
- toRestore.push(chosen);
1341
- }
1342
- const doConfirm = await confirm({ message: `Restore ${toRestore.length} config${toRestore.length > 1 ? "s" : ""}?` });
1343
- handleCancel(doConfirm);
1344
- if (!doConfirm) return;
1345
- if (dryRun) {
1346
- log.info(pc.yellow("Dry run — would restore:"));
1347
- for (const b of toRestore) log.info(` ${b.group} → ${b.name}`);
1348
- return;
1349
- }
1350
- const s = spinner();
1351
- s.start("Restoring...");
1352
- for (const backup of toRestore) {
1353
- const matchingGroup = manifest?.groups.find((g) => g.name === backup.group);
1354
- if (matchingGroup) try {
1355
- const configSubdir = join(backup.path, "config");
1356
- if (matchingGroup.extraBackupPaths && await pathExists(configSubdir)) {
1357
- await copy(configSubdir, matchingGroup.target, { overwrite: true });
1358
- log.success(`Restored ${backup.group} config → ${matchingGroup.target}`);
1359
- for (const extra of matchingGroup.extraBackupPaths) {
1360
- const extraSubdir = join(backup.path, extra.label);
1361
- if (await pathExists(extraSubdir)) {
1362
- await copy(extraSubdir, extra.path, { overwrite: true });
1363
- log.success(`Restored ${backup.group} ${extra.label} → ${extra.path}`);
1364
- }
1365
- }
1366
- } else {
1367
- await copy(backup.path, matchingGroup.target, { overwrite: true });
1368
- log.success(`Restored ${backup.group} → ${matchingGroup.target}`);
1369
- }
1370
- } catch (err) {
1371
- log.warn(`Failed to restore ${backup.group}: ${err instanceof Error ? err.message : String(err)}`);
1372
- }
1373
- else log.warn(`Skipped ${backup.group} — no matching installation record`);
1374
- }
1375
- s.stop("Restore complete!");
1376
- outro("Dotfiles restored from backup.");
1377
- }
1378
- //#endregion
1379
238
  //#region src/update-check.ts
1380
239
  async function checkForUpdate() {
1381
240
  try {
@@ -1392,6 +251,10 @@ async function checkForUpdate() {
1392
251
  }
1393
252
  //#endregion
1394
253
  //#region src/index.ts
254
+ if (Number(process.versions.node.split(".")[0]) < 22) {
255
+ process.stderr.write("\n \x1B[33m@heyitsiveen/dotfiles\x1B[0m requires \x1B[1mNode.js 22\x1B[0m or newer.\n You are running Node " + process.versions.node + ".\n\n Upgrade: \x1B[36mhttps://nodejs.org/\x1B[0m (or via nvm / fnm / volta)\n\n");
256
+ process.exit(1);
257
+ }
1395
258
  runMain(defineCommand({
1396
259
  meta: {
1397
260
  name: PACKAGE_NAME,
@@ -1429,6 +292,7 @@ runMain(defineCommand({
1429
292
  const dryRun = args["dry-run"];
1430
293
  const manifest = await readManifest();
1431
294
  if (args.restore) {
295
+ const { restoreFlow } = await import("./prompts-CzEUjeU4.mjs");
1432
296
  await restoreFlow(dryRun);
1433
297
  return;
1434
298
  }
@@ -1437,6 +301,7 @@ runMain(defineCommand({
1437
301
  log.error("No installation found. Run `npx @heyitsiveen/dotfiles` to install.");
1438
302
  process.exit(1);
1439
303
  }
304
+ const { uninstallFlow } = await import("./prompts-CzEUjeU4.mjs");
1440
305
  await uninstallFlow(manifest, dryRun);
1441
306
  return;
1442
307
  }
@@ -1459,11 +324,17 @@ runMain(defineCommand({
1459
324
  log.error("No installation found. Install dotfiles first, then switch theme.");
1460
325
  process.exit(1);
1461
326
  }
327
+ const { themeFlow } = await import("./prompts-CzEUjeU4.mjs");
1462
328
  await themeFlow(manifest, dryRun, args.theme);
1463
329
  return;
1464
330
  }
1465
- if (manifest) await reRunFlow(manifest, args.platform, dryRun, updatePromise);
1466
- else await firstRunFlow(args.platform, dryRun, updatePromise);
331
+ if (manifest) {
332
+ const { reRunFlow } = await import("./prompts-CzEUjeU4.mjs");
333
+ await reRunFlow(manifest, args.platform, dryRun, updatePromise);
334
+ } else {
335
+ const { firstRunFlow } = await import("./prompts-CzEUjeU4.mjs");
336
+ await firstRunFlow(args.platform, dryRun, updatePromise);
337
+ }
1467
338
  } catch (err) {
1468
339
  if (err.code === "ERR_USE_AFTER_CLOSE") process.exit(0);
1469
340
  log.error(pc.red(err instanceof Error ? err.message : "An unexpected error occurred."));
@@ -1472,4 +343,4 @@ runMain(defineCommand({
1472
343
  }
1473
344
  }));
1474
345
  //#endregion
1475
- export {};
346
+ export { readManifest as a, ASCII_BANNER as c, VERSION as d, install as i, MANIFEST_DIR as l, deleteManifest as n, uninstallGroups as o, findAllBackups as r, updateManifest as s, createManifest as t, THEMES as u };