Skip to content

Commit 4082fab

Browse files
authored
Add monorepo support with multi-app detection and scoped Sail guidance (#4)
* Add monorepo support with multi-app detection and scoped Sail guidance Enhance SessionStart hook to detect multiple Laravel apps in monorepos, determine the active app, and provide per-app Sail guidance. Update tests and README to reflect the new functionality. * Streamline Laravel app detection by simplifying array management and deduplication logic in SessionStart hook. * Initialize `app_dirs` array to ensure proper handling of edge cases in SessionStart hook.
1 parent 60ff91b commit 4082fab

File tree

4 files changed

+276
-57
lines changed

4 files changed

+276
-57
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Plus:
1919

2020
## SessionStart Preview
2121

22-
Below shows the startup message rendered when this plugin detects a Laravel repository. The assistant adapts guidance based on whether Sail is available.
22+
Below shows the startup message rendered when this plugin detects Laravel apps. The assistant now supports monorepos and adapts guidance based on which app is active and whether Sail is available.
2323

2424
### When Sail is detected - use Sail wrappers
2525

@@ -217,3 +217,14 @@ MIT License - see LICENSE file for details
217217

218218
- **Issues**: https://github.com/jpcaparas/superpowers-laravel/issues
219219
- **Marketplace**: https://github.com/jpcaparas/superpowers-laravel
220+
221+
### Monorepos and Multiple Apps
222+
223+
When multiple Laravel apps exist (for example `apps/api`, `apps/admin`), the SessionStart hook:
224+
225+
- Scans the repository recursively (ignores `vendor/`, `node_modules/`, `storage/`, and VCS/IDE folders) to find every `artisan` entrypoint.
226+
- Detects Laravel version per app (prefers `composer.lock` via `jq`, falls back to `composer.json` constraint or a portable parser).
227+
- Shows a summary list of all detected apps with version and Sail availability; marks the app in your current working directory as the “active” app.
228+
- Emits Sail guidance and interactive safety ONLY for the active app.
229+
230+
Tip: `cd` into the app you intend to work on before starting your session to make it the active app.

RELEASE-NOTES.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Laravel Superpowers — Release Notes
22

3+
## v0.1.4 (2025-11-04)
4+
5+
Monorepo-aware SessionStart with multi-app detection, per-app version reporting, and scoped Sail guidance.
6+
7+
### Monorepo Support
8+
9+
- Recursively discovers Laravel apps by locating `artisan` anywhere in the repo (ignores heavy/irrelevant folders like `vendor/`, `node_modules/`, `storage/`, VCS/IDE folders).
10+
- Supports multiple Laravel apps in a single repository. The hook lists all detected apps with:
11+
- Relative path
12+
- Laravel version (from `composer.lock` via `jq` when available, with fallback to `composer.json` constraint or a portable parser)
13+
- Sail availability, and whether containers appear to be running
14+
- Determines the “active” app based on the current working directory; if only one app exists, it becomes active automatically.
15+
- Emits Sail guidance and interactive safety for the active app only, to avoid cross-app confusion.
16+
17+
### Testing
18+
19+
- Extended `scripts/test_session_start.sh` to cover:
20+
- Non-Laravel repo bailout
21+
- Single app (no Sail)
22+
- Sail present but containers stopped (interactive safety messaging)
23+
- Sail present with containers running
24+
- Monorepo with two nested apps on different Laravel versions
25+
- Monorepo where hook runs inside a nested app (active app semantics)
26+
- CI workflow `.github/workflows/test-session-start.yml` continues to run this script; no changes required beyond the added scenarios.
27+
28+
### Notes
29+
30+
- If `jq` is unavailable, the hook falls back to a portable parser for version detection; output may show version constraints or `unknown` in minimal setups.
31+
332
## v0.1.2 (2025-11-04)
433

534
Docs sweep across Laravel 11.x and 12.x, with new skills matching the intersection of stable patterns. Also consolidated duplicates and kept commands thin.

hooks/session-start.sh

Lines changed: 140 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,110 @@ fi
1717
### Laravel-centric startup: optionally inject Laravel intro only
1818

1919
#############################################
20-
# Detect Laravel + Laravel Sail environment #
20+
# Detect Laravel apps (monorepo-aware) #
2121
#############################################
2222

23-
## Detect Laravel projects strictly and bail out when not detected
24-
# Consider it Laravel only when the repo has the canonical `artisan` entrypoint
25-
# or a composer.json that depends on `laravel/framework`.
26-
is_laravel=false
27-
if [ -f "artisan" ] || ( [ -f "composer.json" ] && grep -q '"laravel/framework"' composer.json ); then
28-
is_laravel=true
29-
fi
23+
# Exclusions to keep scanning fast
24+
EXCLUDES=(
25+
-path '*/.git*' -o -path '*/node_modules*' -o -path '*/vendor*' -o -path '*/storage*' -o -path '*/.idea*' -o -path '*/.vscode*'
26+
)
3027

31-
# If not a Laravel project, exit quietly so the plugin does NOT activate
32-
if [ "$is_laravel" != true ]; then
33-
exit 0
34-
fi
28+
find_laravel_apps() {
29+
# Find artisan files anywhere in the repo (monorepo support)
30+
if find . -maxdepth 0 >/dev/null 2>&1; then
31+
# shellcheck disable=SC2068
32+
find . -type f -name artisan \( ${EXCLUDES[@]} \) -prune -o -type f -name artisan -print 2>/dev/null | sed 's#^\./##'
33+
else
34+
# Fallback without -maxdepth (may be slower)
35+
# shellcheck disable=SC2068
36+
find . \( ${EXCLUDES[@]} \) -prune -o -type f -name artisan -print 2>/dev/null | sed 's#^\./##'
37+
fi
38+
}
3539

36-
## Detect Sail availability by executable presence, not composer.json
37-
# Treat Sail as available when either vendor/bin/sail exists/executable, or a top-level
38-
# ./sail helper script is present. We intentionally avoid parsing composer.json.
39-
sail_available=false
40-
if [ -x ./vendor/bin/sail ] || [ -f ./sail ]; then
41-
sail_available=true
42-
fi
40+
get_laravel_version_for_dir() {
41+
local dir="$1"; local version=""; local constraint="";
42+
if [ -f "$dir/composer.lock" ]; then
43+
if command -v jq >/dev/null 2>&1; then
44+
version=$(jq -r '.packages[]? | select(.name=="laravel/framework") | .version' "$dir/composer.lock" 2>/dev/null | head -n1 || true)
45+
fi
46+
if [ -z "$version" ]; then
47+
version=$(awk '/"name"\s*:\s*"laravel\/framework"/{f=1} f && /"version"\s*:/ {gsub(/.*"version"\s*:\s*"/,"",$0); gsub(/".*/ ,"", $0); print; exit}' "$dir/composer.lock" 2>/dev/null || true)
48+
fi
49+
fi
50+
if [ -z "$version" ] && [ -f "$dir/composer.json" ]; then
51+
if command -v jq >/dev/null 2>&1; then
52+
constraint=$(jq -r '.require["laravel/framework"] // empty' "$dir/composer.json" 2>/dev/null || true)
53+
fi
54+
if [ -z "$constraint" ]; then
55+
constraint=$(awk '/"laravel\/framework"\s*:\s*"/{gsub(/.*"laravel\/framework"\s*:\s*"/,"",$0); gsub(/".*/ ,"", $0); print; exit}' "$dir/composer.json" 2>/dev/null || true)
56+
fi
57+
fi
58+
if [ -n "$version" ]; then
59+
echo "$version"
60+
elif [ -n "$constraint" ]; then
61+
echo "$constraint"
62+
else
63+
echo "unknown"
64+
fi
65+
}
66+
67+
has_sail_for_dir() {
68+
local dir="$1";
69+
if [ -x "$dir/vendor/bin/sail" ] || [ -f "$dir/sail" ]; then
70+
echo "yes"
71+
else
72+
echo "no"
73+
fi
74+
}
4375

44-
# Detect if Sail (docker compose) containers are running for this project
45-
containers_running=false
46-
compose_cmd=""
47-
if command -v docker >/dev/null 2>&1; then
48-
# Prefer `docker compose` plugin
49-
if docker compose version >/dev/null 2>&1; then
76+
containers_running_for_dir() {
77+
local dir="$1"; local compose_cmd=""; local running="no";
78+
if [ "${SUPERPOWERS_TEST_SAIL_RUNNING:-}" = "true" ]; then
79+
echo "yes"; return 0
80+
elif [ "${SUPERPOWERS_TEST_SAIL_RUNNING:-}" = "false" ]; then
81+
echo "no"; return 0
82+
fi
83+
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
5084
compose_cmd="docker compose"
85+
elif command -v docker-compose >/dev/null 2>&1; then
86+
compose_cmd="docker-compose"
5187
fi
52-
fi
53-
if [ -z "$compose_cmd" ] && command -v docker-compose >/dev/null 2>&1; then
54-
compose_cmd="docker-compose"
88+
if [ -n "$compose_cmd" ]; then
89+
( cd "$dir" && $compose_cmd ps -q >/dev/null 2>&1 && [ -n "$($compose_cmd ps -q 2>/dev/null)" ] ) && running="yes" || true
90+
fi
91+
echo "$running"
92+
}
93+
94+
# Build app list
95+
declare -a app_dirs
96+
app_dirs=()
97+
while IFS= read -r f; do
98+
[ -z "$f" ] && continue
99+
d=$(dirname "$f")
100+
# Deduplicate
101+
if [ ${#app_dirs[@]} -gt 0 ] && printf '%s\n' "${app_dirs[@]}" | grep -Fxq "$d"; then continue; fi
102+
app_dirs+=("$d")
103+
done < <(find_laravel_apps)
104+
105+
# If no Laravel apps anywhere, exit quietly so the plugin does NOT activate
106+
if [ ${#app_dirs[@]} -eq 0 ]; then
107+
exit 0
55108
fi
56109

57-
if [ -n "$compose_cmd" ]; then
58-
# Run quietly; if any service container exists and is running, consider Sail "up"
59-
if $compose_cmd ps -q >/dev/null 2>&1; then
60-
if [ -n "$($compose_cmd ps -q 2>/dev/null)" ]; then
61-
containers_running=true
62-
fi
110+
# Identify active app based on current working directory (nearest ancestor with artisan)
111+
active_dir=""
112+
search_dir="$PWD"
113+
while [ "$search_dir" != "/" ]; do
114+
if [ -f "$search_dir/artisan" ]; then
115+
active_dir="$search_dir"
116+
break
63117
fi
64-
fi
118+
search_dir="$(cd "$search_dir/.." && pwd)"
119+
done
65120

66-
# Test override to simulate container status in CI
67-
if [ "${SUPERPOWERS_TEST_SAIL_RUNNING:-}" = "true" ]; then
68-
containers_running=true
69-
elif [ "${SUPERPOWERS_TEST_SAIL_RUNNING:-}" = "false" ]; then
70-
containers_running=false
121+
# Default to the only app if just one exists
122+
if [ -z "$active_dir" ] && [ ${#app_dirs[@]} -eq 1 ]; then
123+
active_dir="$(cd "${app_dirs[0]}" && pwd)"
71124
fi
72125

73126
# Read Laravel intro skill
@@ -80,27 +133,65 @@ fi
80133
laravel_intro_escaped=$(echo "$laravel_intro_content" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
81134
warning_escaped=$(echo "$warning_message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
82135

83-
# Build Sail guidance based on detection
84-
sail_guidance=""
85-
if [ "$sail_available" = true ]; then
86-
sail_guidance="Laravel Sail detected (vendor/bin/sail or ./sail present). Prefer Sail commands inside containers to avoid host/env drift.\n\nKey mappings (Sail | Non‑Sail):\n- sail artisan … | php artisan …\n- sail composer … | composer …\n- sail php … | php …\n- sail pnpm … | pnpm … (or npm/yarn/bun)\n- sail mysql/psql/redis … | mysql/psql/redis-cli …\n\nPortable alias:\n alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'\n"
136+
#############################################
137+
# Build output (apps summary + Sail guidance) #
138+
#############################################
87139

88-
if [ "$containers_running" = false ]; then
89-
sail_guidance+="\nInteractive safety: Sail is present but containers are not running. Before executing any host commands (php, composer, mysql, node, pnpm, npm, yarn), ask the user: \"Start Sail containers now with: 'sail up -d'? Or proceed using host tools?\" Do not run host commands unless the user explicitly opts to proceed without Sail.\nTip: Start containers: 'sail up -d' then verify: 'sail ps'.\n"
140+
# Build listing lines with version and Sail per app
141+
declare -a app_lines
142+
for d in "${app_dirs[@]}"; do
143+
rel="${d#./}"
144+
[ "$rel" = "." ] && rel="."
145+
ver=$(get_laravel_version_for_dir "$d")
146+
sail=$(has_sail_for_dir "$d")
147+
running=$(containers_running_for_dir "$d")
148+
if [ "$sail" = "yes" ]; then
149+
line="- ${rel} (Laravel ${ver}; Sail: yes, containers: ${running})"
90150
else
91-
sail_guidance+="\nSail appears to be running (docker compose ps shows active containers). Use Sail commands (artisan/composer/node/db) and avoid host binaries to keep environments consistent.\n"
151+
line="- ${rel} (Laravel ${ver}; Sail: no)"
92152
fi
153+
app_lines+=("$line")
154+
done
155+
156+
apps_summary=$(printf "%s\n" "${app_lines[@]}")
157+
apps_summary_escaped=$(echo "$apps_summary" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
158+
159+
# Active app line
160+
active_line=""
161+
if [ -n "$active_dir" ]; then
162+
# Make relative to repo root
163+
if [ "$active_dir" = "$PWD" ]; then
164+
rel_active="."
165+
else
166+
rel_active="${active_dir#${PWD}/}"
167+
[ -z "$rel_active" ] && rel_active="."
168+
fi
169+
ver_active=$(get_laravel_version_for_dir "$active_dir")
170+
active_line="Active Laravel app: ${rel_active} (Laravel ${ver_active})\n"
171+
else
172+
active_line="No active Laravel app (not currently inside any app directory).\nChange working directory to one of the listed apps to focus this session.\n"
93173
fi
174+
active_line_escaped=$(echo "$active_line" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
94175

95-
# Escape Sail guidance
176+
# Sail guidance for the active app (if any)
177+
sail_guidance=""
178+
if [ -n "$active_dir" ] && [ "$(has_sail_for_dir "$active_dir")" = "yes" ]; then
179+
running_this=$(containers_running_for_dir "$active_dir")
180+
sail_guidance="Laravel Sail detected for active app. Prefer Sail commands inside containers to avoid host/env drift.\n\nKey mappings (Sail | Non‑Sail):\n- sail artisan … | php artisan …\n- sail composer … | composer …\n- sail php … | php …\n- sail pnpm … | pnpm … (or npm/yarn/bun)\n- sail mysql/psql/redis … | mysql/psql/redis-cli …\n\nPortable alias:\n alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'\n"
181+
if [ "$running_this" = "no" ]; then
182+
sail_guidance+="\nInteractive safety: Sail is present but containers are not running. Before executing any host commands (php, composer, mysql, node, pnpm, npm, yarn), ask the user: \"Start Sail containers now with: 'sail up -d'? Or proceed using host tools?\" Do not run host commands unless the user explicitly opts to proceed without Sail.\nTip: Start containers: 'sail up -d' then verify: 'sail ps'.\n"
183+
else
184+
sail_guidance+="\nSail appears to be running for the active app. Use Sail commands (artisan/composer/node/db) and avoid host binaries to keep environments consistent.\n"
185+
fi
186+
fi
96187
sail_guidance_escaped=$(echo "$sail_guidance" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
97188

98189
# Output context injection as JSON
99190
cat <<EOF
100191
{
101192
"hookSpecificOutput": {
102193
"hookEventName": "SessionStart",
103-
"additionalContext": "<EXTREMELY_IMPORTANT>\nThis repository appears to be a Laravel project. Read the following onboarding first, then use the 'Skill' tool to run any Laravel skills you need.\n\n${laravel_intro_escaped}\n\n${sail_guidance_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
194+
"additionalContext": "<EXTREMELY_IMPORTANT>\nLaravel projects detected in this repository. Read the onboarding below, then use the 'Skill' tool.\n\n${active_line_escaped}\nDetected apps:\n${apps_summary_escaped}\n\n${laravel_intro_escaped}\n\n${sail_guidance_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
104195
}
105196
}
106197
EOF

0 commit comments

Comments
 (0)