Del 1: Fra lokal LLM til et wi-fi-sundhedstjek på Cisco Catalyst 9800

Det begyndte egentlig med et ret enkelt spørgsmål: Kan jeg få en lokal AI-agent til at hente data fra en Cisco Catalyst 9800, analysere dem og hjælpe mig med at lave et brugbart wi-fi-sundhedstjek? Ikke ved at lade en sprogmodel gætte på controllerens konfiguration, men ved at kombinere klassisk netværksautomation med en lokal LLM.

Det er nemt nok at spørge en chatbot om en konfiguration. Der er noget længere til en agent, der selv kan indsamle de rigtige data, strukturere dem og sætte dem ind i en faglig sammenhæng. Og det er netop det, jeg er begyndt at udforske. Denne første del handler om at få fundamentet på plads.

Målet er ikke at bygge en AI, der får frie hænder til at konfigurere et produktionsnetværk. Tværtimod. Jeg vil undersøge, om jeg kan bygge en lokal platform til wi-fi-sundhedstjek, hvor almindelig kode håndterer den deterministiske del, mens en lokal LLM bruges til analyse, kontekst og formidling.

I første omgang er ambitionen beskeden: Log ind på én Catalyst 9800 WLC, indsaml relevante konfigurations- og driftsdata, strukturer dem og lad en lokal AI-agent hjælpe med at analysere resultatet. Det lyder måske enkelt, men der er en del byggeklodser, der skal være på plads, før den første SSH-kommando bliver sendt til controlleren.

 

Hvorfor overhovedet bygge det selv

Cisco har allerede værktøjer til automation, assurance og AI. Det samme gælder andre producenter. Så hvorfor bruge tid på at bygge noget selv? For mig handler det om tre ting: læring, kontrol over data og automatisering af standardiserede opgaver.

Jeg arbejder professionelt med wi-fi og netværksdesign, men AI-agenter, lokale LLM’er, MCP, tool calling og moderne AI-orchestration er stadig nye discipliner for mig. Jeg vil ikke nøjes med at bruge værktøjerne. Jeg vil forstå, hvordan de hænger sammen, og hvor grænsen går mellem almindelig kode og noget, en sprogmodel faktisk er god til.

Kontrol over data betyder også noget. En wireless controller kan indeholde information om netværksdesign, SSID’er, sikkerhedskonfiguration, IP-adresser, accesspunkter og klienter. Det er ikke nødvendigvis oplysninger, man uden videre ønsker at sende til en ekstern cloud-baseret LLM. Derfor er det interessant at se, hvor langt jeg kan komme med en model, der kører lokalt.

Og så er der alt det kedelige arbejde. Selvfølgelig er ALT ved wi-fi sjovt, givende og spændende – men et sundhedstjek består nu engang af mange gentagelige kontroller, for eksempel:

  • Hvilken softwareversion kører controlleren?
  • Hvordan er RF-profilerne konfigureret?
  • Hvilke channel widths bruges?
  • Hvordan er transmit power begrænset?
  • Hvilke data rates er konfigureret?
  • Hvordan er WLAN og policy profiles sat op?
  • Hvilke sikkerhedsprotokoller anvendes?
  • Er Fast Transition aktiveret?
  • Hvordan ser accesspunkt- og radioinventaret ud?

Det er præcis den type arbejde, computere er gode til. Pointen er ikke at få en LLM til at gætte sig frem til controllerens konfiguration. Pointen er at give modellen korrekte, strukturerede data og først derefter bruge den dér, hvor en sprogmodel kan tilføre noget.

 

Arkitekturen

Den langsigtede idé ser nogenlunde sådan ud:

Det er en vigtig designbeslutning, at LLM’en ikke skal finde ud af, hvordan oplysningerne hentes fra controlleren. Det skal almindelig kode. Hvis jeg vil vide, hvilken softwareversion en Catalyst 9800 kører, skal en Python-funktion hente informationen og returnere et veldefineret resultat. AI’en kan bagefter hjælpe med at vurdere resultatet i en større sammenhæng.

Det gør løsningen mere reproducerbar og reducerer risikoen for, at modellen opfinder oplysninger. Collection, parsing og faste regler hører efter min mening hjemme i deterministisk kode. LLM’en skal forklare, prioritere og formulere resultaterne, ikke definere, hvad der er sandt.

 

Agenten er ikke modellen

Til agentlaget har jeg valgt at eksperimentere med OpenMonoAgent. OpenMono er et open source, local-first coding-agent framework baseret på .NET. Det kan bruge en lokal llama.cpp-inference-server og understøtter blandt andet tools, permission checks, Docker-sandboxing, MCP og integration direkte i VS Code.

Her er det vigtigt at skelne mellem agenten og LLM’en. OpenMono er ikke modellen. I mit setup ser kæden sådan ud:

OpenMono står for orchestration: samtalen, tools, filadgang, permissions og senere forbindelsen til mine egne wi-fi-funktioner. Qwen-modellen er reasoning- og sprogmotoren.

 

Mit første testmiljø

Mit første testmiljø er bevidst ikke bygget omkring en stor GPU-server. Jeg bruger en MacBook Pro med Apple M2 Pro, 32 GB unified memory og macOS. På tidspunktet for installationen valgte OpenMono automatisk modellen Qwen3.5-9B-Q4_K_M, som kører gennem native llama.cpp med Apple Metal-acceleration. Inference-serveren lytter lokalt på localhost:7474.

Jeg installerede også vision. Det gør det muligt for modellen at analysere billeder ud over almindelig tekst, og jeg ved endnu ikke, om projektet senere skal arbejde med screenshots fra en WLC, netværksdiagrammer eller andre visuelle wi-fi-data. Vision koster lidt ekstra hukommelse. I mit setup blev context window derfor reduceret fra 64k til 49.152 tokens, altså cirka 48k, for at give plads til vision encoderens hukommelsesforbrug.

OpenMono understøtter Apple Silicon direkte og kan selv konfigurere lokal inference til den hardware, det finder. Da jeg satte miljøet op, anbefalede projektet kraftigere Apple Silicon-systemer med mindst 64 GB unified memory til de større modeller, mens mindre modeller kunne bruges på maskiner med mindre hukommelse. Projektet udvikler sig hurtigt, så den model, installeren vælger i dag, kan sagtens være en anden end min.

M2-maskinen er derfor ikke nødvendigvis den endelige inference-platform. Den er min udviklingsplatform, og den er rigelig til at afprøve arkitekturen. En fordel ved Apple Silicon er kombinationen af en forholdsvis kraftig integreret GPU og unified memory. Jeg får en maskine, der kan køre en lokal LLM, men stadig er bærbar og ikke bruger strøm som en større GPU-server.

Jeg bruger hverken Windows eller Linux i denne del af projektet. Arkitekturen kan uden problemer overføres til Linux, men den konkrete opsætning bliver anderledes, især omkring inference og GPU-acceleration. På Mac bruger OpenMono Apple Silicon og Metal; på Linux vil det typisk være en NVIDIA-GPU eller CPU.

Opsætningen på Windows og Linux må læseren derfor selv stykke sammen. Jeg har ganske enkelt ikke tid eller overskud til at vedligeholde flere forskellige setups, men OpenMonos egen dokumentation er et godt udgangspunkt.

 

Installation af OpenMono

Den officielle installation kan startes med én kommando:

bash <(curl -fsSL https://raw.githubusercontent.com/StartupHakk/OpenMonoAgent.ai/refs/heads/main/get-openmono.sh)

Installeren detekterer hardwaren og installerer de nødvendige komponenter til agent og inference. Jeg valgte en single-box-installation, hvor både agent, llama.cpp, Qwen og Metal-accelerationen kører på den samme MacBook:

MacBook
|
+– OpenMono Agent
|
+– llama.cpp
|
+– Qwen
|
+– Metal

Efter installationen kontrollerede jeg inference-serveren med openmono status. I mit tilfælde viste den blandt andet:

Status        : Running
Port          : 7474
Model         : Qwen3.5-9B-Q4_K_M.gguf
Context Size  : 49152
Vision        : enabled
Apple Silicon : Apple M2 Pro (32 GB)

Selve agenten startes med openmono agent. Jeg går ikke gennem hele installationen trin for trin her; fokus er på arkitekturen og de valg, jeg har truffet undervejs – ikke på en detaljeret installationsvejledning.

 

Fra chatbot til agent

En af de første tests var helt enkel: Jeg bad OpenMono om at oprette en tekstfil. Filen var ligegyldig. Det relevante var, at agenten først spurgte om lov til at skrive den. Dermed kunne jeg se hele kæden fra prompt til tool-kald og brugerens godkendelse:

Prompt
|
v
LLM vælger et tool
|
v
Permission check
|
v
Brugergodkendelse
|
v
Tool execution

OpenMono arbejder med sandboxing og permission gates omkring operationer, der kan ændre filer eller systemtilstand. Det bliver særdeles vigtigt senere, for den første version af mit      wi-     fi-sundhedstjek skal være read-only.

Visse operationer skal være tilladt:

show version         ALLOW
show wlan …        ALLOW
show wireless …    ALLOW
REST API GET         ALLOW

Andre skal være afvist:

configure terminal   DENY
REST POST            DENY
REST PATCH           DENY
REST DELETE          DENY

Det må ikke bare stå som et ønske i en prompt. De skrivende operationer skal være teknisk utilgængelige for agenten.

 

Udviklingsmiljøet

Selve wi-fi-delen skal skrives i Python. Jeg bruger et forholdsvis klassisk macOS-setup med Homebrew, Python 3, Zsh og VS Code. I VS Code har jeg Python-udvidelsen med Pylance og OpenMonos egen extension. Projektfilerne ligger under følgende mappe i Dropbox:

~/Library/CloudStorage/Dropbox/Projects/

Dropbox synkroniserer kode, dokumentation, regler og de øvrige projektfiler. Hele scriptet og en kort brugsvejledning findes i appendikset. Jeg bruger et lille Zsh-script, mkpyproj, som opretter nye Python-projekter med den samme grundstruktur hver gang:

wifi-healthcheck-alpha/
+– .venv/
+– .vscode/
|   +– settings.json
+– src/
|   +– main.py
+– .gitignore
+– requirements.txt

Det virtuelle miljø oprettes med:

python3 -m venv .venv

VS Code er konfigureret til automatisk at bruge projektets interpreter:

{
“python.defaultInterpreterPath”: “${workspaceFolder}/.venv/bin/python”,
“python.terminal.activateEnvironment”: true,
“python.analysis.extraPaths”: [“${workspaceFolder}/src”]
}

Jeg har også lidt Zsh-logik, der automatisk aktiverer .venv, når jeg går ind i projektmappen, og deaktiverer miljøet igen, når jeg forlader den. En almindelig cd wifi-healthcheck-alpha giver derfor automatisk prompten (.venv), uden at jeg manuelt skal køre source .venv/bin/activate.

 

Dropbox uden hele Python-miljøet

Der er en lille udfordring ved at lægge Python-projekter i Dropbox: Et virtuelt miljø kan indeholde tusindvis af filer, og der er ingen grund til at synkronisere dem. Det interessante er kildekoden, requirements.txt, dokumentationen, konfigurationen og testene. Selve .venv kan altid genskabes.

Derfor bliver projektet synkroniseret, mens .venv markeres som lokal med macOS File Provider:

xattr -w ‘com.apple.fileprovider.ignore#P’ 1 .venv

Dropbox dokumenterer denne attribut til filer og mapper, der ikke skal synkroniseres på macOS med File Provider. Resultatet er, at resten af projektet fortsat synkroniseres, mens .venv kun findes lokalt:

wifi-healthcheck-alpha/
+– src/                -> Dropbox
+– .vscode/            -> Dropbox
+– requirements.txt    -> Dropbox
+– documentation/      -> Dropbox
+– .venv/              -> kun lokal

 

AI direkte i VS Code

OpenMono har også en VS Code-extension, så agenten kan bruges direkte fra editoren. Den understøtter blandt andet agent-chat, filændringer, command execution og permission prompts og kommunikerer med den lokale OpenMono-agent.

VS Code
|
+– Python
+– Pylance
+– Terminal
+– OpenMono Agent
|
v
Lokal Qwen LLM

Jeg har allerede verificeret, at agenten kan oprette og læse Python-filer fra editoren. Men her har jeg sat endnu en bevidst begrænsning: Jeg vil ikke bruge AI’en til automatisk at skrive hele projektet. Formålet er netop at lære Python, agentarkitektur og automation.

AI’en skal primært fungere som pair programmer. Jeg vil for eksempel kunne bede den om at forklare en funktion, fortælle hvorfor noget ikke virker, foreslå en egnet datastruktur eller give hints til en fejl uden selv at rette den. Det bliver langsommere. Det er også meningen. Til gengæld lærer jeg noget undervejs, og jeg behøver ikke forstyrre mine kolleger, som er meget dygtigere end mig til programmering, med alle mine n00b-spørgsmål. AI’en må være min første line support.

 

Hvor langt er projektet

På nuværende tidspunkt er fundamentet på plads:

Komponent Status
Local LLM DONE
OpenMono DONE
Agent tools DONE
Permission test DONE
VS Code-integration DONE
Python-miljø DONE
Dropbox-synkronisering DONE

Den næste komponent er den første rigtige del af      wi-fi-systemet: En Python-collector, der forbinder til en Cisco Catalyst 9800. Den første version bliver med vilje meget lille: én forbindelse, én controller og én kommando – eksempelvis show version.

Når det fungerer, begynder jeg gradvist at strukturere resultatet, hente flere data og bygge den rules engine, der senere skal sammenholde controllerens konfiguration med definerede wi-fi-best practices. Først derefter bliver AI-delen for alvor interessant.

Når en lokal agent har adgang til både strukturerede controllerdata og mine egne wi-fi-regler, kan jeg nemlig stille et mere nyttigt spørgsmål end: “Kan en LLM noget med wi-fi?” Spørgsmålet bliver i stedet, om klassisk netværksautomation, wi-fi-faglighed og lokale AI-agenter kan kombineres til et værktøj, der faktisk gør noget nyttigt.

Det er det, jeg vil forsøge at finde ud af. Fortsættelse følger.

Fortsættelsen er del 2: Et virtuelt WLC-lab fra bunden.

Publiceringsmetadata

Meta title: AI-agent til Wi-Fi-sundhedstjek på Cisco 9800 | Wingmen

Meta description: Følg arbejdet med en lokal AI-agent til Wi-Fi-sundhedstjek på Cisco Catalyst

9800. Python, lokal LLM og read-only adgang danner grundlaget for projektet.

 

Appendiks

Mit script til nye Pythonprojekter

Her er hele mkpyproj-scriptet, hvis du vil bruge samme projektstruktur. Det opretter et virtuelt Python-miljø, en lille main.py, VS Code-indstillinger, .gitignore og en tom requirements.txt. Jeg har samlet vejledningen her, så du kan komme i gang uden at hente noget fra et repository.

Du skal bruge macOS med Zsh og Python 3 tilgængelig i din PATH. VS Code er valgfrit. Scriptet installerer hverken Python eller VS Code og kræver ikke Git.

 

Gem og kør scriptet

Opret en mappe til scriptet, og åbn filen i terminaleditoren nano:

mkdir -p “$HOME/bin”
nano “$HOME/bin/mkpyproj”

Kopiér hele koden fra næste side, fra #!/usr/bin/env zsh til den sidste EOF, og indsæt den i nano. Gem med Ctrl+O, tryk Enter, og afslut med Ctrl+X. Filen skal være almindelig tekst uden .txt-endelse. Koden i dokumentet er tekst, ikke et billede; lange linjer kan være visuelt ombrudt.

Gør scriptet eksekverbart, og kontrollér syntaksen:

chmod +x “$HOME/bin/mkpyproj”
zsh -n “$HOME/bin/mkpyproj”

Hvis syntakskontrollen ikke skriver noget, er syntaksen accepteret. Opret derefter et projekt. Her vælger vi mappen eksplicit, så eksemplet også virker uden Dropbox:

“$HOME/bin/mkpyproj” –base “$HOME/Projects” \
wifi-healthcheck-alpha

Aktivér miljøet, og prøv den genererede Python-fil:

cd “$HOME/Projects/wifi-healthcheck-alpha”
source .venv/bin/activate
python src/main.py

Du bør få svaret “Hej, verden!”. Når du er færdig, kan du forlade miljøet med deactivate. Min automatiske aktivering ved cd ligger separat i .zshrc; den er ikke en del af dette script. Hvis ~/bin allerede er i din PATH, kan du skrive mkpyproj uden den fulde sti.

 

Placering og eksisterende filer

Uden –base bruger scriptet først MKPYPROJ_BASE, hvis variablen er sat. Ellers vælger det den eksisterende mappe ~/Library/CloudStorage/Dropbox/Projects og falder tilbage til ~/Projects. Brug –no-open, hvis VS Code ikke skal åbnes, og –help for at se mulighederne.

Eksisterende projektfiler bevares som udgangspunkt. –force overskriver src/main.py, .vscode/settings.json og .gitignore. Det overskriver hverken requirements.txt eller et eksisterende virtuelt miljø.

Dropbox-undtagelsen for .venv gælder kun under ~/Library/CloudStorage/Dropbox/ på macOS med File Provider. En anden sti valgt med –base bliver ikke automatisk genkendt som Dropbox. Kontrollér synkroniseringsstatus i Dropbox; .gitignore påvirker kun Git. Hvis attributten ikke kan sættes, skriver scriptet en advarsel og fortsætter.

 

Hele scriptet

#!/usr/bin/env zsh
# Til macOS med Zsh og Python 3. VS Code er valgfrit.
set -euo pipefail

usage() {
cat <<‘EOF’
mkpyproj – Opret et nyt Python-projekt med .venv, .vscode og mere.

Brug:
mkpyproj [options] <projektnavn>

Options:
-b, –base <dir>   Basis-mappe til projekter (default auto-detekteret).
Rækkefølge for auto-detect:
1) $MKPYPROJ_BASE (hvis sat)
2) ~/Library/CloudStorage/Dropbox/Projects
3) ~/Projects
–no-open          Åbn ikke i VS Code automatisk.
-f, –force        Overskriv eksisterende filer (.vscode/settings.json, main.py, .gitignore).
-h, –help         Vis hjælp.

Eksempel:
mkpyproj DNAC
mkpyproj -b “$HOME/Library/CloudStorage/Dropbox/Projects” AP

Dropbox:
Automatisk undtagelse af .venv gælder kun projekter under
~/Library/CloudStorage/Dropbox/ på macOS med File Provider.
Andre Dropbox-placeringer skal håndteres separat; –base ændrer
projektplaceringen, men ikke Dropbox-genkendelsen.
.gitignore styrer Git, ikke cloud-synkronisering.

Miljøet aktiveres manuelt med source .venv/bin/activate.
Automatisk aktivering ved cd kræver separat opsætning i din .zshrc.
EOF
}

# — Parse args —
BASE=”${MKPYPROJ_BASE:-}”
OPEN_VSCODE=1
FORCE=0
NAME=””

while [[ $# -gt 0 ]]; do
case “$1” in
-b|–base)
[[ $# -lt 2 ]] && { echo “Manglende argument til $1” >&2; usage; exit 2; }
[[ -z “$2” || “$2” == -* ]] && { echo “Ugyldig basis-mappe: $2″ >&2; exit 2; }
BASE=”$2”; shift 2;;
–no-open)
OPEN_VSCODE=0; shift;;
-f|–force)
FORCE=1; shift;;
-h|–help)
usage; exit 0;;
-*)
echo “Ukendt option: $1” >&2; usage; exit 2;;
*)
if [[ -z “$NAME” ]]; then
NAME=”$1″; shift
else
echo “Ukendt argument: $1” >&2; usage; exit 2
fi
;;
esac
done

[[ -z “$NAME” ]] && { echo “Angiv et projektnavn.” >&2; usage; exit 2; }
# Projektnavnet skal være én mappe, ikke en sti ud af basis-mappen.
if [[ “$NAME” == */* || “$NAME” == ‘.’ || “$NAME” == ‘..’ ]]; then
echo “Projektnavnet skal være et mappenavn uden / og må ikke være . eller ..” >&2
exit 2
fi

# — Find base dir —
if [[ -z “${BASE}” ]]; then
# Prøv Dropbox først, derefter ~/Projects. Bevar mellemrum i stier.
for p in \
“$HOME/Library/CloudStorage/Dropbox/Projects” \
“$HOME/Projects”
do
if [[ -d “$p” ]]; then BASE=”$p”; break; fi
done
: “${BASE:=”$HOME/Projects”}”
fi

# — Find python —
PY_BIN=””
if command -v python3 >/dev/null 2>&1; then
PY_BIN=”$(command -v python3)”
elif command -v python >/dev/null 2>&1; then
PY_BIN=”$(command -v python)”
else
echo “Kunne ikke finde python3 eller python i PATH.” >&2
echo “Installer Python via Homebrew:  brew install python” >&2
exit 1
fi
if ! “$PY_BIN” -c ‘import sys; sys.exit(0 if sys.version_info.major == 3 else 1)’; then
echo “Scriptet kræver Python 3.” >&2
exit 1
fi
echo “>> Bruger Python: $PY_BIN”

mkdir -p — “$BASE”
BASE=”${BASE:A}”
PROJ_DIR=”$BASE/$NAME”
echo “>> Opretter projekt under: $PROJ_DIR”
mkdir -p — “$PROJ_DIR/src” “$PROJ_DIR/.vscode”

# — Opret venv (kun hvis den ikke findes) —
if [[ ! -d “$PROJ_DIR/.venv” ]]; then
echo “>> Opretter .venv …”
“$PY_BIN” -m venv “$PROJ_DIR/.venv”
else
if [[ ! -x “$PROJ_DIR/.venv/bin/python” ]]; then
echo “Eksisterende .venv mangler en brugbar Python-interpreter; undersøg den manuelt.” >&2
exit 1
fi
echo “>> .venv findes allerede (springer over)”
fi

# — Undgå at Dropbox synkroniserer .venv —
if [[ “$PROJ_DIR” == “$HOME/Library/CloudStorage/Dropbox/”* ]]; then
if [[ “$OSTYPE” == darwin* ]] && command -v xattr >/dev/null 2>&1 &&
xattr -w ‘com.apple.fileprovider.ignore#P’ 1 “$PROJ_DIR/.venv”; then
echo “>> Ignore-attribut sat på .venv. Kontrollér sync-status i Dropbox.”
else
echo “ADVARSEL: .venv blev ikke undtaget fra Dropbox-sync. Kontrollér det manuelt.” >&2
fi
fi

# — .gitignore —
GITIGNORE_CONTENT=$’# Python\n.venv/\n__pycache__/\n*.pyc\n*.pyo\n*.pyd\n*.DS_Store\n.pytest_cache/\n’
if [[ ! -f “$PROJ_DIR/.gitignore” || $FORCE -eq 1 ]]; then
echo “>> Skriver .gitignore”
print -r — “$GITIGNORE_CONTENT” > “$PROJ_DIR/.gitignore”
else
echo “>> .gitignore findes (brug –force for at overskrive)”
fi

# — requirements.txt (opret tom, hvis ikke findes) —
if [[ ! -f “$PROJ_DIR/requirements.txt” ]]; then
echo “>> Opretter tom requirements.txt”
: > “$PROJ_DIR/requirements.txt”
fi

# — src/main.py —
MAIN_CONTENT=$’def main():\n    print(“Hej, verden!”)\n\n\nif __name__ == “__main__”:\n    main()\n’
if [[ ! -f “$PROJ_DIR/src/main.py” || $FORCE -eq 1 ]]; then
echo “>> Skriver src/main.py”
print -r — “$MAIN_CONTENT” > “$PROJ_DIR/src/main.py”
else
echo “>> src/main.py findes (brug –force for at overskrive)”
fi

# — .vscode/settings.json —
SETTINGS_PATH=”$PROJ_DIR/.vscode/settings.json”
SETTINGS_CONTENT=$(cat <<‘JSON’
{
“python.defaultInterpreterPath”: “${workspaceFolder}/.venv/bin/python”,
“python.terminal.activateEnvironment”: true,
“python.analysis.extraPaths”: [“${workspaceFolder}/src”]
}
JSON
)
if [[ ! -f “$SETTINGS_PATH” || $FORCE -eq 1 ]]; then
echo “>> Skriver .vscode/settings.json”
print -r — “$SETTINGS_CONTENT” > “$SETTINGS_PATH”
else
echo “>> .vscode/settings.json findes (brug –force for at overskrive)”
fi

# — Åbn i VS Code (hvis ønsket og tilgængelig) —
if [[ $OPEN_VSCODE -eq 1 ]]; then
if command -v code >/dev/null 2>&1; then
echo “>> Åbner i VS Code …”
(cd “$PROJ_DIR” && code . >/dev/null 2>&1 &)
else
echo “>> Tip: Installér ‘code’ CLI fra VS Code (Cmd+Shift+P -> Shell Command: Install ‘code’ command in PATH).”
fi
fi

cat <<EOF

Projekt klar: $PROJ_DIR

Næste skridt:
cd ${(q)PROJ_DIR}
source .venv/bin/activate
which python
python -m pip install -r requirements.txt  # hvis du har afhængigheder
python src/main.py

God fornøjelse!