From db651dff284d594fd2e4830caccad3dce4da1a8c Mon Sep 17 00:00:00 2001 From: Haylan Date: Sun, 5 Jul 2026 23:25:37 +0200 Subject: [PATCH] Enhance installation and update processes with user confirmation and GPU detection; add required packages --- src/cli.ts | 36 ++++++++++++++++++++++-------------- src/install.ts | 20 ++++++++++++++++---- src/packages.ts | 2 ++ src/stow.ts | 37 +++++++++++++++++++++++++++++-------- src/update.ts | 23 +++++++++++++++++------ 5 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 40ff935..08fc603 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,20 +3,28 @@ import { install } from "./install.js"; import { update } from "./update.js"; import { remove } from "./remove.js"; -const { positionals } = parseArgs({ allowPositionals: true }); +const { positionals, values } = parseArgs({ + allowPositionals: true, + options: { yes: { type: "boolean", short: "y", default: false } }, +}); const [command] = positionals; -switch (command) { - case "install": - await install(); - break; - case "update": - await update(); - break; - case "remove": - await remove(); - break; - default: - console.error("Usage: dotfiles "); - process.exit(1); +try { + switch (command) { + case "install": + await install({ yes: values.yes }); + break; + case "update": + await update({ yes: values.yes }); + break; + case "remove": + await remove(); + break; + default: + console.error("Usage: dotfiles [-y|--yes]"); + process.exit(1); + } +} catch (err) { + console.error(`\nError: ${(err as Error).message}`); + process.exit(1); } diff --git a/src/install.ts b/src/install.ts index 6ff7e4f..b784a07 100644 --- a/src/install.ts +++ b/src/install.ts @@ -4,6 +4,7 @@ import { detectGpuVendors, driverPackagesFor } from "./gpu.js"; import { backupConflicts, ensureSddmEnabled, + ensureSudoSession, ensureYay, ensureZsh, installPackages, @@ -12,18 +13,21 @@ import { writeState, } from "./stow.js"; -export async function install() { +export async function install({ yes = false }: { yes?: boolean } = {}) { p.intro("Hyprland dotfiles install"); - ensureYay(); + p.log.warn( + "This installer assumes a clean Arch Linux system. Running it elsewhere, " + + "or on an already-configured machine, may back up and replace existing config.", + ); const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name)); for (const group of groups) { - const options = packages.filter((pkg) => pkg.group === group); + const groupOptions = packages.filter((pkg) => pkg.group === group); const pick = await p.select({ message: `Choose a ${group}`, - options: options.map((pkg) => ({ value: pkg.name, label: pkg.name })), + options: groupOptions.map((pkg) => ({ value: pkg.name, label: pkg.name })), }); if (p.isCancel(pick)) return p.cancel("Install cancelled."); chosen.add(pick as string); @@ -56,6 +60,14 @@ export async function install() { for (const name of gpuPicks) chosen.add(name); } + p.note([...chosen].join("\n"), "Packages to install"); + if (!yes) { + const proceed = await p.confirm({ message: "Proceed?" }); + if (p.isCancel(proceed) || !proceed) return p.cancel("Install cancelled."); + } + + ensureSudoSession(); + ensureYay(); installPackages([...chosen]); ensureSddmEnabled(); diff --git a/src/packages.ts b/src/packages.ts index 2d92e11..95e262a 100644 --- a/src/packages.ts +++ b/src/packages.ts @@ -12,6 +12,8 @@ export const packages: Pkg[] = [ { name: "xdg-desktop-portal-hyprland", tier: "required" }, { name: "sddm", tier: "required" }, { name: "pciutils", tier: "required" }, + { name: "stow", tier: "required" }, + { name: "ttf-0xproto-nerd", tier: "required" }, { name: "waybar", tier: "recommended", group: "bar" }, { name: "ags", tier: "recommended", group: "bar", aur: true }, { name: "wofi", tier: "recommended", group: "launcher" }, diff --git a/src/stow.ts b/src/stow.ts index 531c835..7087b6b 100644 --- a/src/stow.ts +++ b/src/stow.ts @@ -3,13 +3,14 @@ import { existsSync, lstatSync, mkdirSync, + mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; const REPO_ROOT = new URL("..", import.meta.url).pathname; @@ -18,8 +19,26 @@ const STATE_DIR = join(homedir(), ".config", "dotfiles"); const STATE_FILE = join(STATE_DIR, "state.json"); const BACKUP_ROOT = join(homedir(), ".dotfiles-backup"); -function run(cmd: string, args: string[]) { - execFileSync(cmd, args, { stdio: "inherit" }); +export function run(cmd: string, args: string[], options: { cwd?: string } = {}) { + try { + execFileSync(cmd, args, { stdio: "inherit", ...options }); + } catch (err) { + if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") { + throw new Error(`Command not found: ${cmd}. Is it installed and on PATH?`); + } + throw new Error(`Command failed: ${cmd} ${args.join(" ")}`); + } +} + +export function ensureSudoSession() { + execFileSync("sudo", ["-v"], { stdio: "inherit" }); + setInterval(() => { + try { + execFileSync("sudo", ["-n", "-v"]); + } catch { + // credential expired or revoked; the next real sudo call will re-prompt + } + }, 60_000).unref(); } function commandExists(cmd: string): boolean { @@ -34,11 +53,13 @@ function commandExists(cmd: string): boolean { export function ensureYay() { if (commandExists("yay")) return; run("sudo", ["pacman", "-S", "--needed", "--noconfirm", "git", "base-devel"]); - const tmp = "/tmp/yay-bootstrap"; - rmSync(tmp, { recursive: true, force: true }); - run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]); - execFileSync("makepkg", ["-si", "--noconfirm"], { cwd: tmp, stdio: "inherit" }); - rmSync(tmp, { recursive: true, force: true }); + const tmp = mkdtempSync(join(tmpdir(), "yay-bootstrap-")); + try { + run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]); + run("makepkg", ["-si", "--noconfirm"], { cwd: tmp }); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } } export function installPackages(names: string[]) { diff --git a/src/update.ts b/src/update.ts index f35c216..4c1c0e6 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,11 +1,22 @@ -import { execFileSync } from "node:child_process"; import * as p from "@clack/prompts"; -import { readState, restow } from "./stow.js"; +import { ensureSudoSession, readState, restow, run } from "./stow.js"; -export async function update() { +export async function update({ yes = false }: { yes?: boolean } = {}) { p.intro("Hyprland dotfiles update"); - execFileSync("sudo", ["pacman", "-Syu"], { stdio: "inherit" }); - execFileSync("yay", ["-Syu"], { stdio: "inherit" }); - restow(readState().dirs); + + const dirs = readState().dirs; + p.note( + dirs.length > 0 ? dirs.join("\n") : "(none)", + "Dotfile packages that will be re-linked (pacman/yay will show their own upgrade list and prompts)", + ); + if (!yes) { + const proceed = await p.confirm({ message: "Proceed?" }); + if (p.isCancel(proceed) || !proceed) return p.cancel("Update cancelled."); + } + + ensureSudoSession(); + run("sudo", ["pacman", "-Syu"]); + run("yay", ["-Syu"]); + restow(dirs); p.outro("Done."); }