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