Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/server-shell-setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# server-shell-setup is plain bash with no package.json, so it is outside the
# turbo test matrix. This job runs its Vitest suites on a stock Ubuntu runner:
# first the dry-run tests, then a real `--components all` install followed by
# checks from inside bash, fish and nushell that everything landed.

name: server-shell-setup

on:
pull_request:
paths:
- "packages/server-shell-setup/**"
- ".github/workflows/server-shell-setup.yml"
push:
branches: [master]
paths:
- "packages/server-shell-setup/**"
- ".github/workflows/server-shell-setup.yml"
workflow_dispatch:

jobs:
install-shell:
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
working-directory: packages/server-shell-setup
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Unit tests (dry run, no system changes)
run: npx --yes vitest@3 run test/install-shell.test.js

- name: End-to-end install and verify every shell
env:
SERVER_SHELL_SETUP_E2E: "1"
run: npx --yes vitest@3 run test/install-shell.e2e.test.js
2 changes: 2 additions & 0 deletions packages/about-system-info/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ async function getBasicInfo() {
| `isp` | Internet service provider | `👮 Verizon Business` |
| `os` | Operating system | `⚡ Ubuntu 22.04` |
| `cpu` | CPU information | `📈 Intel Core i7-8700K` |
| `bench` | CPU Geekbench 6 multi-core, rank, top percentile | `💪 7k #612 top 62%` |
| `gpu` | Graphics card | `🎮 NVIDIA GeForce RTX 3080` |
| `gpu_bench` | GPU Geekbench 6 OpenCL, rank, top percentile | `🏎️ 175k #140 top 14%` |
| `disk_used` | Disk usage percentage | `📁 75%` |
| `disk_size` | Disk size per real disk | `💽 256/512GB` |
| `ram_used` | Memory usage | `💾 8/16GB` |
Expand Down
2 changes: 1 addition & 1 deletion packages/about-system-info/src/about-system-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ Available display blocks:
Basic: user, hostname, uptime, shell, os, kernel, device
Resources: disk_used, ram_used, memory_available, swap_used, top_process
Network: ip, iplocal, city, domain, isp, network_interfaces
Hardware: cpu, gpu, temperature, battery, screen_resolution
Hardware: cpu, bench, gpu, gpu_bench, temperature, battery, screen_resolution
System: load_average, users_logged_in, mount_points, services_running
Tools: pacman, ports, containers
`);
Expand Down
28 changes: 28 additions & 0 deletions packages/about-system-info/src/info/hardware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { formatBench, topPercent } from "./hardware";

describe("topPercent", () => {
it("rounds up so the fastest entry reads top 1%", () => {
expect(topPercent(1, 1000)).toBe(1);
expect(topPercent(68, 1000)).toBe(7);
expect(topPercent(50, 1000)).toBe(5);
});

it("stays within 1..100", () => {
expect(topPercent(1000, 1000)).toBe(100);
expect(topPercent(1200, 1000)).toBe(100);
expect(topPercent(5, 0)).toBe(100);
});
});

describe("formatBench", () => {
it("rounds the score to whole thousands and appends the percentile", () => {
expect(formatBench(37967, 1, 1000)).toBe("38k #1 top 1%");
expect(formatBench(20512, 68, 1000)).toBe("21k #68 top 7%");
expect(formatBench(386779, 5, 1026)).toBe("387k #5 top 1%");
});

it("never prints a decimal in the score", () => {
expect(formatBench(19249, 91, 1000)).not.toMatch(/\d\.\d/);
});
});
50 changes: 36 additions & 14 deletions packages/about-system-info/src/info/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ import { execCommand } from "../utils/command";
import { getCachedValue, setCachedValue } from "../cache/cache";
import Fuse from "fuse.js";

/**
* Formats a Geekbench score in whole thousands with its rank and how far up
* the table that rank sits.
* @param score - Raw Geekbench score
* @param rank - Rank in the benchmark table (1 = fastest)
* @param total - Number of entries in the benchmark table
* @returns Score, rank and top percentile
* @example formatBench(20512, 68, 1500) // "21k #68 top 7%"
*/
export function formatBench(score: number, rank: number, total: number): string {
return `${Math.round(score / 1000)}k #${rank} top ${topPercent(rank, total)}%`;
}

/**
* Percentile bucket a rank falls in, rounded up so rank 1 reads "top 1%"
* @param rank - Rank in the benchmark table (1 = fastest)
* @param total - Number of entries in the benchmark table
* @returns Whole percentage between 1 and 100
*/
export function topPercent(rank: number, total: number): number {
if (!total || total < 1) return 100;
return Math.min(100, Math.max(1, Math.ceil((rank / total) * 100)));
}

/**
* Gets CPU model name and specifications
* Uses platform-specific commands (wmic/lscpu/cpuinfo)
Expand Down Expand Up @@ -222,7 +246,7 @@ export function screen_resolution(): string {
* Uses fuzzy matching to find similar CPU models
* @param context - Info context with cache
* @returns Benchmark info with score and rank, or empty string
* @example "37.9k #1", "20.5k #68"
* @example "38k #1 top 1%", "21k #68 top 7%"
*/
export function bench(context: InfoContext): string {
const cached = getCachedValue(context.cache, "bench");
Expand Down Expand Up @@ -274,9 +298,7 @@ export function bench(context: InfoContext): string {
const results = fuse.search(cpuName);
if (results.length > 0) {
const match = results[0].item;
const score = Math.round(match.score / 100) / 10;
const rank = match.rank;
const result = `${score}k #${rank}`;
const result = formatBench(match.score, match.rank, scores.length);
setCachedValue(context.cache, "bench", result);
return result;
}
Expand All @@ -293,7 +315,7 @@ export function bench(context: InfoContext): string {
* Includes score, rank, and architecture type (ARM/Intel/AMD)
* @param context - Info context with cache
* @returns Detailed benchmark info with architecture, or empty string
* @example "Geekbench 6: 37.9k (Rank #1) - ARM", "Geekbench 6: 20.5k (Rank #68) - Intel"
* @example "Geekbench 6: 38k (Rank #1, top 1%) - ARM", "Geekbench 6: 21k (Rank #68, top 7%) - Intel"
*/
export function cpu_bench_info(context: InfoContext): string {
const cached = getCachedValue(context.cache, "cpu_bench_info");
Expand Down Expand Up @@ -349,8 +371,9 @@ export function cpu_bench_info(context: InfoContext): string {
const results = fuse.search(cpuName);
if (results.length > 0) {
const match = results[0].item;
const score = Math.round(match.score / 100) / 10;
const score = Math.round(match.score / 1000);
const rank = match.rank;
const top = topPercent(rank, scores.length);

// Detect architecture type
let arch = "Unknown";
Expand All @@ -364,7 +387,7 @@ export function cpu_bench_info(context: InfoContext): string {
arch = "ARM";
}

const result = `Geekbench 6: ${score}k (Rank #${rank}) - ${arch}`;
const result = `Geekbench 6: ${score}k (Rank #${rank}, top ${top}%) - ${arch}`;
setCachedValue(context.cache, "cpu_bench_info", result);
return result;
}
Expand All @@ -381,7 +404,7 @@ export function cpu_bench_info(context: InfoContext): string {
* Uses fuzzy matching to find similar GPU models
* @param context - Info context with cache
* @returns Benchmark info with score and rank, or empty string
* @example "37.9k #1", "20.5k #68"
* @example "38k #1 top 1%", "21k #68 top 7%"
*/
export function gpu_bench(context: InfoContext): string {
const cached = getCachedValue(context.cache, "gpu_bench");
Expand Down Expand Up @@ -433,9 +456,7 @@ export function gpu_bench(context: InfoContext): string {
const results = fuse.search(gpuName);
if (results.length > 0) {
const match = results[0].item;
const score = Math.round(match.score / 100) / 10;
const rank = match.rank;
const result = `${score}k #${rank}`;
const result = formatBench(match.score, match.rank, scores.length);
setCachedValue(context.cache, "gpu_bench", result);
return result;
}
Expand All @@ -452,7 +473,7 @@ export function gpu_bench(context: InfoContext): string {
* Includes score, rank, and vendor detection
* @param context - Info context with cache
* @returns Detailed benchmark info with vendor, or empty string
* @example "Geekbench 6: 37.9k (Rank #1) - NVIDIA", "Geekbench 6: 20.5k (Rank #68) - AMD"
* @example "Geekbench 6: 38k (Rank #1, top 1%) - NVIDIA", "Geekbench 6: 21k (Rank #68, top 7%) - AMD"
*/
export function gpu_bench_info(context: InfoContext): string {
const cached = getCachedValue(context.cache, "gpu_bench_info");
Expand Down Expand Up @@ -508,8 +529,9 @@ export function gpu_bench_info(context: InfoContext): string {
const results = fuse.search(gpuName);
if (results.length > 0) {
const match = results[0].item;
const score = Math.round(match.score / 100) / 10;
const score = Math.round(match.score / 1000);
const rank = match.rank;
const top = topPercent(rank, scores.length);

// Detect vendor from GPU name
let vendor = "Unknown";
Expand All @@ -532,7 +554,7 @@ export function gpu_bench_info(context: InfoContext): string {
vendor = "Intel";
}

const result = `Geekbench 6: ${score}k (Rank #${rank}) - ${vendor}`;
const result = `Geekbench 6: ${score}k (Rank #${rank}, top ${top}%) - ${vendor}`;
setCachedValue(context.cache, "gpu_bench_info", result);
return result;
}
Expand Down
15 changes: 14 additions & 1 deletion packages/about-system-info/src/info/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,17 @@ export const backgrounds = {
// Default settings
export const DEFAULT_SETTINGS = {
display_order: [
["user", "hostname", "os", "device", "kernel", "cpu", "gpu", "bench"],
[
"user",
"hostname",
"os",
"device",
"kernel",
"cpu",
"bench",
"gpu",
"gpu_bench",
],
[
"disk_used",
"disk_size",
Expand Down Expand Up @@ -90,6 +100,7 @@ export const DEFAULT_SETTINGS = {
cpu: "orange",
gpu: "yellow",
bench: "red",
gpu_bench: "red",
device: "yellow",
kernel: "green",
shell: "orange",
Expand Down Expand Up @@ -119,6 +130,7 @@ export const DEFAULT_SETTINGS = {
cpu: "📈 ",
gpu: "🎮 ",
bench: "💪 ",
gpu_bench: "🏎️ ",
device: "💻 ",
kernel: "🔧 ",
shell: "🐚 ",
Expand Down Expand Up @@ -154,6 +166,7 @@ export const DEFAULT_SETTINGS = {
cpu: "CPU",
gpu: "GPU",
bench: "Bench",
gpu_bench: "GPU Bench",
device: "Device",
kernel: "Kernel",
shell: "Shell",
Expand Down
18 changes: 9 additions & 9 deletions packages/about-system-info/src/systeminfo-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ export interface SystemInfo {
* Empty string if CPU not found or benchmark data unavailable
*
* @example
* - "37.9k #1" (top performing CPU)
* - "20.5k #68" (mid-range CPU)
* - "38k #1 top 1%" (top performing CPU)
* - "21k #68 top 7%" (mid-range CPU)
* - "" (CPU not in benchmark database)
*/
bench: string;
Expand All @@ -142,9 +142,9 @@ export interface SystemInfo {
* Empty string if CPU not found or benchmark data unavailable
*
* @example
* - "Geekbench 6: 37.9k (Rank #1) - ARM" (Apple M4 Ultra)
* - "Geekbench 6: 20.5k (Rank #68) - Intel" (Intel Core i9)
* - "Geekbench 6: 19.2k (Rank #91) - AMD" (AMD Ryzen 9 7950X)
* - "Geekbench 6: 38k (Rank #1, top 1%) - ARM" (Apple M4 Ultra)
* - "Geekbench 6: 21k (Rank #68, top 7%) - Intel" (Intel Core i9)
* - "Geekbench 6: 19k (Rank #91, top 10%) - AMD" (AMD Ryzen 9 7950X)
* - "" (CPU not in benchmark database)
*/
cpu_bench_info: string;
Expand All @@ -156,8 +156,8 @@ export interface SystemInfo {
* Empty string if GPU not found or benchmark data unavailable
*
* @example
* - "37.9k #1" (top performing GPU)
* - "20.5k #68" (mid-range GPU)
* - "38k #1 top 1%" (top performing GPU)
* - "21k #68 top 7%" (mid-range GPU)
* - "" (GPU not in benchmark database)
*/
gpu_bench: string;
Expand All @@ -169,8 +169,8 @@ export interface SystemInfo {
* Empty string if GPU not found or benchmark data unavailable
*
* @example
* - "Geekbench 6: 37.9k (Rank #1) - NVIDIA" (NVIDIA GPU)
* - "Geekbench 6: 20.5k (Rank #68) - AMD" (AMD GPU)
* - "Geekbench 6: 38k (Rank #1, top 1%) - NVIDIA" (NVIDIA GPU)
* - "Geekbench 6: 21k (Rank #68, top 7%) - AMD" (AMD GPU)
* - "" (GPU not in benchmark database)
*/
gpu_bench_info: string;
Expand Down
25 changes: 21 additions & 4 deletions packages/server-shell-setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,29 @@ One-command setup for a modern dev environment: `fish`, `nvim`, `nushell`, `bun`

## Install

On a fresh server, you may first need to set passwords:
On a fresh server, the installer checks whether root has a password before it
installs anything. If root has none, it prompts you to set one on a terminal;
in an unattended run it sets root to the user password you pass in. The root
and user password can be the same:
```bash
sudo passwd # set root password
sudo passwd $USER # set user password
# One password for both root and your user (env var keeps it out of `ps`)
SETUP_PASSWORD='...' bash install-shell.sh --components all --yes

# Or separately
bash install-shell.sh --root-password '...' --user-password '...' --components all --yes

# Only set passwords, install nothing
SETUP_PASSWORD='...' bash install-shell.sh --yes
```

| Option | Env var | Effect |
|--------|---------|--------|
| `--root-password PW` | `SETUP_ROOT_PASSWORD` | Always sets root's password |
| `--user-password PW` | `SETUP_USER_PASSWORD` | Sets your user's password, and root's when root has none |
| `--password PW` | `SETUP_PASSWORD` | Same value for both |

An existing root password is only replaced when `--root-password`/`--password` is given.

**Interactive menu** — pick what to install:
```bash
wget -qO- tinyurl.com/shellsetup | bash
Expand Down Expand Up @@ -518,7 +535,7 @@ It finishes by syncing and dropping the kernel page cache. The Docker prune dele

| Symptom | Fix |
|---------|-----|
| Installer aborts immediately on a fresh server | Set passwords first (`sudo passwd`, `sudo passwd $USER`); many images ship with none, which makes `sudo` fail confusingly |
| Installer aborts immediately on a fresh server | Many images ship with no passwords, which makes `sudo` fail confusingly. Re-run with `SETUP_PASSWORD=... ` (or `--password`) so the installer sets them first |
| Piped install shows the menu instead of running unattended | You dropped `-s --` — it's `bash -s -- all`, not `bash all` |
| Shell is still bash after login | `chsh` may have failed silently; run `chsh -s $(which fish)` and open a new session |
| `Invalid component: <name>` | Check the name against the valid list above; the script exits rather than guessing |
Expand Down
Loading
Loading