feat(bootstrap): auto-detect dev shell into copilot-instructions
Some checks are pending
CI / Lint & self-test (push) Waiting to run

Neues Default-Shell-Feld in der Projekt-Sektion (nach ---), damit der Agent die richtige Shell-Syntax waehlt. Bootstrap erkennt $SHELL (Fallback fish, Whitelist gegen sed-Injection) und schreibt es in frisch erzeugte copilot-instructions.md; manuell aenderbar und ueberlebt copilot-update. selftest +3 Faelle (sh & fish), beide MAINTAINER.md dokumentiert.
This commit is contained in:
Conrad Schulz 2026-06-18 10:05:46 +02:00
parent e83c333d75
commit 5d0194a7b8
9 changed files with 176 additions and 4 deletions

View file

@ -60,6 +60,16 @@
--- ---
## 🖥 Entwicklungsumgebung
<!-- copilot:dev-shell beim Bootstrap automatisch aus $SHELL gesetzt; jederzeit manuell änderbar -->
- **Default-Shell:** fish
Der Agent nutzt diese Shell für **alle** Terminal-Kommandos und passt die Syntax an
(fish ≠ POSIX sh/bash z.B. `set -x VAR wert` statt `export VAR=wert`, `(cmd)` statt
`$(cmd)`, kein `&&`/`||`-Verketten in fish-Skripten). Bei anderem System / neuem Repo
diesen Wert anpassen.
## Project ## Project
<!-- TODO: Beschreibe das Projekt in 1-2 Sätzen --> <!-- TODO: Beschreibe das Projekt in 1-2 Sätzen -->

View file

@ -149,6 +149,7 @@ Lokal vor dem Commit: `sh scripts/selftest.sh` ausführen.
| Kein globales `init.templateDir`, Opt-in `git init-copilot` | Standard-`git init`/`git clone` bleiben unberührt; Quality Gate landet nur in bewusst gewählten Repos | | Kein globales `init.templateDir`, Opt-in `git init-copilot` | Standard-`git init`/`git clone` bleiben unberührt; Quality Gate landet nur in bewusst gewählten Repos |
| Remote-Quelle konfigurierbar (env/Config) + Offline-Modus | Keine hartkodierte Infrastruktur; `git copilot-update` läuft auch ohne Netz und endet graceful (exit 0) | | Remote-Quelle konfigurierbar (env/Config) + Offline-Modus | Keine hartkodierte Infrastruktur; `git copilot-update` läuft auch ohne Netz und endet graceful (exit 0) |
| `detect_vscode_user_dir` in jedem Skript dupliziert | Skripte laufen standalone aus `~/.local/bin` → keine gemeinsame Lib sourcebar; bewusst kopiert statt geteilt | | `detect_vscode_user_dir` in jedem Skript dupliziert | Skripte laufen standalone aus `~/.local/bin` → keine gemeinsame Lib sourcebar; bewusst kopiert statt geteilt |
| Default-Shell-Feld in der Projekt-Sektion (nach `---`) | Bootstrap erkennt `$SHELL` und schreibt es nach `.github/copilot-instructions.md`; liegt bewusst **nach** dem `---`, damit `copilot-update` die Framework-Sektion neu schreiben kann, ohne den repo-/maschinenspezifischen Shell-Wert zu überschreiben |
## Bekannte Fallstricke ## Bekannte Fallstricke
@ -159,3 +160,4 @@ Lokal vor dem Commit: `sh scripts/selftest.sh` ausführen.
| Leerzeile vor `---` in `copilot-instructions.md` wird entfernt | `$(awk ...)` strippt trailing newlines; `printf '%s\n%s\n'` setzt Blöcke direkt zusammen | `printf '%s\n\n%s\n'` verwenden | | Leerzeile vor `---` in `copilot-instructions.md` wird entfernt | `$(awk ...)` strippt trailing newlines; `printf '%s\n%s\n'` setzt Blöcke direkt zusammen | `printf '%s\n\n%s\n'` verwenden |
| `$USER` unbound unter `set -u` | `deploy.sh` nutzt `set -euo pipefail`; `$USER` fehlt in minimalen Envs (`env -i`, CI) | In `detect_vscode_user_dir` `${USER:-}` statt `$USER` | | `$USER` unbound unter `set -u` | `deploy.sh` nutzt `set -euo pipefail`; `$USER` fehlt in minimalen Envs (`env -i`, CI) | In `detect_vscode_user_dir` `${USER:-}` statt `$USER` |
| fish: Funktion ändert Skript-Variable nicht | fish-Funktionen sehen Caller-Locals nicht | `set -g` für Zähler/Flags die eine Funktion schreibt (z. B. `_append_gitignore`) | | fish: Funktion ändert Skript-Variable nicht | fish-Funktionen sehen Caller-Locals nicht | `set -g` für Zähler/Flags die eine Funktion schreibt (z. B. `_append_gitignore`) |
| Anpassbares Feld wird von `copilot-update` überschrieben | Inhalt stand **vor** dem ersten `---` (Framework-Sektion wird immer aus Template neu geschrieben) | Repo-/maschinenspezifische, anpassbare Felder immer **nach** `---` platzieren (Projekt-Sektion bleibt erhalten) siehe Default-Shell-Feld |

View file

@ -0,0 +1,53 @@
# Session History
### Prompt 1
nimm mal mit auf was die default shell auf dem system ist auf dem entwickelt wird, bei mir ist das fast immer fish und das sollte als default drin stehen aber auch änderbar sein bzw. bei einem neuen repo neu hinzugefügt werden muss.
### Antwort 1
Ich habe ein **Default-Shell-Feld** eingeführt, das (a) standardmäßig `fish` enthält, (b) jederzeit manuell änderbar ist und (c) beim Bootstrap eines neuen Repos automatisch aus `$SHELL` des Entwicklungssystems gesetzt wird.
Zentrale Designentscheidung: Das Feld liegt in der **Projekt-Sektion** der `copilot-instructions.md` (nach dem ersten `---`). Grund: `copilot-update` schreibt die Framework-Sektion (vor `---`) bei jedem Update neu — nur hinter `---` überlebt ein repo-/maschinenspezifischer Wert. Diese Constraint habe ich zusätzlich im Repo-Memory festgehalten.
Geänderte Dateien (7):
```text
.github/copilot-instructions.md # neue Sektion "🖥 Entwicklungsumgebung" mit "- **Default-Shell:** fish" (nach ---)
git-templates/.github/copilot-instructions.md # gleiche Sektion im Template (Wert wird beim Bootstrap ersetzt)
scripts/copilot-bootstrap.sh # $SHELL-Erkennung (basename, Fallback fish, Whitelist ^[a-z0-9_-]+$) + _set_dev_shell()
scripts/copilot-bootstrap.fish # Parität zur sh-Version (string match -qr Validierung, functions -e Cleanup)
scripts/selftest.sh # neue Fälle: Default-Shell-Zeile vorhanden + SHELL=zsz/leer → korrekter Wert (sh & fish)
docs/MAINTAINER.md # Designentscheidung + Fallstrick "Feld nach --- platzieren" dokumentiert
git-templates/docs/MAINTAINER.md # Default-Shell-Feld für neue Repos erklärt
```
Implementierungsdetails der Erkennung (identisch in sh und fish):
- `$SHELL``basename` (z. B. `/usr/bin/fish``fish`); leeres `$SHELL` → Fallback `fish`.
- Wert gegen `^[a-z0-9_-]+$` geprüft (verhindert sed-Injection), sonst Fallback `fish`.
- `_set_dev_shell()` ersetzt die `- **Default-Shell:** …`-Zeile **nur in frisch erzeugten** `copilot-instructions.md` (bestehende Dateien bleiben unangetastet → User-Anpassung bleibt erhalten).
### Prompt 2
War das hier alles abgeschlossen und committed, dokumentiert und gepusht?
### Antwort 2
Nein — der Code war fertig und validiert, aber **nicht** committed, **nicht** in die History dokumentiert und **nicht** gepusht; zusätzlich lag der lokale Branch 1 Commit **hinter** `origin/master`. Ich habe den Zustand geprüft, das fehlende Remote-Commit integriert, alles validiert und anschließend History + Kontext geschrieben, committed und gepusht.
Statusprüfung:
```text
git status → 7 unstaged Dateien, "behind 'origin/master' by 1 commit"
git log --oneline @{u}.. → leer (nichts unpushed, weil nichts committed)
sh -n / fish -n → bootstrap.sh OK, selftest.sh OK, bootstrap.fish OK
sh scripts/selftest.sh → 6/6 PASS (inkl. neuer Shell-Erkennungsfälle)
```
Integration des Remote-Commits (`e83c333 docs: add rd13 infrastructure conventions`) trotz dirty tree — die 3 überlappenden Dateien hatten Hunks an unterschiedlichen Stellen, daher konfliktfrei:
```text
git stash push -m dev-shell-feature-wip -- <7 Dateien>
git pull --ff-only # 5dc89f0 → e83c333, Fast-forward
git stash pop # Auto-merge: docs/MAINTAINER.md, git-templates/{.github/copilot-instructions,docs/MAINTAINER}.md → exit 0, keine Konflikte
```
Verifikation nach Merge: beide Änderungssätze (rd13_-Blöcke + Default-Shell) in allen überlappenden Dateien vorhanden, Selftest erneut 6/6 PASS.
Danach: Session-History (diese Datei) angelegt, `PROJECT_CONTEXT.md` aktualisiert, committed (pre-commit Quality Gate bestanden) und nach `origin/master` gepusht.

View file

@ -8,7 +8,7 @@
## Aktueller Projektstatus ## Aktueller Projektstatus
**Letzte Aktualisierung:** 2026-06-13 rd13_-Grundkonvention ergänzt: Runner heißt Docker, Services hinter Caddy, zentrales Proxy-Repo `rd13_system_proxy` **Letzte Aktualisierung:** 2026-06-18 Default-Shell-Feld eingeführt: Bootstrap erkennt `$SHELL` automatisch, schreibt es in die Projekt-Sektion der `copilot-instructions.md` (überlebt `copilot-update`), manuell änderbar; Default `fish`
**Phase:** Produktion / stabil wird bei Bedarf erweitert **Phase:** Produktion / stabil wird bei Bedarf erweitert
--- ---
@ -33,6 +33,7 @@ die zentrale Proxy-Schicht liegt im Repo `rd13_system_proxy`.
| Datum | Aufgabe | Ergebnis | Entscheidungen | | Datum | Aufgabe | Ergebnis | Entscheidungen |
|---|---|---|---| |---|---|---|---|
| 2026-06-18 | Default-Shell-Erkennung | Bootstrap setzt `$SHELL` (Fallback `fish`) automatisch in `copilot-instructions.md`; Feld nach `---` (überlebt update); selftest +3 Fälle; beide MAINTAINER.md dokumentiert | Feld in Projekt-Sektion, nicht Framework-Sektion; Whitelist `^[a-z0-9_-]+$` gegen sed-Injection |
| 2026-06-13 | rd13_-Infrastrukturkonvention | Runner in Docker; alle Services hinter Caddy; zentrales Proxy-Repo `rd13_system_proxy` | this session | | 2026-06-13 | rd13_-Infrastrukturkonvention | Runner in Docker; alle Services hinter Caddy; zentrales Proxy-Repo `rd13_system_proxy` | this session |
| 2026-06-10 | Agent-History → `docs/history/` + Auto-Migration | pre-commit Cutover, alle Skripte, settings/prompt/instructions/docs; `git mv` dieses Repos | `2b20a98` + Dogfood | | 2026-06-10 | Agent-History → `docs/history/` + Auto-Migration | pre-commit Cutover, alle Skripte, settings/prompt/instructions/docs; `git mv` dieses Repos | `2b20a98` + Dogfood |
| 2026-06-10 | WP12 Doku-Portabilität | README/USER/ADMIN/MAINTAINER: env/config, init-copilot, offline; tote IP raus | `afd5b38` | | 2026-06-10 | WP12 Doku-Portabilität | README/USER/ADMIN/MAINTAINER: env/config, init-copilot, offline; tote IP raus | `afd5b38` |
@ -83,6 +84,7 @@ die zentrale Proxy-Schicht liegt im Repo `rd13_system_proxy`.
| CI: shellcheck + fish -n + selftest | Verhindert Regressionen in den Shell-Skripten | `.github/workflows/ci.yml`, `scripts/selftest.sh` | | CI: shellcheck + fish -n + selftest | Verhindert Regressionen in den Shell-Skripten | `.github/workflows/ci.yml`, `scripts/selftest.sh` |
| Agent-History unter `docs/history/` | Aufgeräumte Struktur (alles Doku-Artefakte unter docs/); Cutover statt Dual-Pfad | pre-commit, alle Skripte | | Agent-History unter `docs/history/` | Aufgeräumte Struktur (alles Doku-Artefakte unter docs/); Cutover statt Dual-Pfad | pre-commit, alle Skripte |
| Auto-Migration `history/``docs/history/` in copilot-update | Bestehende Repos ziehen ohne manuellen Eingriff nach (git mv, Fallback mv) | `scripts/copilot-update.{sh,fish}` | | Auto-Migration `history/``docs/history/` in copilot-update | Bestehende Repos ziehen ohne manuellen Eingriff nach (git mv, Fallback mv) | `scripts/copilot-update.{sh,fish}` |
| Default-Shell-Feld in Projekt-Sektion (nach `---`) | Bootstrap erkennt `$SHELL`; Wert überlebt `copilot-update` (Framework-Sektion wird neu geschrieben); manuell pro Repo/Maschine änderbar | `scripts/copilot-bootstrap.{sh,fish}`, `git-templates/.github/copilot-instructions.md` |
--- ---

View file

@ -60,6 +60,16 @@
--- ---
## 🖥 Entwicklungsumgebung
<!-- copilot:dev-shell beim Bootstrap automatisch aus $SHELL gesetzt; jederzeit manuell änderbar -->
- **Default-Shell:** fish
Der Agent nutzt diese Shell für **alle** Terminal-Kommandos und passt die Syntax an
(fish ≠ POSIX sh/bash z.B. `set -x VAR wert` statt `export VAR=wert`, `(cmd)` statt
`$(cmd)`, kein `&&`/`||`-Verketten in fish-Skripten). Bei anderem System / neuem Repo
diesen Wert anpassen.
## Project ## Project
<!-- TODO: Beschreibe das Projekt in 1-2 Sätzen --> <!-- TODO: Beschreibe das Projekt in 1-2 Sätzen -->

View file

@ -59,6 +59,12 @@ cd <repo>
# Dev-Server starten # Dev-Server starten
``` ```
> **Default-Shell:** `.github/copilot-instructions.md` enthält im Abschnitt
> *Entwicklungsumgebung* das Feld `- **Default-Shell:**`. Es sagt dem Copilot-Agent,
> welche Shell-Syntax er für Terminal-Kommandos nutzen soll. Beim Bootstrap wird der
> Wert automatisch aus `$SHELL` erkannt; bei abweichender Entwicklungs-Shell den Wert
> dort anpassen.
--- ---
## Architekturentscheidungen (ADRs) ## Architekturentscheidungen (ADRs)

View file

@ -30,11 +30,37 @@ end
echo "Bootstrapping Copilot config in: $TARGET" echo "Bootstrapping Copilot config in: $TARGET"
# ── Default-Shell des Entwicklungssystems erkennen ───────────────────────────
# Wird in die frisch erzeugte copilot-instructions.md geschrieben, damit der
# Agent die richtige Shell-Syntax fuer Terminal-Kommandos waehlt. Fallback: fish.
if test -n "$SHELL"
set -g DEV_SHELL (basename "$SHELL")
else
set -g DEV_SHELL fish
end
if test -z "$DEV_SHELL"; or not string match -qr '^[a-z0-9_-]+$' -- "$DEV_SHELL"
set -g DEV_SHELL fish
end
# Ersetzt die `- **Default-Shell:** …`-Zeile in $f durch den erkannten Wert.
# set -g DEV_SHELL oben, damit die Funktion den Wert im Skript-Scope sieht.
function _set_dev_shell --argument-names f
test -f "$f"; or return 0
grep -q '^- \*\*Default-Shell:\*\*' "$f" 2>/dev/null; or return 0
set tmp "$f.tmp.$fish_pid"
if sed "s|^- \*\*Default-Shell:\*\* .*|- **Default-Shell:** $DEV_SHELL|" "$f" >$tmp 2>/dev/null
mv $tmp "$f"
else
rm -f $tmp
end
end
# .github/copilot-instructions.md # .github/copilot-instructions.md
if not test -f "$TARGET/.github/copilot-instructions.md" if not test -f "$TARGET/.github/copilot-instructions.md"
mkdir -p "$TARGET/.github" mkdir -p "$TARGET/.github"
cp "$TEMPLATE_DIR/.github/copilot-instructions.md" "$TARGET/.github/copilot-instructions.md" cp "$TEMPLATE_DIR/.github/copilot-instructions.md" "$TARGET/.github/copilot-instructions.md"
echo " ✓ .github/copilot-instructions.md created (TODO-Felder ausfüllen)" _set_dev_shell "$TARGET/.github/copilot-instructions.md"
echo " ✓ .github/copilot-instructions.md created (Default-Shell: $DEV_SHELL; TODO-Felder ausfüllen)"
else else
echo " ─ .github/copilot-instructions.md already exists, skipping" echo " ─ .github/copilot-instructions.md already exists, skipping"
end end
@ -149,6 +175,7 @@ else
end end
functions -e _append_gitignore functions -e _append_gitignore
functions -e _set_dev_shell
echo "" echo ""
echo "Done. Nächste Schritte:" echo "Done. Nächste Schritte:"

View file

@ -27,11 +27,37 @@ fi
echo "Bootstrapping Copilot config in: $TARGET" echo "Bootstrapping Copilot config in: $TARGET"
# ── Default-Shell des Entwicklungssystems erkennen ───────────────────────────
# Wird in die frisch erzeugte copilot-instructions.md geschrieben, damit der
# Agent die richtige Shell-Syntax fuer Terminal-Kommandos waehlt. Fallback: fish.
if [ -n "${SHELL:-}" ]; then
DEV_SHELL=$(basename "$SHELL")
else
DEV_SHELL=fish
fi
case "$DEV_SHELL" in
''|*[!a-z0-9_-]*) DEV_SHELL=fish ;;
esac
# Ersetzt die `- **Default-Shell:** …`-Zeile in $1 durch den erkannten Wert.
_set_dev_shell() {
f="$1"
[ -f "$f" ] || return 0
grep -q '^- \*\*Default-Shell:\*\*' "$f" 2>/dev/null || return 0
tmp="$f.tmp.$$"
if sed "s|^- \*\*Default-Shell:\*\* .*|- **Default-Shell:** $DEV_SHELL|" "$f" > "$tmp" 2>/dev/null; then
mv "$tmp" "$f"
else
rm -f "$tmp"
fi
}
# .github/copilot-instructions.md # .github/copilot-instructions.md
if [ ! -f "$TARGET/.github/copilot-instructions.md" ]; then if [ ! -f "$TARGET/.github/copilot-instructions.md" ]; then
mkdir -p "$TARGET/.github" mkdir -p "$TARGET/.github"
cp "$TEMPLATE_DIR/.github/copilot-instructions.md" "$TARGET/.github/copilot-instructions.md" cp "$TEMPLATE_DIR/.github/copilot-instructions.md" "$TARGET/.github/copilot-instructions.md"
echo " ✓ .github/copilot-instructions.md created (TODO-Felder ausfüllen)" _set_dev_shell "$TARGET/.github/copilot-instructions.md"
echo " ✓ .github/copilot-instructions.md created (Default-Shell: $DEV_SHELL; TODO-Felder ausfüllen)"
else else
echo " ─ .github/copilot-instructions.md already exists, skipping" echo " ─ .github/copilot-instructions.md already exists, skipping"
fi fi

View file

@ -14,7 +14,9 @@ trap 'rm -rf "$WORK"' EXIT
build_template() { build_template() {
tpl="$1" tpl="$1"
mkdir -p "$tpl/.github" "$tpl/.vscode" "$tpl/hooks" "$tpl/docs" "$tpl/docs/history/summary" mkdir -p "$tpl/.github" "$tpl/.vscode" "$tpl/hooks" "$tpl/docs" "$tpl/docs/history/summary"
echo "framework" > "$tpl/.github/copilot-instructions.md" # Realistische copilot-instructions.md inkl. Default-Shell-Zeile (Projekt-Sektion)
printf '# Instr\n\n---\n\n## Entwicklungsumgebung\n\n- **Default-Shell:** fish\n\n## Project\n' \
> "$tpl/.github/copilot-instructions.md"
echo '{}' > "$tpl/.vscode/settings.json" echo '{}' > "$tpl/.vscode/settings.json"
echo '{}' > "$tpl/.vscode/extensions.json" echo '{}' > "$tpl/.vscode/extensions.json"
printf '#!/bin/sh\nexit 0\n' > "$tpl/hooks/pre-commit" printf '#!/bin/sh\nexit 0\n' > "$tpl/hooks/pre-commit"
@ -58,13 +60,47 @@ run_case() {
echo "FAIL: $label .gitignore enthaelt kein Zone.Identifier-Muster" >&2 echo "FAIL: $label .gitignore enthaelt kein Zone.Identifier-Muster" >&2
exit 1 exit 1
fi fi
if ! grep -q '^- \*\*Default-Shell:\*\*' "$target/.github/copilot-instructions.md"; then
echo "FAIL: $label Default-Shell-Zeile fehlt in copilot-instructions.md" >&2
exit 1
fi
echo "PASS: $label ($script)" echo "PASS: $label ($script)"
} }
# Prueft, dass die erkannte $SHELL korrekt in die copilot-instructions.md
# geschrieben wird (deterministisch ueber explizit gesetztes SHELL).
run_shell_case() {
label="$1"
runner="$2"
script="$3"
shellval="$4"
expected="$5"
home="$WORK/home_$label"
target="$WORK/target_$label"
mkdir -p "$home/.git-templates" "$target"
build_template "$home/.git-templates"
git -C "$target" init -q
HOME="$home" SHELL="$shellval" "$runner" "$REPO_DIR/scripts/$script" "$target" >/dev/null
got=$(sed -n 's|^- \*\*Default-Shell:\*\* ||p' "$target/.github/copilot-instructions.md")
if [ "$got" != "$expected" ]; then
echo "FAIL: $label Default-Shell '$got' != erwartet '$expected'" >&2
exit 1
fi
echo "PASS: $label (SHELL='$shellval' → $expected)"
}
run_case sh_bootstrap sh copilot-bootstrap.sh run_case sh_bootstrap sh copilot-bootstrap.sh
# Shell-Erkennung: expliziter Wert wird uebernommen, leeres SHELL faellt auf fish zurueck.
run_shell_case sh_shell_zsh sh copilot-bootstrap.sh /usr/bin/zsh zsh
run_shell_case sh_shell_empty sh copilot-bootstrap.sh "" fish
if command -v fish >/dev/null 2>&1; then if command -v fish >/dev/null 2>&1; then
run_case fish_bootstrap fish copilot-bootstrap.fish run_case fish_bootstrap fish copilot-bootstrap.fish
run_shell_case fish_shell_zsh fish copilot-bootstrap.fish /usr/bin/zsh zsh
run_shell_case fish_shell_empty fish copilot-bootstrap.fish "" fish
else else
echo "SKIP: fish nicht installiert fish-Bootstrap uebersprungen" echo "SKIP: fish nicht installiert fish-Bootstrap uebersprungen"
fi fi