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