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 { 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
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user