Enhance installation and update processes with user confirmation and GPU detection; add required packages
This commit is contained in:
+13
-5
@@ -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) {
|
||||
try {
|
||||
switch (command) {
|
||||
case "install":
|
||||
await install();
|
||||
await install({ yes: values.yes });
|
||||
break;
|
||||
case "update":
|
||||
await update();
|
||||
await update({ yes: values.yes });
|
||||
break;
|
||||
case "remove":
|
||||
await remove();
|
||||
break;
|
||||
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);
|
||||
}
|
||||
|
||||
+16
-4
@@ -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();
|
||||
|
||||
@@ -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" },
|
||||
|
||||
+27
-6
@@ -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 });
|
||||
const tmp = mkdtempSync(join(tmpdir(), "yay-bootstrap-"));
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
export function installPackages(names: string[]) {
|
||||
|
||||
+17
-6
@@ -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.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user