mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
Split five more linux_configuration single_use scripts
install_unreal_mcp.sh 395 -> 155, diagnose_arch_performance.sh 377 -> 123 (two cuts), setup_local_llm.sh 370 -> 163, repo_to_study.sh 365 -> 160, setup_dwm.sh 359 -> 147. The SCRIPT_DIR fix from the last batch held on all five. Fixed a second unguarded cd, this time before the study-material generator ran - the same latent bug class as build_website, surfaced because a moved function gets linted in isolation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
1a75ed5bf5
commit
58fdc6c9de
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "Split five more linux_configuration single_use scripts",
|
||||
"objective": "Bring install_unreal_mcp.sh (395), diagnose_arch_performance.sh (377), setup_local_llm.sh (370), repo_to_study.sh (365) and setup_dwm.sh (359) under the 250-line cap.",
|
||||
"acceptance_criteria": [
|
||||
"All five entry scripts and all six new libs are at most 250 lines.",
|
||||
"bash -n and shellcheck pass on every touched file, with no new suppression and no shellcheck disable directive.",
|
||||
"The stubbed run reports every moved function as defined, for all five scripts.",
|
||||
"Each entry script still declares SCRIPT_DIR exactly once.",
|
||||
"No script is executed. These install packages, write systemd and Ollama config, and build dwm from source.",
|
||||
"The unchecked cd exposed in the repo_to_study steps is fixed rather than suppressed."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Executing any install or diagnostic step.",
|
||||
"fix_arch_performance.sh, a different file that is over the cap but was not touched here.",
|
||||
"Changing what any script does when run, beyond the cd guard."
|
||||
],
|
||||
"verifier": "scratchpad/verify_split.sh per script (bash -n + shellcheck -x + stubbed run), plus check_file_length.sh showing none of the eleven files, plus pre-commit run --files and the real git commit for jscpd."
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
{
|
||||
"intent": "Split five more linux_configuration single_use scripts under the cap, fixing a second latent unchecked cd.",
|
||||
"scope": [
|
||||
"install_unreal_mcp.sh (395 -> 155) + lib/unreal_mcp_repo.sh",
|
||||
"diagnose_arch_performance.sh (377 -> 123) + lib/arch_perf_probes.sh, lib/arch_perf_report.sh",
|
||||
"setup_local_llm.sh (370 -> 163) + lib/local_llm_model.sh",
|
||||
"repo_to_study.sh (365 -> 160) + lib/repo_study_steps.sh",
|
||||
"setup_dwm.sh (359 -> 147) + lib/dwm_config.sh",
|
||||
"Non-goal: no script executed"
|
||||
],
|
||||
"changes": [
|
||||
"One cut each, except diagnose_arch_performance.sh: its first lib came out at 263 lines, so the GPU and journal probes, the safe-fix pass and the report summary moved on to a second lib.",
|
||||
"All five already declared SCRIPT_DIR exactly once and the splitter correctly reused it, confirming the fix made in the previous batch.",
|
||||
"Fixed SC2164 in the repo_to_study steps: a bare cd \"$analysis_dir\" immediately before invoking the study-material generator. A failed cd would have run the generator against the caller's directory. Now guarded with an error message and return 1 - the second instance of this same latent bug found by splitting."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "scratchpad/verify_split.sh per script",
|
||||
"result": "pass",
|
||||
"evidence": "All five report 'libs sourced, functions defined' after bash -n and shellcheck -x. No SCRIPT_DIR collision this time."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck on each new lib",
|
||||
"result": "fail then pass",
|
||||
"evidence": "SC2164 on the unguarded cd in repo_study_steps.sh, fixed at the source. Clean afterwards."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all | grep <these files>",
|
||||
"result": "pass",
|
||||
"evidence": "None of the eleven files listed. The only remaining hit in that grep is fix_arch_performance.sh, a different script that was already over the cap and is untouched here."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed>, then the real git commit",
|
||||
"result": "pass",
|
||||
"evidence": "Clean, jscpd included, now with fifteen new libs across the linux_configuration commits."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"None of the five was run: they install AUR packages, write Ollama modelfiles and systemd config, and compile dwm. The stubbed run proves the libs are reachable and the functions defined, not that the scripts still work end to end.",
|
||||
"Two unchecked cd calls have now turned up in five splits. Others likely remain in unsplit files; splitting surfaces them because a moved function is linted in isolation."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the five scripts return to their previous sizes and the six libs disappear.",
|
||||
"After rollback, re-run bash -n and shellcheck on the restored scripts."
|
||||
]
|
||||
}
|
||||
@ -137,248 +137,8 @@ check_python_version() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Git clone/update ----------
|
||||
setup_repo() {
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
if [[ ! -d "$REPO_DIR/.git" ]]; then
|
||||
log "Cloning unreal-mcp into $REPO_DIR"
|
||||
if require_cmd git; then
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
else
|
||||
fail "git is required but not found after install"
|
||||
fi
|
||||
else
|
||||
log "Repo exists at $REPO_DIR"
|
||||
if [[ $FORCE_UPDATE == true ]]; then
|
||||
log "Updating repo with --force-update"
|
||||
git -C "$REPO_DIR" fetch origin
|
||||
git -C "$REPO_DIR" reset --hard origin/main
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
else
|
||||
log "Pulling latest changes"
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure ownership for the real user when script ran via sudo
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Launcher ----------
|
||||
install_launcher() {
|
||||
local bin_dir="$USER_HOME/.local/bin"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local launcher="$bin_dir/unreal-mcp-server"
|
||||
mkdir -p "$bin_dir"
|
||||
cat > "$launcher" << EOF
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null
|
||||
EOF
|
||||
chmod +x "$launcher"
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
|
||||
log "Installed launcher: $launcher"
|
||||
}
|
||||
|
||||
# ---------- VS Code: Continue MCP config ----------
|
||||
configure_continue() {
|
||||
if [[ $CONFIGURE_CONTINUE != true ]]; then
|
||||
log "Skipping Continue config (--no-continue)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cont_dir="$USER_HOME/.continue"
|
||||
local cont_cfg="$cont_dir/config.json"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
mkdir -p "$cont_dir"
|
||||
|
||||
# Base JSON when no config exists
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
if [[ ! -f $cont_cfg ]]; then
|
||||
cat > "$tmp_file" << JSON
|
||||
{
|
||||
"mcpServers": {
|
||||
"unrealMCP": {
|
||||
"command": "uv",
|
||||
"args": ["--directory", "$python_dir", "run", "unreal_mcp_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
mv "$tmp_file" "$cont_cfg"
|
||||
else
|
||||
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to merge ~/.continue/config.json"
|
||||
fi
|
||||
jq --arg dir "$python_dir" '
|
||||
.mcpServers = (.mcpServers // {}) |
|
||||
.mcpServers.unrealMCP = {
|
||||
command: "uv",
|
||||
args: ["--directory", $dir, "run", "unreal_mcp_server.py"]
|
||||
}
|
||||
' "$cont_cfg" > "$tmp_file" && mv "$tmp_file" "$cont_cfg"
|
||||
fi
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
|
||||
log "Configured Continue MCP at: $cont_cfg"
|
||||
}
|
||||
|
||||
# ---------- VS Code user MCP (native) ----------
|
||||
configure_vscode_user_mcp() {
|
||||
if [[ $CONFIGURE_VSCODE_USER != true ]]; then
|
||||
log "Skipping VS Code user MCP config (--no-vscode)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
|
||||
fi
|
||||
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local json
|
||||
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
|
||||
|
||||
# Handle multiple VS Code variants if present
|
||||
local candidates=(code code-insiders codium)
|
||||
local found_any=false
|
||||
for cli in "${candidates[@]}"; do
|
||||
if ! command -v "$cli" > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
found_any=true
|
||||
log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
|
||||
if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then
|
||||
log "[$cli] user profile: unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
|
||||
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
|
||||
fi
|
||||
|
||||
# Detect profiles with 'unreal' (case-insensitive) and add there too
|
||||
local data_dir=""
|
||||
case "$cli" in
|
||||
code)
|
||||
data_dir="$USER_HOME/.config/Code"
|
||||
;;
|
||||
code-insiders)
|
||||
data_dir="$USER_HOME/.config/Code - Insiders"
|
||||
;;
|
||||
codium)
|
||||
data_dir="$USER_HOME/.config/VSCodium"
|
||||
;;
|
||||
esac
|
||||
local profiles_json="$data_dir/User/profiles/profiles.json"
|
||||
if [[ -f $profiles_json ]]; then
|
||||
# Extract profile names matching /unreal/i
|
||||
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
|
||||
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
|
||||
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
|
||||
local name
|
||||
for name in "${unreal_profiles[@]}"; do
|
||||
log "[$cli] Adding unrealMCP to profile: $name"
|
||||
if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
|
||||
log "[$cli] profile '$name': unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
|
||||
fail "[$cli] --add-mcp failed for profile '$name'."
|
||||
fi
|
||||
done
|
||||
else
|
||||
log "[$cli] No VS Code profiles with 'unreal' in name"
|
||||
fi
|
||||
else
|
||||
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $found_any == false ]]; then
|
||||
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Unreal Plugin copy (optional) ----------
|
||||
install_plugin_into_project() {
|
||||
[[ -n $PROJECT_UPROJECT ]] || return 0
|
||||
local upath="$PROJECT_UPROJECT"
|
||||
if [[ -d $upath ]]; then
|
||||
# Resolve .uproject in the provided directory
|
||||
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true)
|
||||
if [[ ${#_uprojects[@]} -eq 0 ]]; then
|
||||
fail "--project directory '$upath' contains no .uproject files"
|
||||
elif [[ ${#_uprojects[@]} -gt 1 ]]; then
|
||||
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
|
||||
printf ' - %s\n' "${_uprojects[@]}" >&2
|
||||
fail "Please pass the specific .uproject path to --project"
|
||||
else
|
||||
upath="${_uprojects[0]}"
|
||||
log "Resolved .uproject: $upath"
|
||||
fi
|
||||
elif [[ -f $upath ]]; then
|
||||
true
|
||||
else
|
||||
fail "--project path does not exist: $upath"
|
||||
fi
|
||||
if [[ ${upath##*.} != "uproject" ]]; then
|
||||
fail "--project must point to a .uproject file (got: $upath)"
|
||||
fi
|
||||
local proj_dir
|
||||
proj_dir="$(cd "$(dirname "$upath")" && pwd)"
|
||||
RESOLVED_PROJECT_DIR="$proj_dir"
|
||||
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
|
||||
local dst_plugin="$proj_dir/Plugins/UnrealMCP"
|
||||
if [[ ! -d $src_plugin ]]; then
|
||||
fail "Source plugin not found at $src_plugin (did repo layout change?)"
|
||||
fi
|
||||
mkdir -p "$proj_dir/Plugins"
|
||||
log "Copying UnrealMCP plugin to project: $dst_plugin"
|
||||
rsync -a --delete "$src_plugin/" "$dst_plugin/"
|
||||
# Set ownership back to actual user if run as root
|
||||
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
|
||||
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
|
||||
}
|
||||
|
||||
# ---------- Summary ----------
|
||||
print_summary() {
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local plugin_dest="N/A"
|
||||
if [[ -n $RESOLVED_PROJECT_DIR ]]; then
|
||||
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
|
||||
fi
|
||||
cat << EOF
|
||||
============================================
|
||||
Unreal MCP setup complete
|
||||
============================================
|
||||
|
||||
Repo: $REPO_DIR
|
||||
Python dir: $python_dir
|
||||
Launcher: $USER_HOME/.local/bin/unreal-mcp-server
|
||||
|
||||
VS Code (Continue) MCP configured: ${CONFIGURE_CONTINUE}
|
||||
- File: $USER_HOME/.continue/config.json
|
||||
- Server ID: unrealMCP
|
||||
|
||||
VS Code (User profile) MCP configured: ${CONFIGURE_VSCODE_USER}
|
||||
- Command used: code --add-mcp '{"name":"unrealMCP", "command":"uv", "args":["--directory","$python_dir","run","unreal_mcp_server.py"]}'
|
||||
|
||||
Optional usage:
|
||||
- Run server manually: unreal-mcp-server
|
||||
- In VS Code with Continue installed, the unrealMCP server will auto-start when needed.
|
||||
|
||||
Unreal plugin:
|
||||
- Source: MCPGameProject/Plugins/UnrealMCP
|
||||
- If you provided --project, the plugin was copied to: $plugin_dest
|
||||
- In the Unreal Editor: Edit > Plugins > search "UnrealMCP" and enable. Restart when prompted.
|
||||
|
||||
Notes:
|
||||
- Ensure you have Unreal Engine 5.5+ installed.
|
||||
- The Python server listens to the Unreal plugin on TCP port 55557 by default.
|
||||
- For other MCP clients (Claude Desktop, Cursor, Windsurf), copy the JSON snippet from the repo README to their config locations.
|
||||
EOF
|
||||
}
|
||||
# shellcheck source=lib/unreal_mcp_repo.sh
|
||||
source "$SCRIPT_DIR/lib/unreal_mcp_repo.sh"
|
||||
|
||||
main() {
|
||||
log "Installing prerequisites (Arch Linux)"
|
||||
|
||||
221
linux_configuration/scripts/single_use/features/lib/dwm_config.sh
Executable file
221
linux_configuration/scripts/single_use/features/lib/dwm_config.sh
Executable file
@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
# dwm config install, healing and the patch applications.
|
||||
#
|
||||
# Sourced by setup_dwm.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Install our versioned config.h onto the fresh upstream clone. dwm.c stays
|
||||
# pristine here — movestack()/togglefullscr() are defined inside config.h, so
|
||||
# only the two intentional behaviour changes below patch dwm.c.
|
||||
# ---------------------------------------------------------------------------
|
||||
install_config() {
|
||||
log "Installing config.h from the repo (${REPO_DWM_DIR}/config.h)…"
|
||||
cp -- "${REPO_DWM_DIR}/config.h" "$SRC_DIR/config.h"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3b. Auto-merge bleeding-edge config churn. When upstream adds a new config
|
||||
# knob (e.g. `refreshrate`), dwm.c starts referencing a symbol our
|
||||
# hand-written config.h doesn't define, breaking the build. To keep
|
||||
# "always latest master" sustainable, copy across any scalar knob that the
|
||||
# current dwm.c needs but our config.h lacks, using upstream's default.
|
||||
# Only single-line scalars referenced by dwm.c are merged (arrays we own
|
||||
# and unused symbols are left alone), so our customisations always win.
|
||||
# ---------------------------------------------------------------------------
|
||||
heal_config() {
|
||||
local defh="$SRC_DIR/config.def.h" cfgh="$SRC_DIR/config.h" dwmc="$SRC_DIR/dwm.c"
|
||||
[[ -f "$defh" && -f "$dwmc" ]] || return 0
|
||||
local line name added=0
|
||||
while IFS= read -r line; do
|
||||
name="$(sed -nE 's/^.*[^A-Za-z0-9_]([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=.*/\1/p' <<<"$line")"
|
||||
[[ -n "$name" ]] || continue
|
||||
grep -qw "$name" "$cfgh" && continue # we already define it — keep ours
|
||||
grep -qw "$name" "$dwmc" || continue # dwm.c doesn't need it — skip
|
||||
printf '%s\n' "$line" >>"$cfgh"
|
||||
warn "config.h: auto-merged new upstream knob '$name' (bleeding-edge churn)"
|
||||
added=1
|
||||
done < <(grep -E '^[[:space:]]*static[[:space:]]+const[[:space:]]+[^={]*=[^;{]*;' "$defh")
|
||||
((added)) && log "Merged upstream config symbol(s) into config.h for this build."
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3c. Focus-on-click: stop the pointer from changing focus / switching monitors.
|
||||
# dwm defaults to focus-follows-mouse — and worse, crossing the screen
|
||||
# boundary over EMPTY space (motionnotify) switches the active monitor, which
|
||||
# yanks focus away from a fullscreen game on the other screen (no window edge
|
||||
# to stop the pointer). We rewrite enternotify + motionnotify to no-ops so
|
||||
# focus only changes on a CLICK or via the Mod+,/. (Mod+Ctrl+arrows) keys.
|
||||
# Applied as an idempotent source rewrite after each reset so it survives the
|
||||
# bleeding-edge `git pull`. perl -0777 slurps the file so the multi-line
|
||||
# function bodies match at once (.*? stops at the first column-0 `}`, i.e. the
|
||||
# function's own closing brace — nested if-block braces are tab-indented).
|
||||
# The dwm/patches/focus-on-click.patch file is the human-readable equivalent.
|
||||
# If upstream refactors these handlers the rewrite no-ops and we warn loudly
|
||||
# rather than silently dropping the behaviour.
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_focusonclick() {
|
||||
local src="$SRC_DIR/dwm.c"
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
perl -0777 -i -pe '
|
||||
s!\nenternotify\(XEvent \*e\)\n\{.*?\n\}\n!\nenternotify(XEvent *e)\n{\n\t/* focusonclick: pointer never changes focus; use a click or Mod+keys. */\n\t(void)e;\n}\n!s;
|
||||
s!\nmotionnotify\(XEvent \*e\)\n\{.*?\n\}\n!\nmotionnotify(XEvent *e)\n{\n\t/* focusonclick: keep the active monitor fixed when crossing screens. */\n\t(void)e;\n}\n!s;
|
||||
' "$src"
|
||||
|
||||
# Verify both rewrites landed; warn (never abort) if upstream changed shape.
|
||||
local ok=1
|
||||
grep -q 'focusonclick: pointer never changes focus' "$src" || ok=0
|
||||
grep -q 'focusonclick: keep the active monitor fixed' "$src" || ok=0
|
||||
if ((ok)); then
|
||||
log "Applied focus-on-click (pointer no longer changes focus or switches monitors)."
|
||||
else
|
||||
warn "focus-on-click rewrite did NOT match upstream dwm.c — pointer focus unchanged."
|
||||
warn "enternotify/motionnotify were likely refactored upstream; update dwm/patches."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3d. Auto pointer-confinement on fullscreen. dwm has no pointer barriers, so on
|
||||
# a dual-monitor setup the cursor slides off a fullscreen game onto the other
|
||||
# screen (there is no window edge to stop it). We hook setfullscreen() to
|
||||
# start the `pointer-confine` helper (XFixes barriers) when a window goes
|
||||
# fullscreen and stop it when fullscreen ends; unmanage() also stops it so a
|
||||
# game that closes WHILE fullscreen can never leave the cursor trapped. The
|
||||
# hook is a quick `if (system(...)) {}` — the `if` consumes system()'s result
|
||||
# so -Wall stays warning-free; the trailing `&` returns to dwm immediately.
|
||||
# Reapplied after each reset (idempotent via git) and self-verifying; the
|
||||
# dwm/patches/fullscreen-pointer-confine.patch file mirrors it for reading.
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_fullscreen_confine_hook() {
|
||||
local src="$SRC_DIR/dwm.c"
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
perl -0777 -i -pe '
|
||||
s!(\n\t\tc->isfullscreen = 1;\n)!$1\t\tif (system("pconfine-auto on &")) {}\n!;
|
||||
s!(\n\t\tc->isfullscreen = 0;\n)!$1\t\tif (system("pconfine-auto off &")) {}\n!;
|
||||
s!(\nunmanage\(Client \*c, int destroyed\)\n\{\n\tMonitor \*m = c->mon;\n\tXWindowChanges wc;\n)!$1\tif (c->isfullscreen) { if (system("pconfine-auto off &")) {} }\n!;
|
||||
' "$src"
|
||||
|
||||
# Expect: 1 "on", 2 "off" (setfullscreen-leave + unmanage). Warn if not.
|
||||
local on off
|
||||
on=$(grep -c 'pconfine-auto on' "$src")
|
||||
off=$(grep -c 'pconfine-auto off' "$src")
|
||||
if [[ "$on" == 1 && "$off" == 2 ]]; then
|
||||
log "Applied auto pointer-confinement hook (locks the cursor to a fullscreen window's screen)."
|
||||
else
|
||||
warn "pointer-confine hook only partially applied (on=$on off=$off, expected 1/2)."
|
||||
warn "setfullscreen/unmanage were likely refactored upstream; update dwm/patches."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Build & install (PREFIX defaults to /usr/local).
|
||||
# ---------------------------------------------------------------------------
|
||||
build_install() {
|
||||
log "Compiling dwm…"
|
||||
make -C "$SRC_DIR" clean >/dev/null
|
||||
make -C "$SRC_DIR" 2>&1 | tail -15
|
||||
log "Installing dwm (sudo make install)…"
|
||||
sudo make -C "$SRC_DIR" install 2>&1 | tail -8
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4b. Build & install the pointer-confine helper (XFixes barriers) from the
|
||||
# versioned dwm/pointer-confine.c. Standalone C so it stays out of dwm.c;
|
||||
# dwm only spawns it via the setfullscreen() hook. If the X dev headers are
|
||||
# missing it fails soft: warn and skip, leaving the rest of dwm fully working
|
||||
# (fullscreen just won't auto-lock the cursor).
|
||||
# ---------------------------------------------------------------------------
|
||||
build_pointer_confine() {
|
||||
log "Compiling pointer-confine from the repo (${REPO_DWM_DIR}/pointer-confine.c)…"
|
||||
local bin
|
||||
bin="$(mktemp)"
|
||||
if cc -std=c99 -pedantic -Wall -O2 "${REPO_DWM_DIR}/pointer-confine.c" -o "$bin" \
|
||||
-lX11 -lXfixes -lXinerama 2>/tmp/pointer-confine-build.log; then
|
||||
sudo install -m 755 "$bin" "$BIN_CONFINE"
|
||||
log "Installed $BIN_CONFINE."
|
||||
else
|
||||
warn "pointer-confine failed to compile — fullscreen cursor-lock disabled (dwm itself is fine):"
|
||||
sed 's/^/ /' /tmp/pointer-confine-build.log >&2 || true
|
||||
fi
|
||||
rm -f "$bin"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Install the helper scripts (from the repo) and register the lightdm
|
||||
# xsession. The scripts are the versioned files in dwm/bin/; we just place
|
||||
# them on PATH with the right mode.
|
||||
# ---------------------------------------------------------------------------
|
||||
write_session_files() {
|
||||
log "Installing helper scripts from the repo and the lightdm xsession entry…"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwm-session" "$BIN_SESSION"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwmstatus" "$BIN_STATUS"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwm-rebuild" "$BIN_REBUILD"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/switch-wm" "$BIN_SWITCH"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/pconfine-auto" "$BIN_CONFINE_AUTO"
|
||||
|
||||
# --- xsession entry for lightdm (absolute Exec path) --------------------
|
||||
sudo tee "$XSESSION" >/dev/null <<'DESKTOP_EOF'
|
||||
[Desktop Entry]
|
||||
Name=dwm (i3-like)
|
||||
Comment=dynamic window manager, compiled from source
|
||||
Exec=/usr/local/bin/dwm-session
|
||||
TryExec=/usr/local/bin/dwm
|
||||
Type=Application
|
||||
DesktopNames=dwm
|
||||
DESKTOP_EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Verify the build links and the session is registered.
|
||||
# ---------------------------------------------------------------------------
|
||||
verify() {
|
||||
log "Verifying install…"
|
||||
local ver
|
||||
ver="$(dwm -v 2>&1)" || true # dwm -v prints version then exit(1)
|
||||
log "dwm version: ${ver:-<none>}"
|
||||
command -v dwm >/dev/null && log "dwm binary: $(command -v dwm)"
|
||||
[[ -f "$XSESSION" ]] && log "xsession registered: $XSESSION"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
cat <<SUMMARY
|
||||
|
||||
dwm is installed alongside i3 (i3 untouched).
|
||||
This machine autologins (no session picker), so choose the WM you boot into:
|
||||
switch-wm dwm -> boot dwm switch-wm i3 -> boot i3 switch-wm -> show
|
||||
then reboot. Recovery if dwm misbehaves: TTY (Ctrl+Alt+F3) -> 'switch-wm i3' -> reboot.
|
||||
|
||||
Key bindings (Mod = Super):
|
||||
Mod+Return terminator Mod+d dmenu
|
||||
Mod+j / Mod+k focus next / prev Mod+Shift+j/k move in stack
|
||||
Mod+h / Mod+l shrink / grow master Mod+i/Shift+i +/- master count
|
||||
Mod+1..0 view tag 1..10 Mod+Shift+1..0 send to tag
|
||||
Mod+f fullscreen Mod+Shift+space toggle floating
|
||||
Mod+t / Mod+w tiling / monocle Mod+Shift+Return promote to master
|
||||
Mod+Shift+q kill window Mod+Shift+e exit dwm
|
||||
Mod+m mic mute Mod+Shift+r recompile (dwm-rebuild)
|
||||
|
||||
Two monitors (no i3-style per-output workspaces — see config.h note):
|
||||
Mod+, / Mod+. focus the other screen (or Mod+Ctrl+Left/Right)
|
||||
Mod+Shift+, / Mod+Shift+. throw window there (or Mod+Ctrl+Shift+L/R)
|
||||
Focus-on-click is ON: the pointer no longer steals focus or switches monitors
|
||||
when it crosses screens. Focus changes on click/keys.
|
||||
Fullscreen cursor-lock is ON: when a window goes fullscreen (games), the cursor
|
||||
is trapped on that screen (XFixes barriers) and released when fullscreen ends.
|
||||
Stuck barrier? Mod+Shift+p force-releases it.
|
||||
|
||||
Status bar (clock, temps, load, RAM, volume) needs xsetroot:
|
||||
sudo pacman -S xorg-xsetroot # then log out / back in
|
||||
Preview it now without the bar: dwmstatus once
|
||||
|
||||
Customise (permanent): edit files in linux_configuration/dwm/ then re-run this
|
||||
script. Quick experiment: edit ~/.local/src/dwm/config.h + run 'dwm-rebuild'
|
||||
(re-running setup_dwm.sh overwrites it from the repo).
|
||||
SUMMARY
|
||||
}
|
||||
217
linux_configuration/scripts/single_use/features/lib/local_llm_model.sh
Executable file
217
linux_configuration/scripts/single_use/features/lib/local_llm_model.sh
Executable file
@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
# Modelfile authoring and model pull for the local LLM.
|
||||
#
|
||||
# Sourced by setup_local_llm.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# ── 4. Create Modelfile with developer system prompt ─────────────────────────
|
||||
create_modelfile() {
|
||||
section "Creating custom model with developer system prompt"
|
||||
|
||||
# Condense Claude preferences into a system prompt (~1 K tokens).
|
||||
# Note: hooks, skills, and Claude Code MCP tools do NOT port to local models.
|
||||
# This only captures code-style, persona, and workflow preferences.
|
||||
local modelfile
|
||||
modelfile="$(mktemp --suffix=.modelfile)"
|
||||
|
||||
cat > "$modelfile" << MODELFILE_EOF
|
||||
FROM ${MODEL}
|
||||
|
||||
PARAMETER num_ctx ${CTX_LEN}
|
||||
PARAMETER temperature 0.6
|
||||
PARAMETER top_p 0.95
|
||||
PARAMETER top_k 20
|
||||
|
||||
SYSTEM """
|
||||
You are a coding assistant for Krzysztof (kuhy) Rudnicki, a software developer
|
||||
who maintains personal automation and app monorepos.
|
||||
|
||||
## About me
|
||||
- Stack: Python, Bash, C/C++, TypeScript, Dart/Flutter, Go
|
||||
- OS: Arch Linux, i3 window manager
|
||||
- Key repos: testsAndMisc (automation), linux-configuration, praca_magisterska
|
||||
|
||||
## Code style
|
||||
Python:
|
||||
- Use from __future__ import annotations for forward refs
|
||||
- Google docstring convention (triple-quoted, brief summary + Args/Returns)
|
||||
- Absolute imports only, no relative imports
|
||||
- Type hints required on every function signature
|
||||
- Prefer typed dataclasses over plain dicts
|
||||
|
||||
Shell (bash):
|
||||
- Always set -euo pipefail at the top
|
||||
- Double-quote all variable expansions
|
||||
- Avoid fork-heavy patterns: prefer /proc, /sys, bash builtins over \$(...)
|
||||
in tight loops or status-bar scripts (fork-storm anti-pattern)
|
||||
- Use jq/yq for JSON/YAML, not grep/awk
|
||||
- NEVER embed multi-line Python logic inside shell heredocs — put it in a
|
||||
separate .py file and invoke with python3 path/to/file.py
|
||||
|
||||
TypeScript:
|
||||
- Target ES2022, pure ESM (no CommonJS require)
|
||||
- No any — use unknown + narrowing
|
||||
- Discriminated unions for state machines
|
||||
- async/await + structured error handling
|
||||
|
||||
## Behaviour I expect
|
||||
- Keep responses brief and direct — no filler preamble
|
||||
- Ask before making high-impact changes (schema changes, deleting data)
|
||||
- If you spot a bad approach, say so before complying
|
||||
- Development order: implement → run to verify → confirm → tests
|
||||
(tests are always LAST, not first)
|
||||
- Include only one short comment per non-obvious block; no inline summaries
|
||||
of what the code obviously does
|
||||
|
||||
## Testing rules
|
||||
- 100% branch coverage required for Python packages
|
||||
- Use pytest + coverage; never mock the filesystem in integration tests
|
||||
- Shell scripts: shellcheck must pass with no suppressions
|
||||
|
||||
## Git workflow
|
||||
- Commit directly to main (no branches for personal repo)
|
||||
- Conventional commit format: type: brief description
|
||||
- Always run pre-commit hooks before committing
|
||||
"""
|
||||
MODELFILE_EOF
|
||||
|
||||
local custom_model="${MODEL/:/-}-dev"
|
||||
ollama create "$custom_model" -f "$modelfile"
|
||||
rm -f "$modelfile"
|
||||
ok "Custom model created: $custom_model (includes developer system prompt)"
|
||||
ok "Load '$custom_model' in Open WebUI for kuhy-personalised responses"
|
||||
}
|
||||
|
||||
# ── 5. Pull base model ────────────────────────────────────────────────────────
|
||||
pull_model() {
|
||||
section "Pulling ${MODEL}"
|
||||
|
||||
# Check if already present
|
||||
if curl -sf "http://localhost:${OLLAMA_PORT}/api/tags" \
|
||||
| grep -q "\"${MODEL}\""; then
|
||||
ok "${MODEL} already downloaded"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Downloading ${MODEL} — approx 20 GB. This will take a while."
|
||||
warn "Progress is shown live; do not interrupt."
|
||||
ollama pull "${MODEL}"
|
||||
ok "${MODEL} downloaded"
|
||||
}
|
||||
|
||||
# ── 6. Set up Open WebUI via Docker ───────────────────────────────────────────
|
||||
setup_webui() {
|
||||
section "Setting up Open WebUI"
|
||||
|
||||
# Remove existing container so we can apply any config changes cleanly
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$WEBUI_CONTAINER"; then
|
||||
warn "Removing existing $WEBUI_CONTAINER container..."
|
||||
docker rm -f "$WEBUI_CONTAINER"
|
||||
fi
|
||||
|
||||
docker pull ghcr.io/open-webui/open-webui:main
|
||||
|
||||
docker run -d \
|
||||
--name "$WEBUI_CONTAINER" \
|
||||
--restart always \
|
||||
-p "${WEBUI_PORT}:8080" \
|
||||
-v "${WEBUI_VOLUME}:/app/backend/data" \
|
||||
-e OLLAMA_BASE_URL="http://host.docker.internal:${OLLAMA_PORT}" \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
|
||||
ok "Open WebUI container started on port ${WEBUI_PORT}"
|
||||
}
|
||||
|
||||
# ── 7. Enable Docker on boot ──────────────────────────────────────────────────
|
||||
enable_autostart() {
|
||||
section "Enabling auto-start on boot"
|
||||
|
||||
# Docker daemon must start for the container (restart=always) to come up
|
||||
sudo systemctl enable docker
|
||||
ok "docker.service enabled on boot"
|
||||
ok "Ollama is already enabled; Open WebUI container uses restart=always"
|
||||
}
|
||||
|
||||
# ── 8. Verify: GPU utilisation + generation speed ─────────────────────────────
|
||||
verify_gpu() {
|
||||
section "Verifying GPU utilisation and generation speed"
|
||||
|
||||
# Warm up with a short generation and measure tok/s
|
||||
warn "Running a warm-up generation (checking GPU offload + speed)..."
|
||||
local start_ts end_ts elapsed toks_per_sec
|
||||
start_ts="$(date +%s%N)"
|
||||
|
||||
local response
|
||||
response="$(curl -sf "http://localhost:${OLLAMA_PORT}/api/generate" \
|
||||
-d "{\"model\":\"${MODEL}\",\"prompt\":\"Reply with exactly: ready\",\"stream\":false}" \
|
||||
--max-time 120)"
|
||||
|
||||
end_ts="$(date +%s%N)"
|
||||
elapsed=$(( (end_ts - start_ts) / 1000000 )) # milliseconds
|
||||
|
||||
if [[ -z "$response" ]]; then
|
||||
err "No response from Ollama — generation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse eval_count (output tokens) and eval_duration (nanoseconds) from response
|
||||
local eval_count eval_duration_ns
|
||||
eval_count="$(printf '%s' "$response" | grep -o '"eval_count":[0-9]*' | grep -o '[0-9]*' || echo 0)"
|
||||
eval_duration_ns="$(printf '%s' "$response" | grep -o '"eval_duration":[0-9]*' | grep -o '[0-9]*' || echo 1)"
|
||||
|
||||
if (( eval_count > 0 && eval_duration_ns > 0 )); then
|
||||
toks_per_sec=$(( eval_count * 1000000000 / eval_duration_ns ))
|
||||
ok "Generation speed: ${toks_per_sec} tok/s (${eval_count} tokens in ${elapsed}ms)"
|
||||
if (( toks_per_sec < 10 )); then
|
||||
warn "Speed is below 10 tok/s — model may be partially CPU-offloaded"
|
||||
warn "Run: ollama ps to check GPU layer count"
|
||||
warn "If GPU% < 100, try reducing CTX_LEN in this script or use a smaller quant"
|
||||
fi
|
||||
else
|
||||
warn "Could not parse tok/s from response; check manually with: ollama ps"
|
||||
fi
|
||||
|
||||
# Check GPU offload via ollama ps
|
||||
warn "Checking GPU layer utilisation..."
|
||||
if command -v ollama &>/dev/null; then
|
||||
ollama ps 2>/dev/null | head -10 || true
|
||||
fi
|
||||
|
||||
ok "Verify done — see ollama ps output above for GPU% column"
|
||||
}
|
||||
|
||||
# ── 9. WebUI readiness check ──────────────────────────────────────────────────
|
||||
verify_webui() {
|
||||
section "Waiting for Open WebUI to become ready"
|
||||
wait_for_url "http://localhost:${WEBUI_PORT}" "Open WebUI" 60
|
||||
}
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
print_summary() {
|
||||
local custom_model="${MODEL/:/-}-dev"
|
||||
printf '\n\033[1;32m'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf ' Local LLM setup complete!\n'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf ' Open WebUI: http://localhost:%s\n' "$WEBUI_PORT"
|
||||
printf ' Ollama API: http://localhost:%s\n' "$OLLAMA_PORT"
|
||||
printf ' Primary model: %s (%d K context)\n' "$MODEL" $(( CTX_LEN / 1024 ))
|
||||
printf ' Dev model: %s (with code-style system prompt)\n' "$custom_model"
|
||||
printf '\n'
|
||||
printf ' GPU fit notes:\n'
|
||||
printf ' Current context: %d K (safe for 24 GB VRAM)\n' $(( CTX_LEN / 1024 ))
|
||||
printf ' To try 24 K: edit CTX_LEN=24576 in this script\n'
|
||||
printf ' To try 32 K: edit CTX_LEN=32768 (risks partial CPU offload)\n'
|
||||
printf ' After changes: re-run this script; check tok/s in verify step\n'
|
||||
printf '\n'
|
||||
printf ' To use with Claude Code:\n'
|
||||
printf ' Claude Code uses the Anthropic API — it cannot point at Ollama\n'
|
||||
printf ' directly (different wire format). Use Open WebUI for local-model\n'
|
||||
printf ' coding sessions, or ask about setting up a LiteLLM proxy.\n'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf '\033[0m\n'
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
249
linux_configuration/scripts/single_use/features/lib/unreal_mcp_repo.sh
Executable file
249
linux_configuration/scripts/single_use/features/lib/unreal_mcp_repo.sh
Executable file
@ -0,0 +1,249 @@
|
||||
#!/bin/bash
|
||||
# Repo checkout, launcher install and MCP client registration.
|
||||
#
|
||||
# Sourced by install_unreal_mcp.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# ---------- Git clone/update ----------
|
||||
setup_repo() {
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
if [[ ! -d "$REPO_DIR/.git" ]]; then
|
||||
log "Cloning unreal-mcp into $REPO_DIR"
|
||||
if require_cmd git; then
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
else
|
||||
fail "git is required but not found after install"
|
||||
fi
|
||||
else
|
||||
log "Repo exists at $REPO_DIR"
|
||||
if [[ $FORCE_UPDATE == true ]]; then
|
||||
log "Updating repo with --force-update"
|
||||
git -C "$REPO_DIR" fetch origin
|
||||
git -C "$REPO_DIR" reset --hard origin/main
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
else
|
||||
log "Pulling latest changes"
|
||||
git -C "$REPO_DIR" pull --rebase --autostash
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure ownership for the real user when script ran via sudo
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Launcher ----------
|
||||
install_launcher() {
|
||||
local bin_dir="$USER_HOME/.local/bin"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local launcher="$bin_dir/unreal-mcp-server"
|
||||
mkdir -p "$bin_dir"
|
||||
cat > "$launcher" << EOF
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null
|
||||
EOF
|
||||
chmod +x "$launcher"
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
|
||||
log "Installed launcher: $launcher"
|
||||
}
|
||||
|
||||
# ---------- VS Code: Continue MCP config ----------
|
||||
configure_continue() {
|
||||
if [[ $CONFIGURE_CONTINUE != true ]]; then
|
||||
log "Skipping Continue config (--no-continue)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cont_dir="$USER_HOME/.continue"
|
||||
local cont_cfg="$cont_dir/config.json"
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
mkdir -p "$cont_dir"
|
||||
|
||||
# Base JSON when no config exists
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
if [[ ! -f $cont_cfg ]]; then
|
||||
cat > "$tmp_file" << JSON
|
||||
{
|
||||
"mcpServers": {
|
||||
"unrealMCP": {
|
||||
"command": "uv",
|
||||
"args": ["--directory", "$python_dir", "run", "unreal_mcp_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
mv "$tmp_file" "$cont_cfg"
|
||||
else
|
||||
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to merge ~/.continue/config.json"
|
||||
fi
|
||||
jq --arg dir "$python_dir" '
|
||||
.mcpServers = (.mcpServers // {}) |
|
||||
.mcpServers.unrealMCP = {
|
||||
command: "uv",
|
||||
args: ["--directory", $dir, "run", "unreal_mcp_server.py"]
|
||||
}
|
||||
' "$cont_cfg" > "$tmp_file" && mv "$tmp_file" "$cont_cfg"
|
||||
fi
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
|
||||
log "Configured Continue MCP at: $cont_cfg"
|
||||
}
|
||||
|
||||
# ---------- VS Code user MCP (native) ----------
|
||||
configure_vscode_user_mcp() {
|
||||
if [[ $CONFIGURE_VSCODE_USER != true ]]; then
|
||||
log "Skipping VS Code user MCP config (--no-vscode)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! require_cmd jq; then
|
||||
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
|
||||
fi
|
||||
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local json
|
||||
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
|
||||
|
||||
# Handle multiple VS Code variants if present
|
||||
local candidates=(code code-insiders codium)
|
||||
local found_any=false
|
||||
for cli in "${candidates[@]}"; do
|
||||
if ! command -v "$cli" > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
found_any=true
|
||||
log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
|
||||
if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then
|
||||
log "[$cli] user profile: unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
|
||||
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
|
||||
fi
|
||||
|
||||
# Detect profiles with 'unreal' (case-insensitive) and add there too
|
||||
local data_dir=""
|
||||
case "$cli" in
|
||||
code)
|
||||
data_dir="$USER_HOME/.config/Code"
|
||||
;;
|
||||
code-insiders)
|
||||
data_dir="$USER_HOME/.config/Code - Insiders"
|
||||
;;
|
||||
codium)
|
||||
data_dir="$USER_HOME/.config/VSCodium"
|
||||
;;
|
||||
esac
|
||||
local profiles_json="$data_dir/User/profiles/profiles.json"
|
||||
if [[ -f $profiles_json ]]; then
|
||||
# Extract profile names matching /unreal/i
|
||||
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
|
||||
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
|
||||
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
|
||||
local name
|
||||
for name in "${unreal_profiles[@]}"; do
|
||||
log "[$cli] Adding unrealMCP to profile: $name"
|
||||
if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
|
||||
log "[$cli] profile '$name': unrealMCP added/updated"
|
||||
else
|
||||
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
|
||||
fail "[$cli] --add-mcp failed for profile '$name'."
|
||||
fi
|
||||
done
|
||||
else
|
||||
log "[$cli] No VS Code profiles with 'unreal' in name"
|
||||
fi
|
||||
else
|
||||
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $found_any == false ]]; then
|
||||
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- Unreal Plugin copy (optional) ----------
|
||||
install_plugin_into_project() {
|
||||
[[ -n $PROJECT_UPROJECT ]] || return 0
|
||||
local upath="$PROJECT_UPROJECT"
|
||||
if [[ -d $upath ]]; then
|
||||
# Resolve .uproject in the provided directory
|
||||
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true)
|
||||
if [[ ${#_uprojects[@]} -eq 0 ]]; then
|
||||
fail "--project directory '$upath' contains no .uproject files"
|
||||
elif [[ ${#_uprojects[@]} -gt 1 ]]; then
|
||||
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
|
||||
printf ' - %s\n' "${_uprojects[@]}" >&2
|
||||
fail "Please pass the specific .uproject path to --project"
|
||||
else
|
||||
upath="${_uprojects[0]}"
|
||||
log "Resolved .uproject: $upath"
|
||||
fi
|
||||
elif [[ -f $upath ]]; then
|
||||
true
|
||||
else
|
||||
fail "--project path does not exist: $upath"
|
||||
fi
|
||||
if [[ ${upath##*.} != "uproject" ]]; then
|
||||
fail "--project must point to a .uproject file (got: $upath)"
|
||||
fi
|
||||
local proj_dir
|
||||
proj_dir="$(cd "$(dirname "$upath")" && pwd)"
|
||||
RESOLVED_PROJECT_DIR="$proj_dir"
|
||||
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
|
||||
local dst_plugin="$proj_dir/Plugins/UnrealMCP"
|
||||
if [[ ! -d $src_plugin ]]; then
|
||||
fail "Source plugin not found at $src_plugin (did repo layout change?)"
|
||||
fi
|
||||
mkdir -p "$proj_dir/Plugins"
|
||||
log "Copying UnrealMCP plugin to project: $dst_plugin"
|
||||
rsync -a --delete "$src_plugin/" "$dst_plugin/"
|
||||
# Set ownership back to actual user if run as root
|
||||
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
|
||||
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
|
||||
}
|
||||
|
||||
# ---------- Summary ----------
|
||||
print_summary() {
|
||||
local python_dir="$REPO_DIR/Python"
|
||||
local plugin_dest="N/A"
|
||||
if [[ -n $RESOLVED_PROJECT_DIR ]]; then
|
||||
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
|
||||
fi
|
||||
cat << EOF
|
||||
============================================
|
||||
Unreal MCP setup complete
|
||||
============================================
|
||||
|
||||
Repo: $REPO_DIR
|
||||
Python dir: $python_dir
|
||||
Launcher: $USER_HOME/.local/bin/unreal-mcp-server
|
||||
|
||||
VS Code (Continue) MCP configured: ${CONFIGURE_CONTINUE}
|
||||
- File: $USER_HOME/.continue/config.json
|
||||
- Server ID: unrealMCP
|
||||
|
||||
VS Code (User profile) MCP configured: ${CONFIGURE_VSCODE_USER}
|
||||
- Command used: code --add-mcp '{"name":"unrealMCP", "command":"uv", "args":["--directory","$python_dir","run","unreal_mcp_server.py"]}'
|
||||
|
||||
Optional usage:
|
||||
- Run server manually: unreal-mcp-server
|
||||
- In VS Code with Continue installed, the unrealMCP server will auto-start when needed.
|
||||
|
||||
Unreal plugin:
|
||||
- Source: MCPGameProject/Plugins/UnrealMCP
|
||||
- If you provided --project, the plugin was copied to: $plugin_dest
|
||||
- In the Unreal Editor: Edit > Plugins > search "UnrealMCP" and enable. Restart when prompted.
|
||||
|
||||
Notes:
|
||||
- Ensure you have Unreal Engine 5.5+ installed.
|
||||
- The Python server listens to the Unreal plugin on TCP port 55557 by default.
|
||||
- For other MCP clients (Claude Desktop, Cursor, Windsurf), copy the JSON snippet from the repo README to their config locations.
|
||||
EOF
|
||||
}
|
||||
@ -126,220 +126,8 @@ fetch_dwm() {
|
||||
log "dwm source now at commit $(git -C "$SRC_DIR" rev-parse --short HEAD) ($(git -C "$SRC_DIR" log -1 --format=%cd --date=short))"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Install our versioned config.h onto the fresh upstream clone. dwm.c stays
|
||||
# pristine here — movestack()/togglefullscr() are defined inside config.h, so
|
||||
# only the two intentional behaviour changes below patch dwm.c.
|
||||
# ---------------------------------------------------------------------------
|
||||
install_config() {
|
||||
log "Installing config.h from the repo (${REPO_DWM_DIR}/config.h)…"
|
||||
cp -- "${REPO_DWM_DIR}/config.h" "$SRC_DIR/config.h"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3b. Auto-merge bleeding-edge config churn. When upstream adds a new config
|
||||
# knob (e.g. `refreshrate`), dwm.c starts referencing a symbol our
|
||||
# hand-written config.h doesn't define, breaking the build. To keep
|
||||
# "always latest master" sustainable, copy across any scalar knob that the
|
||||
# current dwm.c needs but our config.h lacks, using upstream's default.
|
||||
# Only single-line scalars referenced by dwm.c are merged (arrays we own
|
||||
# and unused symbols are left alone), so our customisations always win.
|
||||
# ---------------------------------------------------------------------------
|
||||
heal_config() {
|
||||
local defh="$SRC_DIR/config.def.h" cfgh="$SRC_DIR/config.h" dwmc="$SRC_DIR/dwm.c"
|
||||
[[ -f "$defh" && -f "$dwmc" ]] || return 0
|
||||
local line name added=0
|
||||
while IFS= read -r line; do
|
||||
name="$(sed -nE 's/^.*[^A-Za-z0-9_]([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=.*/\1/p' <<<"$line")"
|
||||
[[ -n "$name" ]] || continue
|
||||
grep -qw "$name" "$cfgh" && continue # we already define it — keep ours
|
||||
grep -qw "$name" "$dwmc" || continue # dwm.c doesn't need it — skip
|
||||
printf '%s\n' "$line" >>"$cfgh"
|
||||
warn "config.h: auto-merged new upstream knob '$name' (bleeding-edge churn)"
|
||||
added=1
|
||||
done < <(grep -E '^[[:space:]]*static[[:space:]]+const[[:space:]]+[^={]*=[^;{]*;' "$defh")
|
||||
((added)) && log "Merged upstream config symbol(s) into config.h for this build."
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3c. Focus-on-click: stop the pointer from changing focus / switching monitors.
|
||||
# dwm defaults to focus-follows-mouse — and worse, crossing the screen
|
||||
# boundary over EMPTY space (motionnotify) switches the active monitor, which
|
||||
# yanks focus away from a fullscreen game on the other screen (no window edge
|
||||
# to stop the pointer). We rewrite enternotify + motionnotify to no-ops so
|
||||
# focus only changes on a CLICK or via the Mod+,/. (Mod+Ctrl+arrows) keys.
|
||||
# Applied as an idempotent source rewrite after each reset so it survives the
|
||||
# bleeding-edge `git pull`. perl -0777 slurps the file so the multi-line
|
||||
# function bodies match at once (.*? stops at the first column-0 `}`, i.e. the
|
||||
# function's own closing brace — nested if-block braces are tab-indented).
|
||||
# The dwm/patches/focus-on-click.patch file is the human-readable equivalent.
|
||||
# If upstream refactors these handlers the rewrite no-ops and we warn loudly
|
||||
# rather than silently dropping the behaviour.
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_focusonclick() {
|
||||
local src="$SRC_DIR/dwm.c"
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
perl -0777 -i -pe '
|
||||
s!\nenternotify\(XEvent \*e\)\n\{.*?\n\}\n!\nenternotify(XEvent *e)\n{\n\t/* focusonclick: pointer never changes focus; use a click or Mod+keys. */\n\t(void)e;\n}\n!s;
|
||||
s!\nmotionnotify\(XEvent \*e\)\n\{.*?\n\}\n!\nmotionnotify(XEvent *e)\n{\n\t/* focusonclick: keep the active monitor fixed when crossing screens. */\n\t(void)e;\n}\n!s;
|
||||
' "$src"
|
||||
|
||||
# Verify both rewrites landed; warn (never abort) if upstream changed shape.
|
||||
local ok=1
|
||||
grep -q 'focusonclick: pointer never changes focus' "$src" || ok=0
|
||||
grep -q 'focusonclick: keep the active monitor fixed' "$src" || ok=0
|
||||
if ((ok)); then
|
||||
log "Applied focus-on-click (pointer no longer changes focus or switches monitors)."
|
||||
else
|
||||
warn "focus-on-click rewrite did NOT match upstream dwm.c — pointer focus unchanged."
|
||||
warn "enternotify/motionnotify were likely refactored upstream; update dwm/patches."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3d. Auto pointer-confinement on fullscreen. dwm has no pointer barriers, so on
|
||||
# a dual-monitor setup the cursor slides off a fullscreen game onto the other
|
||||
# screen (there is no window edge to stop it). We hook setfullscreen() to
|
||||
# start the `pointer-confine` helper (XFixes barriers) when a window goes
|
||||
# fullscreen and stop it when fullscreen ends; unmanage() also stops it so a
|
||||
# game that closes WHILE fullscreen can never leave the cursor trapped. The
|
||||
# hook is a quick `if (system(...)) {}` — the `if` consumes system()'s result
|
||||
# so -Wall stays warning-free; the trailing `&` returns to dwm immediately.
|
||||
# Reapplied after each reset (idempotent via git) and self-verifying; the
|
||||
# dwm/patches/fullscreen-pointer-confine.patch file mirrors it for reading.
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_fullscreen_confine_hook() {
|
||||
local src="$SRC_DIR/dwm.c"
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
perl -0777 -i -pe '
|
||||
s!(\n\t\tc->isfullscreen = 1;\n)!$1\t\tif (system("pconfine-auto on &")) {}\n!;
|
||||
s!(\n\t\tc->isfullscreen = 0;\n)!$1\t\tif (system("pconfine-auto off &")) {}\n!;
|
||||
s!(\nunmanage\(Client \*c, int destroyed\)\n\{\n\tMonitor \*m = c->mon;\n\tXWindowChanges wc;\n)!$1\tif (c->isfullscreen) { if (system("pconfine-auto off &")) {} }\n!;
|
||||
' "$src"
|
||||
|
||||
# Expect: 1 "on", 2 "off" (setfullscreen-leave + unmanage). Warn if not.
|
||||
local on off
|
||||
on=$(grep -c 'pconfine-auto on' "$src")
|
||||
off=$(grep -c 'pconfine-auto off' "$src")
|
||||
if [[ "$on" == 1 && "$off" == 2 ]]; then
|
||||
log "Applied auto pointer-confinement hook (locks the cursor to a fullscreen window's screen)."
|
||||
else
|
||||
warn "pointer-confine hook only partially applied (on=$on off=$off, expected 1/2)."
|
||||
warn "setfullscreen/unmanage were likely refactored upstream; update dwm/patches."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Build & install (PREFIX defaults to /usr/local).
|
||||
# ---------------------------------------------------------------------------
|
||||
build_install() {
|
||||
log "Compiling dwm…"
|
||||
make -C "$SRC_DIR" clean >/dev/null
|
||||
make -C "$SRC_DIR" 2>&1 | tail -15
|
||||
log "Installing dwm (sudo make install)…"
|
||||
sudo make -C "$SRC_DIR" install 2>&1 | tail -8
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4b. Build & install the pointer-confine helper (XFixes barriers) from the
|
||||
# versioned dwm/pointer-confine.c. Standalone C so it stays out of dwm.c;
|
||||
# dwm only spawns it via the setfullscreen() hook. If the X dev headers are
|
||||
# missing it fails soft: warn and skip, leaving the rest of dwm fully working
|
||||
# (fullscreen just won't auto-lock the cursor).
|
||||
# ---------------------------------------------------------------------------
|
||||
build_pointer_confine() {
|
||||
log "Compiling pointer-confine from the repo (${REPO_DWM_DIR}/pointer-confine.c)…"
|
||||
local bin
|
||||
bin="$(mktemp)"
|
||||
if cc -std=c99 -pedantic -Wall -O2 "${REPO_DWM_DIR}/pointer-confine.c" -o "$bin" \
|
||||
-lX11 -lXfixes -lXinerama 2>/tmp/pointer-confine-build.log; then
|
||||
sudo install -m 755 "$bin" "$BIN_CONFINE"
|
||||
log "Installed $BIN_CONFINE."
|
||||
else
|
||||
warn "pointer-confine failed to compile — fullscreen cursor-lock disabled (dwm itself is fine):"
|
||||
sed 's/^/ /' /tmp/pointer-confine-build.log >&2 || true
|
||||
fi
|
||||
rm -f "$bin"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Install the helper scripts (from the repo) and register the lightdm
|
||||
# xsession. The scripts are the versioned files in dwm/bin/; we just place
|
||||
# them on PATH with the right mode.
|
||||
# ---------------------------------------------------------------------------
|
||||
write_session_files() {
|
||||
log "Installing helper scripts from the repo and the lightdm xsession entry…"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwm-session" "$BIN_SESSION"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwmstatus" "$BIN_STATUS"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/dwm-rebuild" "$BIN_REBUILD"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/switch-wm" "$BIN_SWITCH"
|
||||
sudo install -m 755 "${REPO_DWM_DIR}/bin/pconfine-auto" "$BIN_CONFINE_AUTO"
|
||||
|
||||
# --- xsession entry for lightdm (absolute Exec path) --------------------
|
||||
sudo tee "$XSESSION" >/dev/null <<'DESKTOP_EOF'
|
||||
[Desktop Entry]
|
||||
Name=dwm (i3-like)
|
||||
Comment=dynamic window manager, compiled from source
|
||||
Exec=/usr/local/bin/dwm-session
|
||||
TryExec=/usr/local/bin/dwm
|
||||
Type=Application
|
||||
DesktopNames=dwm
|
||||
DESKTOP_EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Verify the build links and the session is registered.
|
||||
# ---------------------------------------------------------------------------
|
||||
verify() {
|
||||
log "Verifying install…"
|
||||
local ver
|
||||
ver="$(dwm -v 2>&1)" || true # dwm -v prints version then exit(1)
|
||||
log "dwm version: ${ver:-<none>}"
|
||||
command -v dwm >/dev/null && log "dwm binary: $(command -v dwm)"
|
||||
[[ -f "$XSESSION" ]] && log "xsession registered: $XSESSION"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
cat <<SUMMARY
|
||||
|
||||
dwm is installed alongside i3 (i3 untouched).
|
||||
This machine autologins (no session picker), so choose the WM you boot into:
|
||||
switch-wm dwm -> boot dwm switch-wm i3 -> boot i3 switch-wm -> show
|
||||
then reboot. Recovery if dwm misbehaves: TTY (Ctrl+Alt+F3) -> 'switch-wm i3' -> reboot.
|
||||
|
||||
Key bindings (Mod = Super):
|
||||
Mod+Return terminator Mod+d dmenu
|
||||
Mod+j / Mod+k focus next / prev Mod+Shift+j/k move in stack
|
||||
Mod+h / Mod+l shrink / grow master Mod+i/Shift+i +/- master count
|
||||
Mod+1..0 view tag 1..10 Mod+Shift+1..0 send to tag
|
||||
Mod+f fullscreen Mod+Shift+space toggle floating
|
||||
Mod+t / Mod+w tiling / monocle Mod+Shift+Return promote to master
|
||||
Mod+Shift+q kill window Mod+Shift+e exit dwm
|
||||
Mod+m mic mute Mod+Shift+r recompile (dwm-rebuild)
|
||||
|
||||
Two monitors (no i3-style per-output workspaces — see config.h note):
|
||||
Mod+, / Mod+. focus the other screen (or Mod+Ctrl+Left/Right)
|
||||
Mod+Shift+, / Mod+Shift+. throw window there (or Mod+Ctrl+Shift+L/R)
|
||||
Focus-on-click is ON: the pointer no longer steals focus or switches monitors
|
||||
when it crosses screens. Focus changes on click/keys.
|
||||
Fullscreen cursor-lock is ON: when a window goes fullscreen (games), the cursor
|
||||
is trapped on that screen (XFixes barriers) and released when fullscreen ends.
|
||||
Stuck barrier? Mod+Shift+p force-releases it.
|
||||
|
||||
Status bar (clock, temps, load, RAM, volume) needs xsetroot:
|
||||
sudo pacman -S xorg-xsetroot # then log out / back in
|
||||
Preview it now without the bar: dwmstatus once
|
||||
|
||||
Customise (permanent): edit files in linux_configuration/dwm/ then re-run this
|
||||
script. Quick experiment: edit ~/.local/src/dwm/config.h + run 'dwm-rebuild'
|
||||
(re-running setup_dwm.sh overwrites it from the repo).
|
||||
SUMMARY
|
||||
}
|
||||
# shellcheck source=lib/dwm_config.sh
|
||||
source "$SCRIPT_DIR/lib/dwm_config.sh"
|
||||
|
||||
main() {
|
||||
validate_repo_files
|
||||
|
||||
@ -141,216 +141,9 @@ start_ollama() {
|
||||
wait_for_url "http://localhost:${OLLAMA_PORT}/api/tags" "Ollama API" 30
|
||||
}
|
||||
|
||||
# ── 4. Create Modelfile with developer system prompt ─────────────────────────
|
||||
create_modelfile() {
|
||||
section "Creating custom model with developer system prompt"
|
||||
# shellcheck source=lib/local_llm_model.sh
|
||||
source "$SCRIPT_DIR/lib/local_llm_model.sh"
|
||||
|
||||
# Condense Claude preferences into a system prompt (~1 K tokens).
|
||||
# Note: hooks, skills, and Claude Code MCP tools do NOT port to local models.
|
||||
# This only captures code-style, persona, and workflow preferences.
|
||||
local modelfile
|
||||
modelfile="$(mktemp --suffix=.modelfile)"
|
||||
|
||||
cat > "$modelfile" << MODELFILE_EOF
|
||||
FROM ${MODEL}
|
||||
|
||||
PARAMETER num_ctx ${CTX_LEN}
|
||||
PARAMETER temperature 0.6
|
||||
PARAMETER top_p 0.95
|
||||
PARAMETER top_k 20
|
||||
|
||||
SYSTEM """
|
||||
You are a coding assistant for Krzysztof (kuhy) Rudnicki, a software developer
|
||||
who maintains personal automation and app monorepos.
|
||||
|
||||
## About me
|
||||
- Stack: Python, Bash, C/C++, TypeScript, Dart/Flutter, Go
|
||||
- OS: Arch Linux, i3 window manager
|
||||
- Key repos: testsAndMisc (automation), linux-configuration, praca_magisterska
|
||||
|
||||
## Code style
|
||||
Python:
|
||||
- Use from __future__ import annotations for forward refs
|
||||
- Google docstring convention (triple-quoted, brief summary + Args/Returns)
|
||||
- Absolute imports only, no relative imports
|
||||
- Type hints required on every function signature
|
||||
- Prefer typed dataclasses over plain dicts
|
||||
|
||||
Shell (bash):
|
||||
- Always set -euo pipefail at the top
|
||||
- Double-quote all variable expansions
|
||||
- Avoid fork-heavy patterns: prefer /proc, /sys, bash builtins over \$(...)
|
||||
in tight loops or status-bar scripts (fork-storm anti-pattern)
|
||||
- Use jq/yq for JSON/YAML, not grep/awk
|
||||
- NEVER embed multi-line Python logic inside shell heredocs — put it in a
|
||||
separate .py file and invoke with python3 path/to/file.py
|
||||
|
||||
TypeScript:
|
||||
- Target ES2022, pure ESM (no CommonJS require)
|
||||
- No any — use unknown + narrowing
|
||||
- Discriminated unions for state machines
|
||||
- async/await + structured error handling
|
||||
|
||||
## Behaviour I expect
|
||||
- Keep responses brief and direct — no filler preamble
|
||||
- Ask before making high-impact changes (schema changes, deleting data)
|
||||
- If you spot a bad approach, say so before complying
|
||||
- Development order: implement → run to verify → confirm → tests
|
||||
(tests are always LAST, not first)
|
||||
- Include only one short comment per non-obvious block; no inline summaries
|
||||
of what the code obviously does
|
||||
|
||||
## Testing rules
|
||||
- 100% branch coverage required for Python packages
|
||||
- Use pytest + coverage; never mock the filesystem in integration tests
|
||||
- Shell scripts: shellcheck must pass with no suppressions
|
||||
|
||||
## Git workflow
|
||||
- Commit directly to main (no branches for personal repo)
|
||||
- Conventional commit format: type: brief description
|
||||
- Always run pre-commit hooks before committing
|
||||
"""
|
||||
MODELFILE_EOF
|
||||
|
||||
local custom_model="${MODEL/:/-}-dev"
|
||||
ollama create "$custom_model" -f "$modelfile"
|
||||
rm -f "$modelfile"
|
||||
ok "Custom model created: $custom_model (includes developer system prompt)"
|
||||
ok "Load '$custom_model' in Open WebUI for kuhy-personalised responses"
|
||||
}
|
||||
|
||||
# ── 5. Pull base model ────────────────────────────────────────────────────────
|
||||
pull_model() {
|
||||
section "Pulling ${MODEL}"
|
||||
|
||||
# Check if already present
|
||||
if curl -sf "http://localhost:${OLLAMA_PORT}/api/tags" \
|
||||
| grep -q "\"${MODEL}\""; then
|
||||
ok "${MODEL} already downloaded"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Downloading ${MODEL} — approx 20 GB. This will take a while."
|
||||
warn "Progress is shown live; do not interrupt."
|
||||
ollama pull "${MODEL}"
|
||||
ok "${MODEL} downloaded"
|
||||
}
|
||||
|
||||
# ── 6. Set up Open WebUI via Docker ───────────────────────────────────────────
|
||||
setup_webui() {
|
||||
section "Setting up Open WebUI"
|
||||
|
||||
# Remove existing container so we can apply any config changes cleanly
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$WEBUI_CONTAINER"; then
|
||||
warn "Removing existing $WEBUI_CONTAINER container..."
|
||||
docker rm -f "$WEBUI_CONTAINER"
|
||||
fi
|
||||
|
||||
docker pull ghcr.io/open-webui/open-webui:main
|
||||
|
||||
docker run -d \
|
||||
--name "$WEBUI_CONTAINER" \
|
||||
--restart always \
|
||||
-p "${WEBUI_PORT}:8080" \
|
||||
-v "${WEBUI_VOLUME}:/app/backend/data" \
|
||||
-e OLLAMA_BASE_URL="http://host.docker.internal:${OLLAMA_PORT}" \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
|
||||
ok "Open WebUI container started on port ${WEBUI_PORT}"
|
||||
}
|
||||
|
||||
# ── 7. Enable Docker on boot ──────────────────────────────────────────────────
|
||||
enable_autostart() {
|
||||
section "Enabling auto-start on boot"
|
||||
|
||||
# Docker daemon must start for the container (restart=always) to come up
|
||||
sudo systemctl enable docker
|
||||
ok "docker.service enabled on boot"
|
||||
ok "Ollama is already enabled; Open WebUI container uses restart=always"
|
||||
}
|
||||
|
||||
# ── 8. Verify: GPU utilisation + generation speed ─────────────────────────────
|
||||
verify_gpu() {
|
||||
section "Verifying GPU utilisation and generation speed"
|
||||
|
||||
# Warm up with a short generation and measure tok/s
|
||||
warn "Running a warm-up generation (checking GPU offload + speed)..."
|
||||
local start_ts end_ts elapsed toks_per_sec
|
||||
start_ts="$(date +%s%N)"
|
||||
|
||||
local response
|
||||
response="$(curl -sf "http://localhost:${OLLAMA_PORT}/api/generate" \
|
||||
-d "{\"model\":\"${MODEL}\",\"prompt\":\"Reply with exactly: ready\",\"stream\":false}" \
|
||||
--max-time 120)"
|
||||
|
||||
end_ts="$(date +%s%N)"
|
||||
elapsed=$(( (end_ts - start_ts) / 1000000 )) # milliseconds
|
||||
|
||||
if [[ -z "$response" ]]; then
|
||||
err "No response from Ollama — generation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse eval_count (output tokens) and eval_duration (nanoseconds) from response
|
||||
local eval_count eval_duration_ns
|
||||
eval_count="$(printf '%s' "$response" | grep -o '"eval_count":[0-9]*' | grep -o '[0-9]*' || echo 0)"
|
||||
eval_duration_ns="$(printf '%s' "$response" | grep -o '"eval_duration":[0-9]*' | grep -o '[0-9]*' || echo 1)"
|
||||
|
||||
if (( eval_count > 0 && eval_duration_ns > 0 )); then
|
||||
toks_per_sec=$(( eval_count * 1000000000 / eval_duration_ns ))
|
||||
ok "Generation speed: ${toks_per_sec} tok/s (${eval_count} tokens in ${elapsed}ms)"
|
||||
if (( toks_per_sec < 10 )); then
|
||||
warn "Speed is below 10 tok/s — model may be partially CPU-offloaded"
|
||||
warn "Run: ollama ps to check GPU layer count"
|
||||
warn "If GPU% < 100, try reducing CTX_LEN in this script or use a smaller quant"
|
||||
fi
|
||||
else
|
||||
warn "Could not parse tok/s from response; check manually with: ollama ps"
|
||||
fi
|
||||
|
||||
# Check GPU offload via ollama ps
|
||||
warn "Checking GPU layer utilisation..."
|
||||
if command -v ollama &>/dev/null; then
|
||||
ollama ps 2>/dev/null | head -10 || true
|
||||
fi
|
||||
|
||||
ok "Verify done — see ollama ps output above for GPU% column"
|
||||
}
|
||||
|
||||
# ── 9. WebUI readiness check ──────────────────────────────────────────────────
|
||||
verify_webui() {
|
||||
section "Waiting for Open WebUI to become ready"
|
||||
wait_for_url "http://localhost:${WEBUI_PORT}" "Open WebUI" 60
|
||||
}
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
print_summary() {
|
||||
local custom_model="${MODEL/:/-}-dev"
|
||||
printf '\n\033[1;32m'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf ' Local LLM setup complete!\n'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf ' Open WebUI: http://localhost:%s\n' "$WEBUI_PORT"
|
||||
printf ' Ollama API: http://localhost:%s\n' "$OLLAMA_PORT"
|
||||
printf ' Primary model: %s (%d K context)\n' "$MODEL" $(( CTX_LEN / 1024 ))
|
||||
printf ' Dev model: %s (with code-style system prompt)\n' "$custom_model"
|
||||
printf '\n'
|
||||
printf ' GPU fit notes:\n'
|
||||
printf ' Current context: %d K (safe for 24 GB VRAM)\n' $(( CTX_LEN / 1024 ))
|
||||
printf ' To try 24 K: edit CTX_LEN=24576 in this script\n'
|
||||
printf ' To try 32 K: edit CTX_LEN=32768 (risks partial CPU offload)\n'
|
||||
printf ' After changes: re-run this script; check tok/s in verify step\n'
|
||||
printf '\n'
|
||||
printf ' To use with Claude Code:\n'
|
||||
printf ' Claude Code uses the Anthropic API — it cannot point at Ollama\n'
|
||||
printf ' directly (different wire format). Use Open WebUI for local-model\n'
|
||||
printf ' coding sessions, or ask about setting up a LiteLLM proxy.\n'
|
||||
printf '═══════════════════════════════════════════════════════\n'
|
||||
printf '\033[0m\n'
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
log "Local LLM setup — model: ${MODEL}, GPU: RTX 3090, VRAM: 24 GB"
|
||||
|
||||
|
||||
@ -97,262 +97,10 @@ install_optional_tools() {
|
||||
pacman -S --needed --noconfirm "${packages[@]}"
|
||||
}
|
||||
|
||||
collect_basics() {
|
||||
run_and_log "Kernel" uname -a
|
||||
run_and_log "Uptime" uptime
|
||||
run_and_log "Memory" free -h
|
||||
run_and_log "Swap" swapon --show
|
||||
run_and_log "CPU (lscpu)" lscpu
|
||||
run_and_log "Disk Usage" df -h /
|
||||
run_and_log "Boot Time" systemd-analyze
|
||||
run_and_log "Failed Units" systemctl --failed --no-pager
|
||||
run_and_log "Recent Errors (this boot)" journalctl -b -p err --no-pager -n 200
|
||||
|
||||
local cpu_count
|
||||
cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
|
||||
local load1
|
||||
load1=$(awk '{print int($1)}' /proc/loadavg 2>/dev/null || echo 0)
|
||||
if [[ ${load1:-0} -ge ${cpu_count:-1} ]]; then
|
||||
add_finding "1-minute load average is at/above CPU thread count (${load1}/${cpu_count}); background tasks may be saturating the system."
|
||||
fi
|
||||
|
||||
local failed_count
|
||||
failed_count=$(systemctl --failed --no-legend 2>/dev/null | wc -l || true)
|
||||
failed_count=${failed_count//[[:space:]]/}
|
||||
if [[ ${failed_count:-0} -gt 0 ]]; then
|
||||
add_finding "One or more systemd units are failed (${failed_count}); failed services can cause repeated retries and instability."
|
||||
fi
|
||||
|
||||
local acpi_error_count
|
||||
acpi_error_count=$(journalctl -b -p err --no-pager 2>/dev/null | grep -ic 'acpi' || true)
|
||||
if [[ ${acpi_error_count:-0} -ge 5 ]]; then
|
||||
add_finding "Frequent ACPI errors detected in current boot (${acpi_error_count}); BIOS/firmware update may improve stability."
|
||||
fi
|
||||
|
||||
local top_snapshot
|
||||
top_snapshot=$(ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head -n 12 || true)
|
||||
{
|
||||
echo
|
||||
echo "=== Top CPU Processes ==="
|
||||
echo "$top_snapshot"
|
||||
} >>"$REPORT_FILE"
|
||||
|
||||
local xorg_cpu
|
||||
xorg_cpu=$(ps -C Xorg -o %cpu= | awk '{sum+=$1} END {printf "%.0f", sum+0}' || echo 0)
|
||||
if [[ ${xorg_cpu:-0} -ge 20 ]]; then
|
||||
add_finding "Xorg is using high CPU (${xorg_cpu}%); desktop/compositor/GPU driver path may be a primary slowdown source."
|
||||
fi
|
||||
}
|
||||
|
||||
check_cpu_governor() {
|
||||
# An ARRAY, not a newline-joined string: the awk/grep calls below need one
|
||||
# argument per file. With the old unquoted "$gov_files" an empty result
|
||||
# also left awk with NO file operands, so it fell through to reading stdin
|
||||
# and the diagnostic hung instead of reporting 'not found'.
|
||||
local -a gov_files=()
|
||||
mapfile -t gov_files < <(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true)
|
||||
|
||||
if ((${#gov_files[@]} == 0)); then
|
||||
add_action "CPU governor files not found (may be unsupported on this platform)."
|
||||
return
|
||||
fi
|
||||
|
||||
local summary
|
||||
summary=$(awk '{count[$1]++} END {for (g in count) printf "%s:%d ", g, count[g]}' "${gov_files[@]}" 2>/dev/null || true)
|
||||
echo "CPU governor summary: ${summary:-unknown}" >>"$REPORT_FILE"
|
||||
|
||||
if grep -q '^powersave$' "${gov_files[@]}" 2>/dev/null; then
|
||||
add_finding "CPU governor includes 'powersave' on one or more cores; this can make high-end hardware feel slow."
|
||||
fi
|
||||
}
|
||||
|
||||
check_thermal_state() {
|
||||
if has_cmd sensors; then
|
||||
run_and_log "Temperatures (sensors)" sensors
|
||||
else
|
||||
add_action "Install lm_sensors and run 'sensors' to verify thermal throttling."
|
||||
fi
|
||||
|
||||
if has_cmd dmesg; then
|
||||
local therm_hits
|
||||
therm_hits=$(dmesg | grep -Ei 'throttl|thermal|overheat|cpu clock throttled' | tail -n 30 || true)
|
||||
if [[ -n $therm_hits ]]; then
|
||||
add_finding "Kernel logs show thermal/throttling related messages."
|
||||
{
|
||||
echo
|
||||
echo "=== Thermal/Throttling dmesg excerpts ==="
|
||||
echo "$therm_hits"
|
||||
} >>"$REPORT_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_power_services() {
|
||||
local tlp_enabled="false"
|
||||
local ppd_enabled="false"
|
||||
|
||||
if systemctl is-enabled tlp.service >/dev/null 2>&1; then
|
||||
tlp_enabled="true"
|
||||
fi
|
||||
if systemctl is-enabled power-profiles-daemon.service >/dev/null 2>&1; then
|
||||
ppd_enabled="true"
|
||||
fi
|
||||
|
||||
echo "Power services: tlp=${tlp_enabled}, power-profiles-daemon=${ppd_enabled}" >>"$REPORT_FILE"
|
||||
|
||||
if [[ $tlp_enabled == "true" && $ppd_enabled == "true" ]]; then
|
||||
add_finding "Both TLP and power-profiles-daemon are enabled; they often conflict and cause inconsistent performance."
|
||||
fi
|
||||
|
||||
if [[ $tlp_enabled == "false" && $ppd_enabled == "false" ]]; then
|
||||
add_action "No power management daemon is enabled; consider installing/enabling power-profiles-daemon for predictable AC/battery behavior."
|
||||
fi
|
||||
}
|
||||
|
||||
check_storage_health() {
|
||||
run_and_log "Block Devices" lsblk -o NAME,MODEL,ROTA,SIZE,TYPE,MOUNTPOINT,FSTYPE
|
||||
|
||||
if has_cmd fstrim; then
|
||||
run_and_log "fstrim dry-run" fstrim -av --dry-run
|
||||
fi
|
||||
|
||||
if systemctl is-enabled fstrim.timer >/dev/null 2>&1; then
|
||||
add_action "fstrim.timer is enabled (good for SSD performance longevity)."
|
||||
else
|
||||
add_finding "fstrim.timer is not enabled; SSD maintenance trimming may be missing."
|
||||
fi
|
||||
|
||||
if has_cmd smartctl; then
|
||||
local root_disk
|
||||
root_disk=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p$//' || true)
|
||||
if [[ -n ${root_disk:-} && -b $root_disk ]]; then
|
||||
run_and_log "SMART Summary ($root_disk)" smartctl -H "$root_disk"
|
||||
fi
|
||||
else
|
||||
add_action "Install smartmontools and run SMART health checks for your SSD/NVMe."
|
||||
fi
|
||||
}
|
||||
|
||||
check_memory_pressure() {
|
||||
local mem_total mem_available swap_total swap_free
|
||||
mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
|
||||
mem_available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
|
||||
swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo)
|
||||
swap_free=$(awk '/SwapFree/ {print $2}' /proc/meminfo)
|
||||
|
||||
if [[ ${swap_total:-0} -gt 0 ]]; then
|
||||
local swap_used
|
||||
swap_used=$((swap_total - swap_free))
|
||||
local swap_pct
|
||||
swap_pct=$((swap_used * 100 / swap_total))
|
||||
echo "Swap usage: ${swap_pct}%" >>"$REPORT_FILE"
|
||||
if [[ $swap_pct -ge 35 && ${mem_available:-0} -gt $((mem_total / 3)) ]]; then
|
||||
add_finding "High swap usage while RAM is still available; this can cause stutter."
|
||||
add_action "Consider lowering swappiness (temporary: sudo sysctl vm.swappiness=10)."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f /proc/pressure/memory ]]; then
|
||||
run_and_log "Memory PSI" cat /proc/pressure/memory
|
||||
fi
|
||||
}
|
||||
|
||||
check_gpu_state() {
|
||||
if has_cmd nvidia-smi; then
|
||||
run_and_log "NVIDIA State" nvidia-smi
|
||||
local pstate util power
|
||||
pstate=$(nvidia-smi --query-gpu=pstate --format=csv,noheader 2>/dev/null | head -n 1 | xargs || true)
|
||||
util=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true)
|
||||
power=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true)
|
||||
|
||||
{
|
||||
echo "NVIDIA pstate: ${pstate:-unknown}"
|
||||
echo "NVIDIA util: ${util:-unknown}%"
|
||||
echo "NVIDIA power: ${power:-unknown}W"
|
||||
} >>"$REPORT_FILE"
|
||||
|
||||
if [[ ${pstate:-} == "P0" && ${util:-100} -le 5 ]]; then
|
||||
add_finding "NVIDIA GPU is in P0 high-performance state while mostly idle; this can increase heat and trigger thermal limits."
|
||||
add_action "If laptop has hybrid graphics, prefer iGPU mode for desktop workloads and use dGPU on demand."
|
||||
fi
|
||||
else
|
||||
run_and_log "PCI VGA Devices" lspci -nnk | grep -A3 -Ei 'vga|3d|display'
|
||||
fi
|
||||
}
|
||||
|
||||
check_journal_size() {
|
||||
local journal_line
|
||||
journal_line=$(journalctl --disk-usage 2>/dev/null || true)
|
||||
echo "Journal usage: ${journal_line:-unknown}" >>"$REPORT_FILE"
|
||||
|
||||
if [[ $journal_line =~ ([0-9]+\.?[0-9]*)\ (G|M) ]]; then
|
||||
local value unit
|
||||
value="${BASH_REMATCH[1]}"
|
||||
unit="${BASH_REMATCH[2]}"
|
||||
if [[ $unit == "G" ]]; then
|
||||
add_finding "Systemd journal is large (${value}G); excessive logs can waste I/O and disk space."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
apply_safe_fixes() {
|
||||
if [[ $APPLY_SAFE_FIXES != "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Applying safe fixes..."
|
||||
|
||||
if ! systemctl is-enabled fstrim.timer >/dev/null 2>&1; then
|
||||
systemctl enable --now fstrim.timer
|
||||
add_action "Enabled and started fstrim.timer."
|
||||
fi
|
||||
|
||||
if systemctl is-enabled tlp.service >/dev/null 2>&1 && systemctl is-enabled power-profiles-daemon.service >/dev/null 2>&1; then
|
||||
systemctl disable --now tlp.service
|
||||
add_action "Disabled tlp.service to avoid conflict with power-profiles-daemon."
|
||||
fi
|
||||
|
||||
local journal_line
|
||||
journal_line=$(journalctl --disk-usage 2>/dev/null || true)
|
||||
if [[ $journal_line =~ ([0-9]+\.?[0-9]*)\ G ]]; then
|
||||
journalctl --vacuum-size=300M
|
||||
add_action "Vacuumed systemd journal to 300M."
|
||||
fi
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo
|
||||
echo "=============================="
|
||||
echo " Arch Performance Diagnostics"
|
||||
echo "=============================="
|
||||
echo "Report: $REPORT_FILE"
|
||||
echo
|
||||
|
||||
if [[ ${#FINDINGS[@]} -eq 0 ]]; then
|
||||
log_ok "No high-confidence bottlenecks detected by automated checks."
|
||||
else
|
||||
log_warn "Likely issues found (${#FINDINGS[@]}):"
|
||||
local item
|
||||
for item in "${FINDINGS[@]}"; do
|
||||
echo " - $item"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#ACTIONS[@]} -gt 0 ]]; then
|
||||
echo
|
||||
log_info "Actions/recommendations:"
|
||||
local action
|
||||
for action in "${ACTIONS[@]}"; do
|
||||
echo " - $action"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Recommended next command for deep per-process analysis:"
|
||||
echo " sudo iotop -oPa"
|
||||
echo " top"
|
||||
echo " systemd-analyze blame"
|
||||
}
|
||||
# shellcheck source=lib/arch_perf_probes.sh
|
||||
source "$SCRIPT_DIR/lib/arch_perf_probes.sh"
|
||||
# shellcheck source=lib/arch_perf_report.sh
|
||||
source "$SCRIPT_DIR/lib/arch_perf_report.sh"
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
166
linux_configuration/scripts/single_use/fixes/lib/arch_perf_probes.sh
Executable file
166
linux_configuration/scripts/single_use/fixes/lib/arch_perf_probes.sh
Executable file
@ -0,0 +1,166 @@
|
||||
#!/bin/bash
|
||||
# Individual diagnostic probes for the Arch performance report.
|
||||
#
|
||||
# Sourced by diagnose_arch_performance.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
collect_basics() {
|
||||
run_and_log "Kernel" uname -a
|
||||
run_and_log "Uptime" uptime
|
||||
run_and_log "Memory" free -h
|
||||
run_and_log "Swap" swapon --show
|
||||
run_and_log "CPU (lscpu)" lscpu
|
||||
run_and_log "Disk Usage" df -h /
|
||||
run_and_log "Boot Time" systemd-analyze
|
||||
run_and_log "Failed Units" systemctl --failed --no-pager
|
||||
run_and_log "Recent Errors (this boot)" journalctl -b -p err --no-pager -n 200
|
||||
|
||||
local cpu_count
|
||||
cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
|
||||
local load1
|
||||
load1=$(awk '{print int($1)}' /proc/loadavg 2>/dev/null || echo 0)
|
||||
if [[ ${load1:-0} -ge ${cpu_count:-1} ]]; then
|
||||
add_finding "1-minute load average is at/above CPU thread count (${load1}/${cpu_count}); background tasks may be saturating the system."
|
||||
fi
|
||||
|
||||
local failed_count
|
||||
failed_count=$(systemctl --failed --no-legend 2>/dev/null | wc -l || true)
|
||||
failed_count=${failed_count//[[:space:]]/}
|
||||
if [[ ${failed_count:-0} -gt 0 ]]; then
|
||||
add_finding "One or more systemd units are failed (${failed_count}); failed services can cause repeated retries and instability."
|
||||
fi
|
||||
|
||||
local acpi_error_count
|
||||
acpi_error_count=$(journalctl -b -p err --no-pager 2>/dev/null | grep -ic 'acpi' || true)
|
||||
if [[ ${acpi_error_count:-0} -ge 5 ]]; then
|
||||
add_finding "Frequent ACPI errors detected in current boot (${acpi_error_count}); BIOS/firmware update may improve stability."
|
||||
fi
|
||||
|
||||
local top_snapshot
|
||||
top_snapshot=$(ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head -n 12 || true)
|
||||
{
|
||||
echo
|
||||
echo "=== Top CPU Processes ==="
|
||||
echo "$top_snapshot"
|
||||
} >>"$REPORT_FILE"
|
||||
|
||||
local xorg_cpu
|
||||
xorg_cpu=$(ps -C Xorg -o %cpu= | awk '{sum+=$1} END {printf "%.0f", sum+0}' || echo 0)
|
||||
if [[ ${xorg_cpu:-0} -ge 20 ]]; then
|
||||
add_finding "Xorg is using high CPU (${xorg_cpu}%); desktop/compositor/GPU driver path may be a primary slowdown source."
|
||||
fi
|
||||
}
|
||||
|
||||
check_cpu_governor() {
|
||||
# An ARRAY, not a newline-joined string: the awk/grep calls below need one
|
||||
# argument per file. With the old unquoted "$gov_files" an empty result
|
||||
# also left awk with NO file operands, so it fell through to reading stdin
|
||||
# and the diagnostic hung instead of reporting 'not found'.
|
||||
local -a gov_files=()
|
||||
mapfile -t gov_files < <(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true)
|
||||
|
||||
if ((${#gov_files[@]} == 0)); then
|
||||
add_action "CPU governor files not found (may be unsupported on this platform)."
|
||||
return
|
||||
fi
|
||||
|
||||
local summary
|
||||
summary=$(awk '{count[$1]++} END {for (g in count) printf "%s:%d ", g, count[g]}' "${gov_files[@]}" 2>/dev/null || true)
|
||||
echo "CPU governor summary: ${summary:-unknown}" >>"$REPORT_FILE"
|
||||
|
||||
if grep -q '^powersave$' "${gov_files[@]}" 2>/dev/null; then
|
||||
add_finding "CPU governor includes 'powersave' on one or more cores; this can make high-end hardware feel slow."
|
||||
fi
|
||||
}
|
||||
|
||||
check_thermal_state() {
|
||||
if has_cmd sensors; then
|
||||
run_and_log "Temperatures (sensors)" sensors
|
||||
else
|
||||
add_action "Install lm_sensors and run 'sensors' to verify thermal throttling."
|
||||
fi
|
||||
|
||||
if has_cmd dmesg; then
|
||||
local therm_hits
|
||||
therm_hits=$(dmesg | grep -Ei 'throttl|thermal|overheat|cpu clock throttled' | tail -n 30 || true)
|
||||
if [[ -n $therm_hits ]]; then
|
||||
add_finding "Kernel logs show thermal/throttling related messages."
|
||||
{
|
||||
echo
|
||||
echo "=== Thermal/Throttling dmesg excerpts ==="
|
||||
echo "$therm_hits"
|
||||
} >>"$REPORT_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_power_services() {
|
||||
local tlp_enabled="false"
|
||||
local ppd_enabled="false"
|
||||
|
||||
if systemctl is-enabled tlp.service >/dev/null 2>&1; then
|
||||
tlp_enabled="true"
|
||||
fi
|
||||
if systemctl is-enabled power-profiles-daemon.service >/dev/null 2>&1; then
|
||||
ppd_enabled="true"
|
||||
fi
|
||||
|
||||
echo "Power services: tlp=${tlp_enabled}, power-profiles-daemon=${ppd_enabled}" >>"$REPORT_FILE"
|
||||
|
||||
if [[ $tlp_enabled == "true" && $ppd_enabled == "true" ]]; then
|
||||
add_finding "Both TLP and power-profiles-daemon are enabled; they often conflict and cause inconsistent performance."
|
||||
fi
|
||||
|
||||
if [[ $tlp_enabled == "false" && $ppd_enabled == "false" ]]; then
|
||||
add_action "No power management daemon is enabled; consider installing/enabling power-profiles-daemon for predictable AC/battery behavior."
|
||||
fi
|
||||
}
|
||||
|
||||
check_storage_health() {
|
||||
run_and_log "Block Devices" lsblk -o NAME,MODEL,ROTA,SIZE,TYPE,MOUNTPOINT,FSTYPE
|
||||
|
||||
if has_cmd fstrim; then
|
||||
run_and_log "fstrim dry-run" fstrim -av --dry-run
|
||||
fi
|
||||
|
||||
if systemctl is-enabled fstrim.timer >/dev/null 2>&1; then
|
||||
add_action "fstrim.timer is enabled (good for SSD performance longevity)."
|
||||
else
|
||||
add_finding "fstrim.timer is not enabled; SSD maintenance trimming may be missing."
|
||||
fi
|
||||
|
||||
if has_cmd smartctl; then
|
||||
local root_disk
|
||||
root_disk=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p$//' || true)
|
||||
if [[ -n ${root_disk:-} && -b $root_disk ]]; then
|
||||
run_and_log "SMART Summary ($root_disk)" smartctl -H "$root_disk"
|
||||
fi
|
||||
else
|
||||
add_action "Install smartmontools and run SMART health checks for your SSD/NVMe."
|
||||
fi
|
||||
}
|
||||
|
||||
check_memory_pressure() {
|
||||
local mem_total mem_available swap_total swap_free
|
||||
mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
|
||||
mem_available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
|
||||
swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo)
|
||||
swap_free=$(awk '/SwapFree/ {print $2}' /proc/meminfo)
|
||||
|
||||
if [[ ${swap_total:-0} -gt 0 ]]; then
|
||||
local swap_used
|
||||
swap_used=$((swap_total - swap_free))
|
||||
local swap_pct
|
||||
swap_pct=$((swap_used * 100 / swap_total))
|
||||
echo "Swap usage: ${swap_pct}%" >>"$REPORT_FILE"
|
||||
if [[ $swap_pct -ge 35 && ${mem_available:-0} -gt $((mem_total / 3)) ]]; then
|
||||
add_finding "High swap usage while RAM is still available; this can cause stutter."
|
||||
add_action "Consider lowering swappiness (temporary: sudo sysctl vm.swappiness=10)."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f /proc/pressure/memory ]]; then
|
||||
run_and_log "Memory PSI" cat /proc/pressure/memory
|
||||
fi
|
||||
}
|
||||
103
linux_configuration/scripts/single_use/fixes/lib/arch_perf_report.sh
Executable file
103
linux_configuration/scripts/single_use/fixes/lib/arch_perf_report.sh
Executable file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# GPU and journal probes, the safe-fix pass and the report summary.
|
||||
#
|
||||
# Sourced by diagnose_arch_performance.sh; split out to keep
|
||||
# arch_perf_probes.sh under the 250-line cap. Sourced rather than run, so
|
||||
# it inherits the caller's strict mode and the variables above the source.
|
||||
|
||||
check_gpu_state() {
|
||||
if has_cmd nvidia-smi; then
|
||||
run_and_log "NVIDIA State" nvidia-smi
|
||||
local pstate util power
|
||||
pstate=$(nvidia-smi --query-gpu=pstate --format=csv,noheader 2>/dev/null | head -n 1 | xargs || true)
|
||||
util=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true)
|
||||
power=$(nvidia-smi --query-gpu=power.draw --format=csv,noheader,nounits 2>/dev/null | head -n 1 | xargs || true)
|
||||
|
||||
{
|
||||
echo "NVIDIA pstate: ${pstate:-unknown}"
|
||||
echo "NVIDIA util: ${util:-unknown}%"
|
||||
echo "NVIDIA power: ${power:-unknown}W"
|
||||
} >>"$REPORT_FILE"
|
||||
|
||||
if [[ ${pstate:-} == "P0" && ${util:-100} -le 5 ]]; then
|
||||
add_finding "NVIDIA GPU is in P0 high-performance state while mostly idle; this can increase heat and trigger thermal limits."
|
||||
add_action "If laptop has hybrid graphics, prefer iGPU mode for desktop workloads and use dGPU on demand."
|
||||
fi
|
||||
else
|
||||
run_and_log "PCI VGA Devices" lspci -nnk | grep -A3 -Ei 'vga|3d|display'
|
||||
fi
|
||||
}
|
||||
|
||||
check_journal_size() {
|
||||
local journal_line
|
||||
journal_line=$(journalctl --disk-usage 2>/dev/null || true)
|
||||
echo "Journal usage: ${journal_line:-unknown}" >>"$REPORT_FILE"
|
||||
|
||||
if [[ $journal_line =~ ([0-9]+\.?[0-9]*)\ (G|M) ]]; then
|
||||
local value unit
|
||||
value="${BASH_REMATCH[1]}"
|
||||
unit="${BASH_REMATCH[2]}"
|
||||
if [[ $unit == "G" ]]; then
|
||||
add_finding "Systemd journal is large (${value}G); excessive logs can waste I/O and disk space."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
apply_safe_fixes() {
|
||||
if [[ $APPLY_SAFE_FIXES != "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Applying safe fixes..."
|
||||
|
||||
if ! systemctl is-enabled fstrim.timer >/dev/null 2>&1; then
|
||||
systemctl enable --now fstrim.timer
|
||||
add_action "Enabled and started fstrim.timer."
|
||||
fi
|
||||
|
||||
if systemctl is-enabled tlp.service >/dev/null 2>&1 && systemctl is-enabled power-profiles-daemon.service >/dev/null 2>&1; then
|
||||
systemctl disable --now tlp.service
|
||||
add_action "Disabled tlp.service to avoid conflict with power-profiles-daemon."
|
||||
fi
|
||||
|
||||
local journal_line
|
||||
journal_line=$(journalctl --disk-usage 2>/dev/null || true)
|
||||
if [[ $journal_line =~ ([0-9]+\.?[0-9]*)\ G ]]; then
|
||||
journalctl --vacuum-size=300M
|
||||
add_action "Vacuumed systemd journal to 300M."
|
||||
fi
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo
|
||||
echo "=============================="
|
||||
echo " Arch Performance Diagnostics"
|
||||
echo "=============================="
|
||||
echo "Report: $REPORT_FILE"
|
||||
echo
|
||||
|
||||
if [[ ${#FINDINGS[@]} -eq 0 ]]; then
|
||||
log_ok "No high-confidence bottlenecks detected by automated checks."
|
||||
else
|
||||
log_warn "Likely issues found (${#FINDINGS[@]}):"
|
||||
local item
|
||||
for item in "${FINDINGS[@]}"; do
|
||||
echo " - $item"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#ACTIONS[@]} -gt 0 ]]; then
|
||||
echo
|
||||
log_info "Actions/recommendations:"
|
||||
local action
|
||||
for action in "${ACTIONS[@]}"; do
|
||||
echo " - $action"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Recommended next command for deep per-process analysis:"
|
||||
echo " sudo iotop -oPa"
|
||||
echo " top"
|
||||
echo " systemd-analyze blame"
|
||||
}
|
||||
215
linux_configuration/scripts/single_use/utils/lib/repo_study_steps.sh
Executable file
215
linux_configuration/scripts/single_use/utils/lib/repo_study_steps.sh
Executable file
@ -0,0 +1,215 @@
|
||||
#!/bin/bash
|
||||
# Dependency checks and the per-repo study-material steps.
|
||||
#
|
||||
# Sourced by repo_to_study.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
#==============================================================================
|
||||
# Check Dependencies
|
||||
#==============================================================================
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
|
||||
# Check for required scripts
|
||||
if [ ! -x "$ANALYZE_SCRIPT" ]; then
|
||||
missing+=("analyze_repo.sh not found at $ANALYZE_SCRIPT")
|
||||
fi
|
||||
|
||||
if [ ! -x "$STUDY_SCRIPT" ]; then
|
||||
missing+=("generate_study_materials.sh not found at $STUDY_SCRIPT")
|
||||
fi
|
||||
|
||||
# Check for basic tools
|
||||
for cmd in git curl grep sed awk; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
print_error "Missing dependencies:"
|
||||
for dep in "${missing[@]}"; do
|
||||
echo " - $dep"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Ensure Offline Docs are Available
|
||||
#==============================================================================
|
||||
ensure_offline_docs() {
|
||||
local docs_dir="$HOME/.local/share/offline-docs"
|
||||
|
||||
if [ ! -d "$docs_dir/python" ]; then
|
||||
print_info "Offline docs not found. Setting up Python documentation..."
|
||||
if [ -x "$SETUP_DOCS_SCRIPT" ]; then
|
||||
"$SETUP_DOCS_SCRIPT" --python
|
||||
else
|
||||
print_info "Run setup_offline_docs.sh --all to enable offline documentation"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Global to store repo name for cloned repos
|
||||
REPO_NAME=""
|
||||
|
||||
#==============================================================================
|
||||
# Get Repository
|
||||
#==============================================================================
|
||||
get_repo() {
|
||||
local input="$1"
|
||||
local repo_dir=""
|
||||
|
||||
# Check if it's a URL (git clone needed)
|
||||
if [[ $input =~ ^https?:// ]] || [[ $input =~ ^git@ ]]; then
|
||||
print_step "Cloning repository..."
|
||||
|
||||
# Extract repo name from URL
|
||||
REPO_NAME=$(basename "$input" .git)
|
||||
repo_dir="$WORK_DIR/$REPO_NAME"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then
|
||||
print_success "Cloned: $input"
|
||||
else
|
||||
print_error "Failed to clone repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$repo_dir"
|
||||
# Local path
|
||||
elif [ -d "$input" ]; then
|
||||
# Convert to absolute path
|
||||
repo_dir="$(cd "$input" && pwd)"
|
||||
REPO_NAME=$(basename "$repo_dir")
|
||||
print_success "Using local repository: $repo_dir"
|
||||
echo "$repo_dir"
|
||||
else
|
||||
print_error "Invalid input: '$input' is not a valid URL or directory"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Analyze Repository
|
||||
#==============================================================================
|
||||
analyze_repo() {
|
||||
local repo_path="$1"
|
||||
local repo_name="$REPO_NAME"
|
||||
[ -z "$repo_name" ] && repo_name=$(basename "$repo_path")
|
||||
|
||||
print_step "Analyzing repository..."
|
||||
|
||||
# Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/)
|
||||
"$ANALYZE_SCRIPT" "$repo_path" >&2 || true
|
||||
|
||||
# Find the results directory
|
||||
local results_dir="/tmp/repo_analysis/results_${repo_name}"
|
||||
if [ ! -d "$results_dir" ]; then
|
||||
# Try without prefix
|
||||
results_dir="/tmp/repo_analysis/results"
|
||||
fi
|
||||
|
||||
if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then
|
||||
print_error "Could not find analysis results at $results_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Analysis complete: $results_dir"
|
||||
echo "$results_dir"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Generate Study Materials
|
||||
#==============================================================================
|
||||
generate_materials() {
|
||||
local analysis_dir="$1"
|
||||
local output_dir="$2"
|
||||
|
||||
print_step "Generating study materials with offline documentation..."
|
||||
|
||||
# Run study materials generator
|
||||
cd "$analysis_dir" || { print_error "Cannot enter $analysis_dir"; return 1; }
|
||||
if "$STUDY_SCRIPT" . 2>/dev/null | grep -E "^(Created|✓|Files created)" | head -5; then
|
||||
print_success "Study materials generated"
|
||||
else
|
||||
# Try anyway, might have succeeded
|
||||
true
|
||||
fi
|
||||
|
||||
# Create output directory and copy results
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Copy generated files
|
||||
[ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/"
|
||||
[ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/"
|
||||
[ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/"
|
||||
|
||||
# Copy analysis data
|
||||
mkdir -p "$output_dir/analysis"
|
||||
[ -d "per_language" ] && cp -r "per_language" "$output_dir/analysis/"
|
||||
[ -f "grep_imports.txt" ] && cp "grep_imports.txt" "$output_dir/analysis/"
|
||||
[ -f "grep_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/"
|
||||
[ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/"
|
||||
|
||||
print_success "Files saved to: $output_dir"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Show Summary
|
||||
#==============================================================================
|
||||
show_summary() {
|
||||
local output_dir="$1"
|
||||
|
||||
print_header "Study Materials Ready!"
|
||||
|
||||
echo -e "${BOLD}Output directory:${NC} $output_dir"
|
||||
echo ""
|
||||
echo -e "${BOLD}Generated files:${NC}"
|
||||
|
||||
if [ -f "$output_dir/documentation_links.md" ]; then
|
||||
local doc_lines
|
||||
doc_lines=$(wc -l <"$output_dir/documentation_links.md")
|
||||
echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)"
|
||||
echo " Contains links to OFFLINE documentation"
|
||||
fi
|
||||
|
||||
if [ -f "$output_dir/anki_cards.txt" ]; then
|
||||
local card_count
|
||||
card_count=$(grep -c $'^\w' "$output_dir/anki_cards.txt" 2>/dev/null || echo "0")
|
||||
echo -e " 🎴 ${GREEN}anki_cards.txt${NC} (~$card_count cards)"
|
||||
echo " Import to Anki: File → Import → Tab separated"
|
||||
fi
|
||||
|
||||
if [ -f "$output_dir/llm_anki_prompt.md" ]; then
|
||||
echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}"
|
||||
echo " Use with ChatGPT/Claude to generate more cards"
|
||||
fi
|
||||
|
||||
if [ -d "$output_dir/analysis" ]; then
|
||||
echo -e " 📊 ${GREEN}analysis/${NC}"
|
||||
echo " Raw analysis data (imports, keywords, functions per language)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Quick preview of imports with offline docs:${NC}"
|
||||
if [ -f "$output_dir/documentation_links.md" ]; then
|
||||
grep -A20 "import/from" "$output_dir/documentation_links.md" 2>/dev/null |
|
||||
grep "^\| \`" | head -5 |
|
||||
sed 's/|/│/g'
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Next steps:${NC}"
|
||||
echo " 1. Open documentation_links.md to browse offline docs"
|
||||
echo " 2. Import anki_cards.txt into Anki for spaced repetition"
|
||||
echo " 3. Use llm_anki_prompt.md to generate more targeted cards"
|
||||
echo ""
|
||||
echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Main
|
||||
#==============================================================================
|
||||
@ -103,214 +103,9 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Check Dependencies
|
||||
#==============================================================================
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
# shellcheck source=lib/repo_study_steps.sh
|
||||
source "$SCRIPT_DIR/lib/repo_study_steps.sh"
|
||||
|
||||
# Check for required scripts
|
||||
if [ ! -x "$ANALYZE_SCRIPT" ]; then
|
||||
missing+=("analyze_repo.sh not found at $ANALYZE_SCRIPT")
|
||||
fi
|
||||
|
||||
if [ ! -x "$STUDY_SCRIPT" ]; then
|
||||
missing+=("generate_study_materials.sh not found at $STUDY_SCRIPT")
|
||||
fi
|
||||
|
||||
# Check for basic tools
|
||||
for cmd in git curl grep sed awk; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
print_error "Missing dependencies:"
|
||||
for dep in "${missing[@]}"; do
|
||||
echo " - $dep"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Ensure Offline Docs are Available
|
||||
#==============================================================================
|
||||
ensure_offline_docs() {
|
||||
local docs_dir="$HOME/.local/share/offline-docs"
|
||||
|
||||
if [ ! -d "$docs_dir/python" ]; then
|
||||
print_info "Offline docs not found. Setting up Python documentation..."
|
||||
if [ -x "$SETUP_DOCS_SCRIPT" ]; then
|
||||
"$SETUP_DOCS_SCRIPT" --python
|
||||
else
|
||||
print_info "Run setup_offline_docs.sh --all to enable offline documentation"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Global to store repo name for cloned repos
|
||||
REPO_NAME=""
|
||||
|
||||
#==============================================================================
|
||||
# Get Repository
|
||||
#==============================================================================
|
||||
get_repo() {
|
||||
local input="$1"
|
||||
local repo_dir=""
|
||||
|
||||
# Check if it's a URL (git clone needed)
|
||||
if [[ $input =~ ^https?:// ]] || [[ $input =~ ^git@ ]]; then
|
||||
print_step "Cloning repository..."
|
||||
|
||||
# Extract repo name from URL
|
||||
REPO_NAME=$(basename "$input" .git)
|
||||
repo_dir="$WORK_DIR/$REPO_NAME"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
if git clone --depth 1 "$input" "$repo_dir" >&2 2>&1; then
|
||||
print_success "Cloned: $input"
|
||||
else
|
||||
print_error "Failed to clone repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$repo_dir"
|
||||
# Local path
|
||||
elif [ -d "$input" ]; then
|
||||
# Convert to absolute path
|
||||
repo_dir="$(cd "$input" && pwd)"
|
||||
REPO_NAME=$(basename "$repo_dir")
|
||||
print_success "Using local repository: $repo_dir"
|
||||
echo "$repo_dir"
|
||||
else
|
||||
print_error "Invalid input: '$input' is not a valid URL or directory"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Analyze Repository
|
||||
#==============================================================================
|
||||
analyze_repo() {
|
||||
local repo_path="$1"
|
||||
local repo_name="$REPO_NAME"
|
||||
[ -z "$repo_name" ] && repo_name=$(basename "$repo_path")
|
||||
|
||||
print_step "Analyzing repository..."
|
||||
|
||||
# Run the analyzer (it outputs to stderr/stdout, results go to /tmp/repo_analysis/)
|
||||
"$ANALYZE_SCRIPT" "$repo_path" >&2 || true
|
||||
|
||||
# Find the results directory
|
||||
local results_dir="/tmp/repo_analysis/results_${repo_name}"
|
||||
if [ ! -d "$results_dir" ]; then
|
||||
# Try without prefix
|
||||
results_dir="/tmp/repo_analysis/results"
|
||||
fi
|
||||
|
||||
if [ ! -d "$results_dir" ] || [ ! -d "$results_dir/per_language" ]; then
|
||||
print_error "Could not find analysis results at $results_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Analysis complete: $results_dir"
|
||||
echo "$results_dir"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Generate Study Materials
|
||||
#==============================================================================
|
||||
generate_materials() {
|
||||
local analysis_dir="$1"
|
||||
local output_dir="$2"
|
||||
|
||||
print_step "Generating study materials with offline documentation..."
|
||||
|
||||
# Run study materials generator
|
||||
cd "$analysis_dir"
|
||||
if "$STUDY_SCRIPT" . 2>/dev/null | grep -E "^(Created|✓|Files created)" | head -5; then
|
||||
print_success "Study materials generated"
|
||||
else
|
||||
# Try anyway, might have succeeded
|
||||
true
|
||||
fi
|
||||
|
||||
# Create output directory and copy results
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Copy generated files
|
||||
[ -f "documentation_links.md" ] && cp "documentation_links.md" "$output_dir/"
|
||||
[ -f "anki_cards.txt" ] && cp "anki_cards.txt" "$output_dir/"
|
||||
[ -f "llm_anki_prompt.md" ] && cp "llm_anki_prompt.md" "$output_dir/"
|
||||
|
||||
# Copy analysis data
|
||||
mkdir -p "$output_dir/analysis"
|
||||
[ -d "per_language" ] && cp -r "per_language" "$output_dir/analysis/"
|
||||
[ -f "grep_imports.txt" ] && cp "grep_imports.txt" "$output_dir/analysis/"
|
||||
[ -f "grep_keywords.txt" ] && cp "grep_keywords.txt" "$output_dir/analysis/"
|
||||
[ -f "grep_function_calls.txt" ] && cp "grep_function_calls.txt" "$output_dir/analysis/"
|
||||
|
||||
print_success "Files saved to: $output_dir"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Show Summary
|
||||
#==============================================================================
|
||||
show_summary() {
|
||||
local output_dir="$1"
|
||||
|
||||
print_header "Study Materials Ready!"
|
||||
|
||||
echo -e "${BOLD}Output directory:${NC} $output_dir"
|
||||
echo ""
|
||||
echo -e "${BOLD}Generated files:${NC}"
|
||||
|
||||
if [ -f "$output_dir/documentation_links.md" ]; then
|
||||
local doc_lines
|
||||
doc_lines=$(wc -l <"$output_dir/documentation_links.md")
|
||||
echo -e " 📚 ${GREEN}documentation_links.md${NC} ($doc_lines lines)"
|
||||
echo " Contains links to OFFLINE documentation"
|
||||
fi
|
||||
|
||||
if [ -f "$output_dir/anki_cards.txt" ]; then
|
||||
local card_count
|
||||
card_count=$(grep -c $'^\w' "$output_dir/anki_cards.txt" 2>/dev/null || echo "0")
|
||||
echo -e " 🎴 ${GREEN}anki_cards.txt${NC} (~$card_count cards)"
|
||||
echo " Import to Anki: File → Import → Tab separated"
|
||||
fi
|
||||
|
||||
if [ -f "$output_dir/llm_anki_prompt.md" ]; then
|
||||
echo -e " 🤖 ${GREEN}llm_anki_prompt.md${NC}"
|
||||
echo " Use with ChatGPT/Claude to generate more cards"
|
||||
fi
|
||||
|
||||
if [ -d "$output_dir/analysis" ]; then
|
||||
echo -e " 📊 ${GREEN}analysis/${NC}"
|
||||
echo " Raw analysis data (imports, keywords, functions per language)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Quick preview of imports with offline docs:${NC}"
|
||||
if [ -f "$output_dir/documentation_links.md" ]; then
|
||||
grep -A20 "import/from" "$output_dir/documentation_links.md" 2>/dev/null |
|
||||
grep "^\| \`" | head -5 |
|
||||
sed 's/|/│/g'
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Next steps:${NC}"
|
||||
echo " 1. Open documentation_links.md to browse offline docs"
|
||||
echo " 2. Import anki_cards.txt into Anki for spaced repetition"
|
||||
echo " 3. Use llm_anki_prompt.md to generate more targeted cards"
|
||||
echo ""
|
||||
echo -e "${CYAN}To view a doc:${NC} xdg-open 'file:///path/from/documentation_links.md'"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Main
|
||||
#==============================================================================
|
||||
main() {
|
||||
# Handle help
|
||||
if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
|
||||
Loading…
Reference in New Issue
Block a user