diff --git a/.claude/packages/about-system-info/CLAUDE.md b/.claude/packages/about-system-info/CLAUDE.md index 704abb4f..5016c536 100644 --- a/.claude/packages/about-system-info/CLAUDE.md +++ b/.claude/packages/about-system-info/CLAUDE.md @@ -18,11 +18,18 @@ ISP and installed tools as one compact emoji line. Designed to be dropped into `.github/workflows/about-system-desktop.yml`, with the CLI compiled inside it so nothing needs installing first. +- **`about-system web`** serves a React + shadcn/ui dashboard (`web/`, its own + Vite config) plus `GET /api/info` from `src/web-server.ts`. The web build runs + *after* the library build because the library build empties `dist/`. React, + Tailwind and the shadcn deps are devDependencies — they are bundled into + `dist/web`, so the published CLI gains no runtime deps. + ## Layout `src/about-system-cli.ts` (bin) · `src/index.ts` (library) · `src/info/` (per-block collectors) · `src/cache/` · `src/bench/` · -`src/system-info-api.ts` · `src/types/` +`src/system-info-api.ts` · `src/web-server.ts` · `src/types/` · +`web/` (dashboard: `src/App.tsx`, shadcn components in `src/components/ui`) ```bash cd packages/about-system-info diff --git a/packages/about-system-info/README.md b/packages/about-system-info/README.md index d4194660..cdde3444 100644 --- a/packages/about-system-info/README.md +++ b/packages/about-system-info/README.md @@ -121,6 +121,22 @@ about-system --json about-system --help ``` +### Web Dashboard + +`about-system web` starts a small HTTP server with a React + [shadcn/ui](https://ui.shadcn.com) dashboard: usage bars for memory and disk, every info block grouped into cards, a filter box, light/dark mode, and auto-refresh every 10 seconds. + +```bash +about-system web # http://127.0.0.1:3777 +about-system web --port 8080 # or PORT=8080 about-system web +about-system web --host 0.0.0.0 --port 8080 # reachable from other machines +``` + +It listens on localhost by default. Pass `--host 0.0.0.0` to reach it on a remote server, but note that anyone who can reach the port sees the public IP, open ports, and running processes — prefer an SSH tunnel (`ssh -L 3777:localhost:3777 server`) or put it behind auth. + +The page reads `GET /api/info`, which returns the same object as `about-system --json`. + +To work on the dashboard itself (`web/`), run `about-system web` in one terminal and `bun run dev:web` in another — the Vite dev server proxies `/api` to port 3777. + ### Installation as Shell Greeting ```bash diff --git a/packages/about-system-info/package.json b/packages/about-system-info/package.json index 8b2a63e2..4445ea3a 100644 --- a/packages/about-system-info/package.json +++ b/packages/about-system-info/package.json @@ -1,6 +1,6 @@ { "name": "about-system", - "version": "0.9.32", + "version": "0.10.0", "description": "A Node.js script to display key system information with emojis. Cross-platform support for Windows, macOS, and Linux with customizable output and caching.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -26,11 +26,14 @@ } }, "scripts": { - "build": "vite build", + "build": "vite build && vite build --config web/vite.config.ts", + "build:web": "vite build --config web/vite.config.ts", "build:tsc": "tsc", "dev": "vite build --watch", + "dev:web": "vite --config web/vite.config.ts", "ship": "npm run build && npx standard-version --release-as patch; rm CHANGELOG.md; npm publish", "start": "npm run build && node dist/about-system-cli.js", + "web": "npm run build && node dist/about-system-cli.js web", "install-greeting": "npm run build && node dist/about-system-cli.js --install", "show-settings": "npm run build && node dist/about-system-cli.js --settings-show", "reset-settings": "npm run build && node dist/about-system-cli.js --settings-reset", @@ -39,6 +42,7 @@ "app:dev": "cd native && npm run dev", "app:build": "cd native && npm run build:desktop", "test": "vitest run", + "typecheck:web": "tsc -p web/tsconfig.json", "test:ci": "vitest run --reporter=junit --outputFile=./junit.xml --coverage --coverage.reporter=lcov", "test:watch": "vitest", "coverage": "vitest run --coverage" @@ -57,6 +61,7 @@ "greeting", "emoji", "system-monitoring", + "dashboard", "desktop-app", "tauri" ], @@ -89,8 +94,21 @@ "ora": "^9.0.0" }, "devDependencies": { + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-slot": "^1.3.3", + "@tailwindcss/vite": "^4.3.3", "@types/node": "^25.0.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^5", "@vitest/coverage-v8": "^4.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.48.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "tailwind-merge": "^3.7.0", + "tailwindcss": "^4.3.3", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-dts": "^4.5.4", diff --git a/packages/about-system-info/src/about-system-cli.ts b/packages/about-system-info/src/about-system-cli.ts index 8e6e9a02..f550d87e 100644 --- a/packages/about-system-info/src/about-system-cli.ts +++ b/packages/about-system-info/src/about-system-cli.ts @@ -5,6 +5,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { getSystemInfo } from "./system-info-api"; import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types"; +import { startWebServer, DEFAULT_WEB_PORT, DEFAULT_WEB_HOST } from "./web-server"; const __filename = fileURLToPath(import.meta.url); @@ -380,6 +381,33 @@ function installShellGreeting(): void { } } +/** Reads `--flag value` or `--flag=value` from args. */ +function flagValue(args: string[], flag: string): string | undefined { + for (let i = 0; i < args.length; i++) { + if (args[i] === flag) return args[i + 1]; + if (args[i].startsWith(flag + "=")) return args[i].slice(flag.length + 1); + } + return undefined; +} + +async function runWebServer(args: string[]): Promise { + const rawPort = flagValue(args, "--port") ?? process.env.PORT; + const port = rawPort === undefined ? DEFAULT_WEB_PORT : Number(rawPort); + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new Error(`Invalid port: ${rawPort}`); + } + const host = flagValue(args, "--host") ?? DEFAULT_WEB_HOST; + + const { url } = await startWebServer({ port, host }); + console.log(`About System web UI running at ${url}`); + if (host === DEFAULT_WEB_HOST) { + console.log("Listening on localhost only. Use --host 0.0.0.0 to expose it on the network."); + } else { + console.log("Warning: system details (public IP, ports, processes) are visible to anyone who can reach this port."); + } + console.log("Press Ctrl+C to stop."); +} + function parseCLIMode(args: string[]): string[] | null { for (const arg of args) { if (!arg.startsWith("--") && arg.includes(",")) { @@ -406,6 +434,7 @@ System Info Script - TypeScript Version Usage: about-system [options] about-system # CLI mode: show specific parts only + about-system web [--port N] [--host H] # Serve the web dashboard Options: --help, -h Show this help message @@ -429,6 +458,8 @@ Examples: about-system --set emojis.cpu "🚀 " about-system --set labels.cpu "Processor" about-system --json + about-system web # http://127.0.0.1:${DEFAULT_WEB_PORT} + about-system web --port 8080 --host 0.0.0.0 Settings file: ${SETTINGS_FILE} Cache file: ${CACHE_FILE} @@ -470,6 +501,11 @@ async function main(): Promise { return; } + if (args[0] === "web" || args.includes("--web")) { + await runWebServer(args); + return; + } + if (args.includes("--json")) { const info = await getSystemInfo(); console.log(JSON.stringify(info, null, 2)); diff --git a/packages/about-system-info/src/web-server.test.ts b/packages/about-system-info/src/web-server.test.ts new file mode 100644 index 00000000..ab3483ca --- /dev/null +++ b/packages/about-system-info/src/web-server.test.ts @@ -0,0 +1,69 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import type http from "http"; + +vi.mock("./system-info-api", () => ({ + getSystemInfo: vi.fn(async () => ({ user: "tester", hostname: "box", ram_used: "8/16GB" })), +})); + +import { resolveStaticFile, startWebServer } from "./web-server"; + +let webRoot: string; +let server: http.Server; +let baseUrl: string; + +beforeAll(async () => { + webRoot = fs.mkdtempSync(path.join(os.tmpdir(), "about-system-web-")); + fs.mkdirSync(path.join(webRoot, "assets")); + fs.writeFileSync(path.join(webRoot, "index.html"), "
"); + fs.writeFileSync(path.join(webRoot, "assets", "app.js"), "console.log(1)"); + + ({ server, url: baseUrl } = await startWebServer({ port: 0, webRoot })); +}); + +afterAll(() => { + server?.close(); + fs.rmSync(webRoot, { recursive: true, force: true }); +}); + +describe("resolveStaticFile", () => { + it("serves files inside the web root", () => { + expect(resolveStaticFile(webRoot, "/assets/app.js")).toBe(path.join(webRoot, "assets", "app.js")); + }); + + it("falls back to index.html for unknown routes", () => { + expect(resolveStaticFile(webRoot, "/some/route")).toBe(path.join(webRoot, "index.html")); + }); + + it("never escapes the web root", () => { + for (const attempt of ["/../../etc/passwd", "/%2e%2e/%2e%2e/etc/passwd", "/..%2f..%2fetc/passwd"]) { + const file = resolveStaticFile(webRoot, attempt); + expect(file === null || file.startsWith(webRoot)).toBe(true); + } + }); +}); + +describe("web server", () => { + it("returns system info as JSON from /api/info", async () => { + const res = await fetch(`${baseUrl}/api/info`); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("application/json"); + expect(await res.json()).toEqual({ user: "tester", hostname: "box", ram_used: "8/16GB" }); + }); + + it("serves the dashboard with the right content types", async () => { + const index = await fetch(`${baseUrl}/`); + expect(index.headers.get("content-type")).toContain("text/html"); + expect(await index.text()).toContain('id=root'); + + const js = await fetch(`${baseUrl}/assets/app.js`); + expect(js.headers.get("content-type")).toContain("text/javascript"); + }); + + it("rejects non-GET methods", async () => { + const res = await fetch(`${baseUrl}/api/info`, { method: "POST" }); + expect(res.status).toBe(405); + }); +}); diff --git a/packages/about-system-info/src/web-server.ts b/packages/about-system-info/src/web-server.ts new file mode 100644 index 00000000..75786132 --- /dev/null +++ b/packages/about-system-info/src/web-server.ts @@ -0,0 +1,120 @@ +/** + * @fileoverview `about-system web` — serves the React dashboard and a JSON API. + * + * The dashboard is prebuilt into dist/web by `vite build --config web/vite.config.ts` + * and served as static files; the only dynamic route is GET /api/info, which + * returns the same object as `about-system --json`. + */ + +import http from "http"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { getSystemInfo } from "./system-info-api"; + +export interface WebServerOptions { + port?: number; + host?: string; + /** Directory holding the built dashboard. Defaults to dist/web next to this file. */ + webRoot?: string; +} + +export const DEFAULT_WEB_PORT = 3777; +export const DEFAULT_WEB_HOST = "127.0.0.1"; + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + ".woff2": "font/woff2", +}; + +function defaultWebRoot(): string { + return path.join(path.dirname(fileURLToPath(import.meta.url)), "web"); +} + +/** + * Resolves a request path to a file inside webRoot, or null if it escapes the + * root or does not exist. Unknown paths fall back to index.html (SPA routing). + */ +export function resolveStaticFile(webRoot: string, urlPath: string): string | null { + let decoded: string; + try { + decoded = decodeURIComponent(urlPath.split("?")[0]); + } catch { + return null; + } + const root = path.resolve(webRoot); + const candidate = path.resolve(root, "." + path.posix.normalize("/" + decoded)); + if (candidate !== root && !candidate.startsWith(root + path.sep)) return null; + + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate; + const index = path.join(root, "index.html"); + return fs.existsSync(index) ? index : null; +} + +function send(res: http.ServerResponse, status: number, type: string, body: string | Buffer): void { + res.writeHead(status, { + "Content-Type": type, + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + }); + res.end(body); +} + +export function createWebServer(options: WebServerOptions = {}): http.Server { + const webRoot = options.webRoot ?? defaultWebRoot(); + + return http.createServer(async (req, res) => { + const url = req.url ?? "/"; + + if (req.method !== "GET" && req.method !== "HEAD") { + send(res, 405, "text/plain; charset=utf-8", "Method Not Allowed"); + return; + } + + if (url === "/api/info" || url.startsWith("/api/info?")) { + try { + const info = await getSystemInfo(); + send(res, 200, MIME_TYPES[".json"], JSON.stringify(info)); + } catch (error) { + send(res, 500, MIME_TYPES[".json"], JSON.stringify({ error: (error as Error).message })); + } + return; + } + + const file = resolveStaticFile(webRoot, url); + if (!file) { + send( + res, + 404, + "text/plain; charset=utf-8", + "Dashboard not found. Build it with: npm run build:web" + ); + return; + } + const type = MIME_TYPES[path.extname(file)] ?? "application/octet-stream"; + send(res, 200, type, fs.readFileSync(file)); + }); +} + +/** Starts the dashboard server and resolves with the URL it is listening on. */ +export function startWebServer(options: WebServerOptions = {}): Promise<{ server: http.Server; url: string }> { + const port = options.port ?? DEFAULT_WEB_PORT; + const host = options.host ?? DEFAULT_WEB_HOST; + const server = createWebServer(options); + + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, host, () => { + const address = server.address(); + const actualPort = typeof address === "object" && address ? address.port : port; + const shownHost = host === "0.0.0.0" || host === "::" ? "localhost" : host; + resolve({ server, url: `http://${shownHost}:${actualPort}` }); + }); + }); +} diff --git a/packages/about-system-info/vite.config.ts b/packages/about-system-info/vite.config.ts index 93743050..c16f0a31 100644 --- a/packages/about-system-info/vite.config.ts +++ b/packages/about-system-info/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ fileName: (format, entryName) => `${entryName}.js`, }, rollupOptions: { - external: ['os', 'fs', 'path', 'child_process', 'https', 'url'], + external: ['os', 'fs', 'path', 'child_process', 'http', 'https', 'url'], output: { preserveModules: false, exports: 'named', diff --git a/packages/about-system-info/web/components.json b/packages/about-system-info/web/components.json new file mode 100644 index 00000000..37e56c34 --- /dev/null +++ b/packages/about-system-info/web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/about-system-info/web/index.html b/packages/about-system-info/web/index.html new file mode 100644 index 00000000..6f1071f9 --- /dev/null +++ b/packages/about-system-info/web/index.html @@ -0,0 +1,12 @@ + + + + + + About System + + +
+ + + diff --git a/packages/about-system-info/web/src/App.tsx b/packages/about-system-info/web/src/App.tsx new file mode 100644 index 00000000..8d393e76 --- /dev/null +++ b/packages/about-system-info/web/src/App.tsx @@ -0,0 +1,362 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Activity, + Cpu, + HardDrive, + MemoryStick, + Moon, + Pause, + Play, + RefreshCw, + Search, + Sun, + Timer, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Progress } from "@/components/ui/progress"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; + +/** The object returned by GET /api/info — every field is a display string. */ +type SystemInfo = Record; + +type Field = { key: string; emoji: string; label: string; list?: boolean }; + +// Grouped the same way as the desktop app (native/dist/index.html) so the two +// UIs read as one tool. `list` fields are space-separated and render as badges. +const GROUPS: { title: string; fields: Field[] }[] = [ + { + title: "System", + fields: [ + { key: "user", emoji: "👤", label: "User" }, + { key: "hostname", emoji: "🏠", label: "Host" }, + { key: "os", emoji: "⚡", label: "OS" }, + { key: "kernel", emoji: "🔧", label: "Kernel" }, + { key: "device", emoji: "💻", label: "Device" }, + { key: "shell", emoji: "🐚", label: "Shell" }, + { key: "uptime", emoji: "⏱️", label: "Uptime" }, + { key: "users_logged_in", emoji: "🧑‍🤝‍🧑", label: "Logged in" }, + ], + }, + { + title: "Hardware", + fields: [ + { key: "cpu", emoji: "📈", label: "CPU" }, + { key: "cpu_bench_info", emoji: "🏅", label: "CPU rank" }, + { key: "gpu", emoji: "🎮", label: "GPU" }, + { key: "gpu_bench_info", emoji: "🥇", label: "GPU rank" }, + { key: "load_average", emoji: "📊", label: "Load" }, + { key: "temperature", emoji: "🌡️", label: "Temp" }, + { key: "battery", emoji: "🔋", label: "Battery" }, + { key: "screen_resolution", emoji: "🖥️", label: "Display" }, + ], + }, + { + title: "Memory & storage", + fields: [ + { key: "ram_used", emoji: "💾", label: "RAM" }, + { key: "memory_available", emoji: "🧠", label: "Available" }, + { key: "swap_used", emoji: "🔄", label: "Swap" }, + { key: "disk_used", emoji: "📁", label: "Disk used" }, + { key: "disk_size", emoji: "🗄️", label: "Disk size" }, + { key: "top_process", emoji: "🔝", label: "Top process" }, + { key: "mount_points", emoji: "📌", label: "Mounts", list: true }, + ], + }, + { + title: "Network", + fields: [ + { key: "ip", emoji: "🌎", label: "Public IP" }, + { key: "iplocal", emoji: "🌐", label: "Local IP" }, + { key: "city", emoji: "📍", label: "Location" }, + { key: "isp", emoji: "👮", label: "ISP" }, + { key: "domain", emoji: "🔗", label: "Domain" }, + { key: "network_interfaces", emoji: "🔌", label: "Interfaces", list: true }, + { key: "ports", emoji: "🚪", label: "Open ports", list: true }, + ], + }, + { + title: "Software", + fields: [ + { key: "pacman", emoji: "🚀", label: "Tools", list: true }, + { key: "containers", emoji: "📦", label: "Containers", list: true }, + { key: "services_running", emoji: "⚙️", label: "Services", list: true }, + ], + }, +]; + +const REFRESH_MS = 10_000; + +function clean(value: string | undefined): string { + return (value ?? "").replace(/%%/g, "%").trim(); +} + +/** "8/16GB" -> 50, "35%" -> 35; null when the string has no usable ratio. */ +function percentOf(value: string): number | null { + const pct = value.match(/(\d+(?:\.\d+)?)\s*%/); + if (pct) return Math.min(100, Number(pct[1])); + const ratio = value.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)/); + if (ratio && Number(ratio[2]) > 0) return Math.min(100, (Number(ratio[1]) / Number(ratio[2])) * 100); + return null; +} + +function levelColor(pct: number): string { + if (pct >= 90) return "bg-red-500"; + if (pct >= 70) return "bg-amber-500"; + return "bg-emerald-500"; +} + +function useDarkMode(): [boolean, () => void] { + const [dark, setDark] = useState(() => { + try { + const saved = localStorage.getItem("about-system-theme"); + if (saved) return saved === "dark"; + } catch { + // Storage can be unavailable; fall through to the OS preference. + } + return window.matchMedia("(prefers-color-scheme: dark)").matches; + }); + + useEffect(() => { + document.documentElement.classList.toggle("dark", dark); + try { + localStorage.setItem("about-system-theme", dark ? "dark" : "light"); + } catch { + // Ignore: the theme just won't be remembered. + } + }, [dark]); + + return [dark, () => setDark((d) => !d)]; +} + +function StatCard({ + icon: Icon, + title, + value, + detail, + pct, +}: { + icon: typeof Cpu; + title: string; + value: string; + detail?: string; + pct?: number | null; +}) { + return ( + + + {title} + + + +
{value || "—"}
+ {pct != null && } + {detail &&

{detail}

} +
+
+ ); +} + +function LoadingGrid() { + return ( +
+ {Array.from({ length: 4 }, (_, i) => ( + + ))} + {Array.from({ length: 4 }, (_, i) => ( + + ))} +
+ ); +} + +export default function App() { + const [info, setInfo] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [live, setLive] = useState(true); + const [query, setQuery] = useState(""); + const [dark, toggleDark] = useDarkMode(); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("./api/info", { cache: "no-store" }); + if (!res.ok) throw new Error(`Server responded ${res.status}`); + setInfo(await res.json()); + setError(null); + } catch (e) { + setError((e as Error).message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + load(); + }, [load]); + + useEffect(() => { + if (!live) return; + const id = window.setInterval(load, REFRESH_MS); + return () => window.clearInterval(id); + }, [live, load]); + + const groups = useMemo(() => { + if (!info) return []; + const q = query.trim().toLowerCase(); + return GROUPS.map((group) => ({ + ...group, + fields: group.fields.filter((f) => { + const value = clean(info[f.key]); + if (!value) return false; + if (!q) return true; + return `${f.label} ${f.key} ${value}`.toLowerCase().includes(q); + }), + })).filter((g) => g.fields.length > 0); + }, [info, query]); + + const title = info ? [clean(info.user), clean(info.hostname)].filter(Boolean).join("@") : "About System"; + const ram = clean(info?.ram_used); + const disk = clean(info?.disk_used); + const load1 = clean(info?.load_average).split(/\s+/)[0] ?? ""; + + return ( +
+
+
+

{title || "About System"}

+
+ {info?.os && {clean(info.os)}} + {info?.platform && {info.platform}} + {info?.timestamp && Updated {new Date(info.timestamp).toLocaleTimeString()}} +
+
+
+
+ + setQuery(e.target.value)} + placeholder="Filter…" + className="pl-8" + aria-label="Filter fields" + /> +
+ + + +
+
+ + {error && ( + + + Couldn't read system information + {error} + + + )} + + {!info && !error ? ( + + ) : info ? ( + <> +
+ + + + +
+ +
+ {groups.map((group) => ( + + + {group.title} + + +
+ {group.fields.map((f) => { + const value = clean(info[f.key]); + return ( +
+
+ {f.emoji} + {f.label} +
+
+ {f.list ? ( +
+ {value.split(/\s+/).map((item, i) => ( + + {item} + + ))} +
+ ) : ( + value + )} +
+
+ ); + })} +
+
+
+ ))} + {groups.length === 0 && ( +

+ No fields match “{query}”. +

+ )} +
+ + ) : null} + +
+ Served by about-system web +
+
+ ); +} diff --git a/packages/about-system-info/web/src/components/ui/badge.tsx b/packages/about-system-info/web/src/components/ui/badge.tsx new file mode 100644 index 00000000..c4358f66 --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/badge.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive text-white", + outline: "text-foreground", + }, + }, + defaultVariants: { variant: "default" }, + } +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + return ; +} + +export { Badge, badgeVariants }; diff --git a/packages/about-system-info/web/src/components/ui/button.tsx b/packages/about-system-info/web/src/components/ui/button.tsx new file mode 100644 index 00000000..6c55d713 --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/button.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + icon: "size-9", + }, + }, + defaultVariants: { variant: "default", size: "default" }, + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button"; + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/packages/about-system-info/web/src/components/ui/card.tsx b/packages/about-system-info/web/src/components/ui/card.tsx new file mode 100644 index 00000000..7f9f7378 --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/card.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +export { Card, CardHeader, CardTitle, CardDescription, CardContent }; diff --git a/packages/about-system-info/web/src/components/ui/input.tsx b/packages/about-system-info/web/src/components/ui/input.tsx new file mode 100644 index 00000000..18c600a3 --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/packages/about-system-info/web/src/components/ui/progress.tsx b/packages/about-system-info/web/src/components/ui/progress.tsx new file mode 100644 index 00000000..7e7b917a --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/progress.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +import { cn } from "@/lib/utils"; + +function Progress({ + className, + value, + indicatorClassName, + ...props +}: React.ComponentProps & { indicatorClassName?: string }) { + return ( + + + + ); +} + +export { Progress }; diff --git a/packages/about-system-info/web/src/components/ui/skeleton.tsx b/packages/about-system-info/web/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..a9069a5c --- /dev/null +++ b/packages/about-system-info/web/src/components/ui/skeleton.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/packages/about-system-info/web/src/index.css b/packages/about-system-info/web/src/index.css new file mode 100644 index 00000000..acf1abfe --- /dev/null +++ b/packages/about-system-info/web/src/index.css @@ -0,0 +1,74 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground antialiased; + } +} diff --git a/packages/about-system-info/web/src/lib/utils.ts b/packages/about-system-info/web/src/lib/utils.ts new file mode 100644 index 00000000..a5ef1935 --- /dev/null +++ b/packages/about-system-info/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/about-system-info/web/src/main.tsx b/packages/about-system-info/web/src/main.tsx new file mode 100644 index 00000000..12fa35b9 --- /dev/null +++ b/packages/about-system-info/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/packages/about-system-info/web/tsconfig.json b/packages/about-system-info/web/tsconfig.json new file mode 100644 index 00000000..06c6ace9 --- /dev/null +++ b/packages/about-system-info/web/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "types": ["vite/client"], + "baseUrl": ".", + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src"] +} diff --git a/packages/about-system-info/web/vite.config.ts b/packages/about-system-info/web/vite.config.ts new file mode 100644 index 00000000..9d192e56 --- /dev/null +++ b/packages/about-system-info/web/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +// The dashboard served by `about-system web`. Built after the library build +// (which empties dist/) into dist/web, where src/web-server.ts looks for it. +export default defineConfig({ + root: __dirname, + base: "./", + plugins: [react(), tailwindcss()], + resolve: { + alias: { "@": resolve(__dirname, "src") }, + }, + build: { + outDir: resolve(__dirname, "../dist/web"), + emptyOutDir: true, + }, + server: { + // `bun run dev:web` with `about-system web` running on the default port. + proxy: { "/api": "http://127.0.0.1:3777" }, + }, +}); diff --git a/packages/server-shell-setup/README.md b/packages/server-shell-setup/README.md index ce631c26..165cf3db 100644 --- a/packages/server-shell-setup/README.md +++ b/packages/server-shell-setup/README.md @@ -26,7 +26,7 @@ > If you hold a unix shell up to your ear, can you hear the C? -One-command setup for a modern dev environment: `fish`, `nvim`, `nushell`, `bun`, `node`, `helix`, `starship`, `docker`, and more. Includes fish aliases for `service_manager`, `killport`, `search`, and others. +One-command setup for a modern dev environment: `fish`, `nvim`, `nushell`, `bun`, `node`, `helix`, `yazi`, `starship`, `docker`, and more. Includes fish aliases for `service_manager`, `killport`, `search`, and others. **Supported systems**: Arch, Ubuntu/Debian, Android (Termux), macOS, Fedora, Alpine @@ -78,7 +78,7 @@ The `-s --` is what forwards arguments through the pipe to `bash`. Dropping it s | `\| bash -s -- fish,node,docker` | Only the named components, unattended | | `\| bash -s -- sudo` | Passwordless sudo only (CLI-only, not in the menu) | -Valid CLI component names: `fish`, `nushell`, `nvim`, `helix`, `node`, `bun`, `pacstall`, `docker`, `starship`, `systeminfo`, `code`, `sudo`. An unrecognized name exits with an error and lists the valid ones. +Valid CLI component names: `fish`, `nushell`, `nvim`, `helix`, `yazi`, `node`, `bun`, `pacstall`, `docker`, `starship`, `systeminfo`, `code`, `sudo`. An unrecognized name exits with an error and lists the valid ones. Two things to know about `all`: - The interactive menu's "Install Everything" includes `code` (code-server); the CLI `all` argument does not. Pass `code` explicitly if you want it unattended. @@ -96,6 +96,7 @@ When the run finishes, the script `exec`s into fish if fish is on the PATH — s | `nushell` | Data-oriented shell that handles structured data natively | `npm i -g nushell` (`pkg` on Termux) | `nu --version` | | `nvim` | Neovim with [NvChad](https://nvchad.com) config pre-installed | distro package + git clone | `nvim --version` | | `helix` | Modal terminal editor written in Rust, no config needed | distro package | `hx --version` | +| `yazi` | [Yazi](https://yazi-rs.github.io) terminal file manager, plus a `y` wrapper for bash, fish, and nushell that `cd`s into the last directory on quit | distro package on Arch, Alpine, macOS, Termux; official prebuilt release to `/usr/local/bin` on Debian/Ubuntu and Fedora/RHEL | `yazi --version` | | `node` | Node.js via [Volta](https://volta.sh) version manager (no sudo issues); also installs pnpm, yarn, git0, vite, turbo | [get.volta.sh](https://get.volta.sh) | `node -v`, `volta -v` | | `bun` | Fast JavaScript runtime, bundler, and package manager | [bun.sh/install](https://bun.sh/install) | `bun --version` | | `docker` | Docker with rootless mode enabled | get.docker.com + rootless setuptool | `docker version` | @@ -219,6 +220,20 @@ The script clones the [NvChad starter](https://github.com/NvChad/starter) to `~/ | `:checkhealth` | Diagnose a broken install | | `:w` / `:q` / `:wq` / `:q!` | Write / quit / write+quit / force quit | +### yazi — terminal file manager + +Run `y` (not `yazi`) so that quitting leaves your shell in the directory you browsed to. + +| Key | Action | +|-----|--------| +| `h/j/k/l` | Parent / down / up / enter | +| `Space` | Toggle selection | +| `y` / `x` / `p` | Yank / cut / paste | +| `d` / `r` / `a` | Trash / rename / create (end with `/` for a directory) | +| `/` / `z` | Find in directory / jump with fzf | +| `.` | Toggle hidden files | +| `q` | Quit (and `cd` there, via `y`) | + ### helix — modal editor, batteries included Helix is selection-first: you select, then act (the reverse of vim). No config or plugins needed for LSP. @@ -444,11 +459,11 @@ Useful for auditing before you run it, or for undoing parts afterward. | Path | Written by | Contents | |------|-----------|----------| | `~/.config/fish/config.fish` | fish, starship, systeminfo, docker | Prompt init, greeting, `about-system` call, PATH additions | -| `~/.config/fish/functions/*.fish` | fish | `in`, `e`, `del`, `setup`, `search`, `killport`, `service_manager` | -| `~/.config/nushell/config.nu` | nushell, starship, systeminfo | Banner off, `EDITOR=nvim`, starship autoload, `about-system` call | +| `~/.config/fish/functions/*.fish` | fish, yazi | `in`, `e`, `del`, `setup`, `search`, `killport`, `service_manager`, `y` | +| `~/.config/nushell/config.nu` | nushell, starship, systeminfo, yazi | Banner off, `EDITOR=nvim`, starship autoload, `about-system` call | | `~/.config/nvim` | nvim | NvChad starter clone (existing config moved to `~/.config/nvim.bak`) | | `~/.config/starship.toml` | starship | Overwritten with the `ƒ` prompt character config | -| `~/.bashrc` | node, starship, systeminfo, docker | Volta PATH, starship init, `about-system` call, `/usr/bin` on PATH | +| `~/.bashrc` | node, starship, systeminfo, docker, yazi | Volta PATH, starship init, `about-system` call, `/usr/bin` on PATH, `y` function | | `~/.volta/` | node | Volta toolchain and shims | | `~/.hushlogin` | systeminfo | Suppresses the default login banner | | `/etc/motd`, `/etc/update-motd.d` | systeminfo | **Deleted** so the custom greeting is the only banner | diff --git a/packages/server-shell-setup/install-shell.sh b/packages/server-shell-setup/install-shell.sh index 735bbf32..6d2f18fd 100644 --- a/packages/server-shell-setup/install-shell.sh +++ b/packages/server-shell-setup/install-shell.sh @@ -718,6 +718,92 @@ install_helix() { success "Helix installed" } +# Yazi is packaged on Arch, Alpine, Homebrew, and Termux. Debian/Ubuntu and +# Fedora/RHEL do not ship it in their default repositories, so on those (or if +# the distro package is unavailable) install the official prebuilt release. +install_yazi_release() { + have curl || die "curl is required to download the Yazi release." + have unzip || die "unzip is required to extract the Yazi release." + + local arch libc + case "$(uname -m)" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="aarch64" ;; + *) die "No prebuilt Yazi release exists for architecture: $(uname -m)." ;; + esac + libc="gnu" + [[ "$PLATFORM" == "alpine" ]] && libc="musl" + + local target="yazi-${arch}-unknown-linux-${libc}" + local url="https://github.com/sxyazi/yazi/releases/latest/download/${target}.zip" + + if (( DRY_RUN )); then + printf '%b+%b download %q and install yazi, ya to /usr/local/bin\n' "$YELLOW" "$NC" "$url" + return 0 + fi + + local workdir + workdir=$(mktemp -d) + curl -fsSL -o "$workdir/yazi.zip" "$url" + unzip -q "$workdir/yazi.zip" -d "$workdir" + sudo install -m 0755 "$workdir/$target/yazi" "$workdir/$target/ya" /usr/local/bin/ + rm -rf -- "${workdir:?}" +} + +install_yazi() { + header "Installing Yazi file manager" + + case "$PLATFORM" in + debian|fedora) + install_packages file + have yazi && log "Already installed: yazi" || install_yazi_release + ;; + alpine) + install_packages file + if ! have yazi; then + install_packages yazi || { + warn "The yazi apk is unavailable; installing the prebuilt release instead." + install_yazi_release + } + fi + ;; + arch) install_packages yazi file ;; + macos) install_packages yazi ;; + termux) install_packages yazi file ;; + esac + + # `y` wraps yazi so quitting it changes the shell into the last directory. + append_managed_block "$HOME/.bashrc" "yazi" 'y() { + local tmp cwd + tmp="$(mktemp -t "yazi-cwd.XXXXXX")" + command yazi "$@" --cwd-file="$tmp" + IFS= read -r -d "" cwd < "$tmp" + [ -n "$cwd" ] && [ "$cwd" != "$PWD" ] && builtin cd -- "$cwd" + command rm -f -- "$tmp" +}' + + replace_managed_block "$CONFIG_DIR/fish/functions/y.fish" "function-y" 'function y --wraps=yazi --description "Open Yazi and cd to its last directory on exit" + set tmp (mktemp -t "yazi-cwd.XXXXXX") + command yazi $argv --cwd-file="$tmp" + if read -z cwd < "$tmp"; and test "$cwd" != "$PWD"; and test -d "$cwd" + builtin cd -- "$cwd" + end + command rm -f -- "$tmp" +end' + + append_managed_block "$CONFIG_DIR/nushell/config.nu" "yazi" 'def --env y [...args] { + let tmp = (mktemp -t "yazi-cwd.XXXXXX") + yazi ...$args --cwd-file $tmp + let cwd = (open $tmp) + if $cwd != "" and $cwd != $env.PWD { + cd $cwd + } + rm -fp $tmp +}' + + success "Yazi installed; run y to browse and cd on exit" +} + install_node() { header "Installing Node.js with Volta" @@ -984,7 +1070,7 @@ print_help() { Usage: ${SCRIPT_NAME} [options] Options: - --components LIST Comma-separated list: fish,nushell,nvim,helix,node,bun,pacstall,docker,starship,systeminfo,code,sudo,ssh,all + --components LIST Comma-separated list: fish,nushell,nvim,helix,yazi,node,bun,pacstall,docker,starship,systeminfo,code,sudo,ssh,all --node-version VERSION Node major/version for Volta (default: ${NODE_VERSION}) --set-fish-default-shell Ask to make Fish the login shell after installation --yes, -y Accept confirmation prompts @@ -1017,6 +1103,7 @@ Select components to install (comma-separated numbers, component names, or all): 12) code-server 13) Enable passwordless sudo (advanced/security-sensitive) 14) Enable SSH password authentication (advanced/security-sensitive) + 15) Yazi terminal file manager EOF } @@ -1034,7 +1121,7 @@ select_all_components() { # `all` deliberately excludes security-sensitive sudo/SSH settings and # excludes Docker/code-server because those are infrastructure choices, not # universally desirable workstation defaults. - COMPONENTS=(fish nushell nvim helix node bun pacstall starship systeminfo) + COMPONENTS=(fish nushell nvim helix yazi node bun pacstall starship systeminfo) } parse_component_token() { @@ -1054,6 +1141,7 @@ parse_component_token() { 12|code|code-server) add_component code ;; 13|sudo) add_component sudo ;; 14|ssh) add_component ssh ;; + 15|yazi) add_component yazi ;; '') ;; *) die "Unknown component: ${token}" ;; esac @@ -1104,7 +1192,7 @@ parse_args() { print_help exit 0 ;; - all|fish|nushell|nu|nvim|neovim|helix|node|bun|pacstall|docker|starship|systeminfo|code|code-server|sudo|ssh) + all|fish|nushell|nu|nvim|neovim|helix|yazi|node|bun|pacstall|docker|starship|systeminfo|code|code-server|sudo|ssh) # Backward-compatible positional component list support. parse_component_list "$1" shift @@ -1138,6 +1226,8 @@ verify_component() { ;; helix) have hx && success "Verified: $(hx --version | head -n1)" || warn "Helix was not found on PATH." ;; + yazi) have yazi && success "Verified: $(yazi --version | head -n1)" || warn "Yazi was not found on PATH." + ;; node) if have node; then success "Verified: $(node --version)" @@ -1177,6 +1267,7 @@ install_components() { nushell) install_nushell ;; nvim) install_nvim ;; helix) install_helix ;; + yazi) install_yazi ;; node) install_node ;; bun) install_bun ;; pacstall) install_pacstall ;; diff --git a/skills/about-system/SKILL.md b/skills/about-system/SKILL.md index 89ab612f..11ebf85a 100644 --- a/skills/about-system/SKILL.md +++ b/skills/about-system/SKILL.md @@ -24,6 +24,7 @@ The package is **ESM-only** (`"type": "module"`) and ships four entry points: `. | Everything, formatted | `about-system` | | Only some blocks | `about-system cpu,ram_used,disk_used` — positional, comma-separated, no flag | | Machine-readable output | `about-system --json` | +| Browser dashboard | `about-system web [--port 3777] [--host 127.0.0.1]` — React/shadcn UI + `GET /api/info` (same JSON as `--json`). Localhost-only unless `--host 0.0.0.0`; UI source in `web/`, built to `dist/web` by `bun run build` | | One value in a script/dashboard | `import { getSystemInfo } from "about-system"` → `(await getSystemInfo()).cpu` | | One block, cheaply, no full sweep | `import { infoFunctions } from "about-system/api"` → `infoFunctions.cpu({ cache: {} })` | | Run on every terminal launch | `about-system --install` | @@ -57,6 +58,8 @@ const uptime = infoFunctions.uptime(); | Symptom | Cause → fix | | --- | --- | +| `about-system web` answers "Dashboard not found" | `dist/web` is missing — the dashboard is built by the second half of `bun run build` (or `bun run build:web`). A plain `vite build` only builds the library. | +| Web UI unreachable from another machine | It binds `127.0.0.1` by default. Use `--host 0.0.0.0` (exposes system details to anyone who can reach the port) or an SSH tunnel. | | `infoFunctions is not exported` / undefined import | The root entry exports only `getSystemInfo`, `loadCache`, `saveCache` and types. `infoFunctions` lives in the `about-system/api` subpath — the README's root import is wrong. | | `--cache-clear` does nothing / unknown flag | That flag in the README doesn't exist. The real one is `--refresh`. | | Values are stale (IP, disk, uptime) | Cached by design, per-block TTL (IP 5 min, CPU/OS/device 24 h, top process 5 s). Run `--refresh`, or delete `systeminfo-cache.json` in the OS temp dir. | diff --git a/skills/server-shell-setup/SKILL.md b/skills/server-shell-setup/SKILL.md index 551d568b..b4369dae 100644 --- a/skills/server-shell-setup/SKILL.md +++ b/skills/server-shell-setup/SKILL.md @@ -1,6 +1,6 @@ --- name: server-shell-setup -description: Guide to server-shell-setup (packages/server-shell-setup), the one-command dev-environment bootstrap for fish, nushell, nvim, helix, node via Volta, bun, docker, starship, code-server and more — interactive vs unattended installs, selecting individual components, the fish aliases it adds, and the supported distros. Use when working with server-shell-setup or troubleshooting it — the installer aborting on a fresh server, sudo or password prompts, a shell that doesn't become the default, docker rootless issues, or components that silently skip on an unsupported distro. +description: Guide to server-shell-setup (packages/server-shell-setup), the one-command dev-environment bootstrap for fish, nushell, nvim, helix, yazi, node via Volta, bun, docker, starship, code-server and more — interactive vs unattended installs, selecting individual components, the fish aliases it adds, and the supported distros. Use when working with server-shell-setup or troubleshooting it — the installer aborting on a fresh server, sudo or password prompts, a shell that doesn't become the default, docker rootless issues, or components that silently skip on an unsupported distro. --- # Working With server-shell-setup @@ -36,6 +36,7 @@ The `-s --` is what forwards arguments through the pipe to bash — dropping it | `nushell` | Structured-data shell | | `nvim` | Neovim preconfigured with NvChad | | `helix` | Modal editor, no config needed | +| `yazi` | Yazi file manager + a `y` wrapper (bash/fish/nu) that `cd`s to the last directory on quit. Distro package on Arch/Alpine/macOS/Termux; prebuilt GitHub release into `/usr/local/bin` on Debian/Ubuntu and Fedora/RHEL (x86_64/aarch64 only) | | `node` | Node via Volta (no sudo/permission problems), plus pnpm, yarn, git0, vite, turbo | | `bun` | Bun runtime + package manager | | `docker` | Docker with rootless mode |