Enhance installation and update processes with user confirmation and GPU detection; add required packages

This commit is contained in:
2026-07-05 23:25:37 +02:00
parent 93de248c5a
commit db651dff28
5 changed files with 86 additions and 32 deletions
+13 -5
View File
@@ -3,20 +3,28 @@ import { install } from "./install.js";
import { update } from "./update.js"; import { update } from "./update.js";
import { remove } from "./remove.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; const [command] = positionals;
switch (command) { try {
switch (command) {
case "install": case "install":
await install(); await install({ yes: values.yes });
break; break;
case "update": case "update":
await update(); await update({ yes: values.yes });
break; break;
case "remove": case "remove":
await remove(); await remove();
break; break;
default: default:
console.error("Usage: dotfiles <install|update|remove>"); console.error("Usage: dotfiles <install|update|remove> [-y|--yes]");
process.exit(1);
}
} catch (err) {
console.error(`\nError: ${(err as Error).message}`);
process.exit(1); process.exit(1);
} }
+16 -4
View File
@@ -4,6 +4,7 @@ import { detectGpuVendors, driverPackagesFor } from "./gpu.js";
import { import {
backupConflicts, backupConflicts,
ensureSddmEnabled, ensureSddmEnabled,
ensureSudoSession,
ensureYay, ensureYay,
ensureZsh, ensureZsh,
installPackages, installPackages,
@@ -12,18 +13,21 @@ import {
writeState, writeState,
} from "./stow.js"; } from "./stow.js";
export async function install() { export async function install({ yes = false }: { yes?: boolean } = {}) {
p.intro("Hyprland dotfiles install"); 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)); const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name));
for (const group of groups) { 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({ const pick = await p.select({
message: `Choose a ${group}`, 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."); if (p.isCancel(pick)) return p.cancel("Install cancelled.");
chosen.add(pick as string); chosen.add(pick as string);
@@ -56,6 +60,14 @@ export async function install() {
for (const name of gpuPicks) chosen.add(name); 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]); installPackages([...chosen]);
ensureSddmEnabled(); ensureSddmEnabled();
+2
View File
@@ -12,6 +12,8 @@ export const packages: Pkg[] = [
{ name: "xdg-desktop-portal-hyprland", tier: "required" }, { name: "xdg-desktop-portal-hyprland", tier: "required" },
{ name: "sddm", tier: "required" }, { name: "sddm", tier: "required" },
{ name: "pciutils", tier: "required" }, { name: "pciutils", tier: "required" },
{ name: "stow", tier: "required" },
{ name: "ttf-0xproto-nerd", tier: "required" },
{ name: "waybar", tier: "recommended", group: "bar" }, { name: "waybar", tier: "recommended", group: "bar" },
{ name: "ags", tier: "recommended", group: "bar", aur: true }, { name: "ags", tier: "recommended", group: "bar", aur: true },
{ name: "wofi", tier: "recommended", group: "launcher" }, { name: "wofi", tier: "recommended", group: "launcher" },
+27 -6
View File
@@ -3,13 +3,14 @@ import {
existsSync, existsSync,
lstatSync, lstatSync,
mkdirSync, mkdirSync,
mkdtempSync,
readdirSync, readdirSync,
readFileSync, readFileSync,
renameSync, renameSync,
rmSync, rmSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
import { homedir } from "node:os"; import { homedir, tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
const REPO_ROOT = new URL("..", import.meta.url).pathname; 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 STATE_FILE = join(STATE_DIR, "state.json");
const BACKUP_ROOT = join(homedir(), ".dotfiles-backup"); const BACKUP_ROOT = join(homedir(), ".dotfiles-backup");
function run(cmd: string, args: string[]) { export function run(cmd: string, args: string[], options: { cwd?: string } = {}) {
execFileSync(cmd, args, { stdio: "inherit" }); 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 { function commandExists(cmd: string): boolean {
@@ -34,11 +53,13 @@ function commandExists(cmd: string): boolean {
export function ensureYay() { export function ensureYay() {
if (commandExists("yay")) return; if (commandExists("yay")) return;
run("sudo", ["pacman", "-S", "--needed", "--noconfirm", "git", "base-devel"]); run("sudo", ["pacman", "-S", "--needed", "--noconfirm", "git", "base-devel"]);
const tmp = "/tmp/yay-bootstrap"; const tmp = mkdtempSync(join(tmpdir(), "yay-bootstrap-"));
rmSync(tmp, { recursive: true, force: true }); try {
run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]); run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]);
execFileSync("makepkg", ["-si", "--noconfirm"], { cwd: tmp, stdio: "inherit" }); run("makepkg", ["-si", "--noconfirm"], { cwd: tmp });
} finally {
rmSync(tmp, { recursive: true, force: true }); rmSync(tmp, { recursive: true, force: true });
}
} }
export function installPackages(names: string[]) { export function installPackages(names: string[]) {
+17 -6
View File
@@ -1,11 +1,22 @@
import { execFileSync } from "node:child_process";
import * as p from "@clack/prompts"; 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"); p.intro("Hyprland dotfiles update");
execFileSync("sudo", ["pacman", "-Syu"], { stdio: "inherit" });
execFileSync("yay", ["-Syu"], { stdio: "inherit" }); const dirs = readState().dirs;
restow(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."); p.outro("Done.");
} }