@kennethsolomon/shipkit 3.20.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/package.json +1 -1
- package/skills/sk:laravel-init/SKILL.md +8 -7
- package/skills/sk:laravel-new/SKILL.md +1 -0
- package/skills/sk:setup-claude/SKILL.md +51 -0
- package/skills/sk:setup-claude/references/skill-profiles.md +25 -3
- package/skills/sk:setup-claude/scripts/__pycache__/apply_setup_claude.cpython-314.pyc +0 -0
- package/skills/sk:setup-claude/scripts/apply_setup_claude.py +110 -10
- package/skills/sk:setup-claude/templates/.claude/rules/laravel.md.template +14 -0
- package/skills/sk:setup-claude/tests/__pycache__/test_apply_setup_claude.cpython-314.pyc +0 -0
- package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +267 -0
- package/skills/sk:setup-optimizer/SKILL.md +16 -3
package/README.md
CHANGED
|
@@ -365,7 +365,9 @@ Rule files in `.claude/rules/` auto-activate in Claude Code when you edit matchi
|
|
|
365
365
|
|
|
366
366
|
## MCP Servers
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
### Global (opt-in, installed to `~/.mcp.json`)
|
|
369
|
+
|
|
370
|
+
Installed by `/sk:setup-claude` and `/sk:setup-optimizer` when you opt in.
|
|
369
371
|
|
|
370
372
|
| Server | What it does | Best for |
|
|
371
373
|
|---|---|---|
|
|
@@ -373,6 +375,14 @@ Installed optionally by `/sk:setup-claude` and `/sk:setup-optimizer`.
|
|
|
373
375
|
| **context7** | Fetches current, version-accurate docs for libraries you're using — no stale API suggestions | React 19, Next.js 15, Tailwind v4, shadcn/ui |
|
|
374
376
|
| **ccstatusline** | Persistent statusline: context window %, model, git branch, current task | Every session |
|
|
375
377
|
|
|
378
|
+
### Project-level (stack-conditional, installed to `.mcp.json`)
|
|
379
|
+
|
|
380
|
+
Added/removed automatically based on detected stack. No opt-in required.
|
|
381
|
+
|
|
382
|
+
| Server | Stack | What it does |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| **laravel-boost** | Laravel | Database schema, read-only queries, docs search (version-matched), application logs, browser errors, last exception, Eloquent model list |
|
|
385
|
+
|
|
376
386
|
---
|
|
377
387
|
|
|
378
388
|
## On-Demand Tools
|
package/package.json
CHANGED
|
@@ -12,13 +12,14 @@ Configure this Laravel project with production-ready conventions. Safe to run on
|
|
|
12
12
|
Invokes the `setup-claude` skill to:
|
|
13
13
|
|
|
14
14
|
1. **Detect stack** from `composer.json` + `package.json`
|
|
15
|
-
2. **Install missing dev tools**: PHPStan/Larastan, Rector, Pint, Pest
|
|
16
|
-
3. **
|
|
17
|
-
4. **
|
|
18
|
-
5. **
|
|
19
|
-
6. **
|
|
20
|
-
7. **
|
|
21
|
-
8. **
|
|
15
|
+
2. **Install missing dev tools**: PHPStan/Larastan, Rector, Pint, Pest, Laravel Boost
|
|
16
|
+
3. **Configure Laravel Boost MCP** in `.mcp.json` — database schema, queries, docs search, logs, browser errors
|
|
17
|
+
4. **Publish config files** (if missing): `phpstan.neon`, `rector.php`, `pint.json`
|
|
18
|
+
5. **Configure strict models** in `AppServiceProvider`
|
|
19
|
+
6. **Generate `CLAUDE.md`** — stack-aware, with full workflow, conventions, and sub-agent patterns
|
|
20
|
+
7. **Bootstrap `tasks/`** — findings, lessons, todo, progress, security-findings
|
|
21
|
+
8. **Generate `.claude/commands/`** — lint, test
|
|
22
|
+
9. **Pre-seed `tasks/findings.md`** with detected stack info
|
|
22
23
|
|
|
23
24
|
## Idempotent
|
|
24
25
|
|
|
@@ -277,8 +277,59 @@ composer require --dev laravel/pint
|
|
|
277
277
|
# Pest (if missing)
|
|
278
278
|
composer require --dev pestphp/pest pestphp/pest-plugin-laravel
|
|
279
279
|
./vendor/bin/pest --init
|
|
280
|
+
|
|
281
|
+
# Laravel Boost MCP (if missing) — provides database schema, queries, docs search, logs, browser errors
|
|
282
|
+
composer require --dev laravel/boost
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Laravel Boost MCP Configuration
|
|
286
|
+
|
|
287
|
+
**Do NOT run `php artisan boost:install`** — it generates its own CLAUDE.md and skills that conflict with ShipKit-managed files. We configure the MCP server entry directly.
|
|
288
|
+
|
|
289
|
+
After installing `laravel/boost`, configure the MCP server in the project's `.mcp.json` (create-if-missing, merge additively):
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"mcpServers": {
|
|
294
|
+
"laravel-boost": {
|
|
295
|
+
"command": "php",
|
|
296
|
+
"args": ["artisan", "boost:mcp"],
|
|
297
|
+
"env": {}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
280
301
|
```
|
|
281
302
|
|
|
303
|
+
For **Laravel Sail** projects (detected by `vendor/bin/sail` existing), use:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"mcpServers": {
|
|
308
|
+
"laravel-boost": {
|
|
309
|
+
"command": "vendor/bin/sail",
|
|
310
|
+
"args": ["artisan", "boost:mcp"],
|
|
311
|
+
"env": {}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Idempotency:** If `laravel-boost` already exists in `.mcp.json`, check whether the command matches the current Sail status. If `vendor/bin/sail` exists but the entry uses `php` (or vice versa), update the entry to match. Otherwise skip.
|
|
318
|
+
|
|
319
|
+
**What Boost MCP provides (9 tools):**
|
|
320
|
+
|
|
321
|
+
| Tool | Purpose |
|
|
322
|
+
|------|---------|
|
|
323
|
+
| ApplicationInfo | PHP/Laravel version, DB engine, installed packages, Eloquent models |
|
|
324
|
+
| DatabaseSchema | Full schema — tables, columns, indexes, foreign keys (summary + filter modes) |
|
|
325
|
+
| DatabaseQuery | Read-only SQL queries (SELECT, SHOW, EXPLAIN, DESCRIBE only) |
|
|
326
|
+
| DatabaseConnections | All configured DB connections and the default |
|
|
327
|
+
| SearchDocs | Laravel ecosystem docs (Laravel, Pest, Livewire, Filament, Tailwind, Inertia) |
|
|
328
|
+
| ReadLogEntries | Last N log entries (PSR-3 and JSON format) |
|
|
329
|
+
| BrowserLogs | Browser console errors/warnings from `storage/logs/browser.log` |
|
|
330
|
+
| LastError | Last exception from cache or log file |
|
|
331
|
+
| GetAbsoluteUrl | Convert relative paths or named routes to absolute URLs |
|
|
332
|
+
|
|
282
333
|
### Laravel Official Plugins
|
|
283
334
|
|
|
284
335
|
After tool installation, suggest the two official Laravel plugins from Taylor Otwell:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# ShipKit Skill Profiles — Stack-Based Filtering
|
|
2
2
|
|
|
3
|
-
**Last Updated:** 2026-03-
|
|
4
|
-
**Total Skills:** 44 | **Agents:** 13 | **Rules:** 6
|
|
3
|
+
**Last Updated:** 2026-03-30
|
|
4
|
+
**Total Skills:** 44 | **Agents:** 13 | **Rules:** 6 | **Project MCP:** 1
|
|
5
5
|
|
|
6
|
-
This file is the source of truth for which skills, agents, and
|
|
6
|
+
This file is the source of truth for which skills, agents, rules, and project-level MCP servers get installed per project. Read by `sk:setup-claude` (initial setup) and `sk:setup-optimizer` (ongoing sync).
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -179,6 +179,24 @@ These are installed universally because they run inside `sk:gates`. Perf auto-sk
|
|
|
179
179
|
|
|
180
180
|
---
|
|
181
181
|
|
|
182
|
+
## Project-Level MCP Server → Stack Mapping
|
|
183
|
+
|
|
184
|
+
MCP servers configured in the project's `.mcp.json` (not global `~/.mcp.json`). Managed by `sk:setup-claude` (initial setup) and `sk:setup-optimizer` (ongoing sync).
|
|
185
|
+
|
|
186
|
+
| MCP Server | Stack | Command | Purpose |
|
|
187
|
+
|-----------|-------|---------|---------|
|
|
188
|
+
| laravel-boost | laravel | `php artisan boost:mcp` (or `vendor/bin/sail artisan boost:mcp` for Sail) | Database schema, queries, docs search, logs, browser errors |
|
|
189
|
+
|
|
190
|
+
**Sync rules:**
|
|
191
|
+
- **Add** to `.mcp.json` when stack matches and entry is missing
|
|
192
|
+
- **Remove** from `.mcp.json` when stack no longer matches (e.g., project switched from Laravel to Next.js)
|
|
193
|
+
- **Update** existing entry if command is stale (e.g., Sail added/removed — switch between `php` and `vendor/bin/sail`)
|
|
194
|
+
- Never touch MCP entries not in this table (user-added entries are preserved)
|
|
195
|
+
- Sail detection: use `vendor/bin/sail` command variant if `vendor/bin/sail` exists
|
|
196
|
+
- **Ownership:** Project-level MCP is managed by `sk:setup-claude` (initial) and `sk:setup-optimizer` Step 0.5 (ongoing). Step 1.7 handles only global MCP.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
182
200
|
## Installation Formula
|
|
183
201
|
|
|
184
202
|
For a given project with detected `stack` and `capabilities`:
|
|
@@ -196,6 +214,10 @@ installed_agents = universal_agents
|
|
|
196
214
|
|
|
197
215
|
installed_rules = universal_rules (tests.md)
|
|
198
216
|
+ stack_matching_rules(stack, capabilities)
|
|
217
|
+
|
|
218
|
+
project_mcp = stack_matching_mcp(stack)
|
|
219
|
+
# Add matching entries to .mcp.json
|
|
220
|
+
# Remove non-matching entries from .mcp.json (only managed entries — never touch user-added)
|
|
199
221
|
```
|
|
200
222
|
|
|
201
223
|
User overrides (`extra`, `disabled`) are never touched by auto-detection.
|
|
Binary file
|
|
@@ -58,6 +58,14 @@ def _has_dep(pkg: dict, name: str) -> bool:
|
|
|
58
58
|
return name in deps
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _has_composer_dep(composer: dict, name: str) -> bool:
|
|
62
|
+
"""Check if a Composer package is in require or require-dev."""
|
|
63
|
+
deps = {}
|
|
64
|
+
for key in ("require", "require-dev"):
|
|
65
|
+
deps.update(composer.get(key, {}) or {})
|
|
66
|
+
return name in deps
|
|
67
|
+
|
|
68
|
+
|
|
61
69
|
def _any_dep_prefix(pkg: dict, prefix: str) -> bool:
|
|
62
70
|
deps = {}
|
|
63
71
|
for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
|
|
@@ -107,13 +115,16 @@ def _write_cached_detection(repo_root: Path, detection: Detection) -> None:
|
|
|
107
115
|
|
|
108
116
|
def detect(repo_root: Path) -> Detection:
|
|
109
117
|
package_json = _read_json(repo_root / "package.json") or {}
|
|
118
|
+
composer_json = _read_json(repo_root / "composer.json") or {}
|
|
110
119
|
scripts = (package_json.get("scripts") or {}) if isinstance(package_json, dict) else {}
|
|
111
120
|
|
|
112
121
|
project_name = str(package_json.get("name") or repo_root.name)
|
|
113
122
|
description = str(package_json.get("description") or "Project instructions for Claude Code.")
|
|
114
123
|
|
|
115
|
-
# Language
|
|
116
|
-
if (
|
|
124
|
+
# Language — composer.json with laravel/framework takes priority over package.json
|
|
125
|
+
if _has_composer_dep(composer_json, "laravel/framework"):
|
|
126
|
+
language = "PHP"
|
|
127
|
+
elif (repo_root / "tsconfig.json").exists() or _has_dep(package_json, "typescript"):
|
|
117
128
|
language = "TypeScript"
|
|
118
129
|
elif (repo_root / "package.json").exists():
|
|
119
130
|
language = "JavaScript"
|
|
@@ -129,7 +140,21 @@ def detect(repo_root: Path) -> Detection:
|
|
|
129
140
|
language = "Unknown"
|
|
130
141
|
|
|
131
142
|
# Framework (keep simple; expand later)
|
|
132
|
-
|
|
143
|
+
is_laravel = _has_composer_dep(composer_json, "laravel/framework")
|
|
144
|
+
if is_laravel:
|
|
145
|
+
# Sub-detect Laravel stack flavor
|
|
146
|
+
if _has_composer_dep(composer_json, "inertiajs/inertia-laravel"):
|
|
147
|
+
if _has_dep(package_json, "react"):
|
|
148
|
+
framework = "Laravel (Inertia + React)"
|
|
149
|
+
elif _has_dep(package_json, "vue"):
|
|
150
|
+
framework = "Laravel (Inertia + Vue)"
|
|
151
|
+
else:
|
|
152
|
+
framework = "Laravel (Inertia)"
|
|
153
|
+
elif _has_composer_dep(composer_json, "livewire/livewire"):
|
|
154
|
+
framework = "Laravel (Livewire)"
|
|
155
|
+
else:
|
|
156
|
+
framework = "Laravel (API)"
|
|
157
|
+
elif _has_dep(package_json, "next"):
|
|
133
158
|
framework = "Next.js (App Router)"
|
|
134
159
|
elif _has_dep(package_json, "react"):
|
|
135
160
|
framework = "React"
|
|
@@ -139,7 +164,9 @@ def detect(repo_root: Path) -> Detection:
|
|
|
139
164
|
framework = "Unknown"
|
|
140
165
|
|
|
141
166
|
# Database / ORM
|
|
142
|
-
if
|
|
167
|
+
if is_laravel and (repo_root / "database" / "migrations").exists():
|
|
168
|
+
database = "Eloquent ORM"
|
|
169
|
+
elif _has_dep(package_json, "drizzle-orm"):
|
|
143
170
|
if _has_dep(package_json, "better-sqlite3"):
|
|
144
171
|
database = "Drizzle ORM + SQLite"
|
|
145
172
|
elif _has_dep(package_json, "pg"):
|
|
@@ -165,7 +192,9 @@ def detect(repo_root: Path) -> Detection:
|
|
|
165
192
|
ui = "Unknown"
|
|
166
193
|
|
|
167
194
|
# Testing
|
|
168
|
-
if
|
|
195
|
+
if _has_composer_dep(composer_json, "pestphp/pest"):
|
|
196
|
+
testing = "Pest"
|
|
197
|
+
elif _has_dep(package_json, "vitest"):
|
|
169
198
|
testing = "Vitest"
|
|
170
199
|
elif _has_dep(package_json, "jest"):
|
|
171
200
|
testing = "Jest"
|
|
@@ -190,10 +219,16 @@ def detect(repo_root: Path) -> Detection:
|
|
|
190
219
|
else:
|
|
191
220
|
browser_automation = "None"
|
|
192
221
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
222
|
+
if is_laravel:
|
|
223
|
+
dev_cmd = "php artisan serve"
|
|
224
|
+
build_cmd = "npm run build"
|
|
225
|
+
lint_cmd = "vendor/bin/pint"
|
|
226
|
+
test_cmd = "vendor/bin/pest"
|
|
227
|
+
else:
|
|
228
|
+
dev_cmd = scripts.get("dev") and "npm run dev" or "npm run dev"
|
|
229
|
+
build_cmd = scripts.get("build") and "npm run build" or "npm run build"
|
|
230
|
+
lint_cmd = scripts.get("lint") and "npm run lint" or "npm run lint"
|
|
231
|
+
test_cmd = scripts.get("test") and "npm test" or "npm test"
|
|
197
232
|
|
|
198
233
|
typo_dir = repo_root / ".claude" / "docs" / "achritectural_change_log"
|
|
199
234
|
correct_dir = repo_root / ".claude" / "docs" / "architectural_change_log"
|
|
@@ -413,7 +448,7 @@ def _rules_filter(detection: Detection):
|
|
|
413
448
|
def _filter(filename: str) -> bool:
|
|
414
449
|
if filename in always:
|
|
415
450
|
return True
|
|
416
|
-
if filename == "laravel.md.template" and "Laravel"
|
|
451
|
+
if filename == "laravel.md.template" and detection.framework.startswith("Laravel"):
|
|
417
452
|
return True
|
|
418
453
|
if filename == "react.md.template" and (
|
|
419
454
|
"React" in detection.framework or detection.framework == "Next.js (App Router)"
|
|
@@ -424,6 +459,67 @@ def _rules_filter(detection: Detection):
|
|
|
424
459
|
return _filter
|
|
425
460
|
|
|
426
461
|
|
|
462
|
+
# Stack-conditional MCP server mapping (managed entries only)
|
|
463
|
+
_MANAGED_MCP_SERVERS: Dict[str, dict] = {
|
|
464
|
+
"laravel-boost": {
|
|
465
|
+
"stack_prefix": "Laravel",
|
|
466
|
+
"command": "php",
|
|
467
|
+
"args": ["artisan", "boost:mcp"],
|
|
468
|
+
"sail_command": "vendor/bin/sail",
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _deploy_project_mcp(
|
|
474
|
+
repo_root: Path,
|
|
475
|
+
detection: Detection,
|
|
476
|
+
*,
|
|
477
|
+
dry_run: bool,
|
|
478
|
+
) -> List[tuple]:
|
|
479
|
+
"""Add/remove/update managed MCP entries in .mcp.json based on detected stack."""
|
|
480
|
+
mcp_path = repo_root / ".mcp.json"
|
|
481
|
+
results: List[tuple] = []
|
|
482
|
+
|
|
483
|
+
existing = _read_json(mcp_path) or {}
|
|
484
|
+
servers = existing.get("mcpServers", {})
|
|
485
|
+
changed = False
|
|
486
|
+
|
|
487
|
+
for name, config in _MANAGED_MCP_SERVERS.items():
|
|
488
|
+
stack_matches = detection.framework.startswith(config["stack_prefix"])
|
|
489
|
+
use_sail = (repo_root / "vendor" / "bin" / "sail").exists()
|
|
490
|
+
command = config["sail_command"] if use_sail else config["command"]
|
|
491
|
+
expected_entry = {"command": command, "args": config["args"], "env": {}}
|
|
492
|
+
|
|
493
|
+
if stack_matches:
|
|
494
|
+
if name not in servers:
|
|
495
|
+
servers[name] = expected_entry
|
|
496
|
+
changed = True
|
|
497
|
+
elif servers[name].get("command") != command:
|
|
498
|
+
# Sail status changed — update command
|
|
499
|
+
servers[name]["command"] = command
|
|
500
|
+
changed = True
|
|
501
|
+
else:
|
|
502
|
+
if name in servers:
|
|
503
|
+
del servers[name]
|
|
504
|
+
changed = True
|
|
505
|
+
|
|
506
|
+
if not changed:
|
|
507
|
+
results.append(("skipped", mcp_path))
|
|
508
|
+
return results
|
|
509
|
+
|
|
510
|
+
if dry_run:
|
|
511
|
+
action = "updated" if mcp_path.exists() else "created"
|
|
512
|
+
results.append((action, mcp_path))
|
|
513
|
+
return results
|
|
514
|
+
|
|
515
|
+
existing["mcpServers"] = servers
|
|
516
|
+
mcp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
517
|
+
mcp_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
518
|
+
action = "updated" if mcp_path.exists() else "created"
|
|
519
|
+
results.append((action, mcp_path))
|
|
520
|
+
return results
|
|
521
|
+
|
|
522
|
+
|
|
427
523
|
def apply(
|
|
428
524
|
repo_root: Path,
|
|
429
525
|
skill_root: Path,
|
|
@@ -571,6 +667,10 @@ def apply(
|
|
|
571
667
|
action_sl = "created"
|
|
572
668
|
_collect_results([(action_sl, statusline_dest)], repo_root, created, updated, skipped)
|
|
573
669
|
|
|
670
|
+
# --- Deploy project-level .mcp.json (stack-conditional) ---
|
|
671
|
+
mcp_results = _deploy_project_mcp(repo_root, detection, dry_run=dry_run)
|
|
672
|
+
_collect_results(mcp_results, repo_root, created, updated, skipped)
|
|
673
|
+
|
|
574
674
|
if dry_run:
|
|
575
675
|
print("setup-claude dry-run complete (no files written)")
|
|
576
676
|
else:
|
|
@@ -19,6 +19,20 @@ paths:
|
|
|
19
19
|
- **Config**: Access config via `config()` helper, never `env()` outside config files.
|
|
20
20
|
- **Strict mode**: Models use strict mode (prevent lazy loading, silently discarding attributes, accessing missing attributes).
|
|
21
21
|
|
|
22
|
+
## Laravel Boost MCP
|
|
23
|
+
|
|
24
|
+
Project MCP server auto-configured in `.mcp.json` by `/sk:laravel-init`. Provides 9 tools Claude can call directly:
|
|
25
|
+
|
|
26
|
+
| Tool | When to use |
|
|
27
|
+
|------|-------------|
|
|
28
|
+
| `DatabaseSchema` | Before writing migrations or queries — get exact column types, indexes, FKs |
|
|
29
|
+
| `DatabaseQuery` | Run read-only SELECTs to validate data assumptions |
|
|
30
|
+
| `SearchDocs` | Look up Laravel/Livewire/Filament/Pest docs matched to your installed versions |
|
|
31
|
+
| `ReadLogEntries` | Check recent errors without leaving Claude |
|
|
32
|
+
| `BrowserLogs` | Inspect browser console errors captured server-side |
|
|
33
|
+
| `LastError` | Get the last exception instantly |
|
|
34
|
+
| `ApplicationInfo` | Check installed packages and Eloquent models at session start |
|
|
35
|
+
|
|
22
36
|
## Code Refinement
|
|
23
37
|
|
|
24
38
|
After running `/sk:execute-plan`, use the `laravel-simplifier` agent to refine recently modified PHP/Laravel code:
|
|
Binary file
|
|
@@ -158,5 +158,272 @@ class TestApplySetupClaude(unittest.TestCase):
|
|
|
158
158
|
self.assertIn("Notes:", buf.getvalue())
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def test_laravel_detection_inertia_react(self):
|
|
162
|
+
mod = _load_apply_module()
|
|
163
|
+
|
|
164
|
+
with tempfile.TemporaryDirectory() as td:
|
|
165
|
+
repo_root = Path(td)
|
|
166
|
+
(repo_root / "composer.json").write_text(
|
|
167
|
+
json.dumps({
|
|
168
|
+
"require": {"laravel/framework": "^12.0"},
|
|
169
|
+
"require-dev": {
|
|
170
|
+
"pestphp/pest": "^3.0",
|
|
171
|
+
"inertiajs/inertia-laravel": "^2.0",
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
encoding="utf-8",
|
|
175
|
+
)
|
|
176
|
+
(repo_root / "package.json").write_text(
|
|
177
|
+
json.dumps({"dependencies": {"react": "^19.0"}}),
|
|
178
|
+
encoding="utf-8",
|
|
179
|
+
)
|
|
180
|
+
(repo_root / "database" / "migrations").mkdir(parents=True)
|
|
181
|
+
|
|
182
|
+
detection = mod.detect(repo_root)
|
|
183
|
+
self.assertEqual(detection.framework, "Laravel (Inertia + React)")
|
|
184
|
+
self.assertEqual(detection.language, "PHP")
|
|
185
|
+
self.assertEqual(detection.database, "Eloquent ORM")
|
|
186
|
+
self.assertEqual(detection.testing, "Pest")
|
|
187
|
+
self.assertEqual(detection.dev_cmd, "php artisan serve")
|
|
188
|
+
self.assertEqual(detection.lint_cmd, "vendor/bin/pint")
|
|
189
|
+
self.assertEqual(detection.test_cmd, "vendor/bin/pest")
|
|
190
|
+
|
|
191
|
+
def test_laravel_detection_livewire(self):
|
|
192
|
+
mod = _load_apply_module()
|
|
193
|
+
|
|
194
|
+
with tempfile.TemporaryDirectory() as td:
|
|
195
|
+
repo_root = Path(td)
|
|
196
|
+
(repo_root / "composer.json").write_text(
|
|
197
|
+
json.dumps({
|
|
198
|
+
"require": {
|
|
199
|
+
"laravel/framework": "^12.0",
|
|
200
|
+
"livewire/livewire": "^3.0",
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
encoding="utf-8",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
detection = mod.detect(repo_root)
|
|
207
|
+
self.assertEqual(detection.framework, "Laravel (Livewire)")
|
|
208
|
+
|
|
209
|
+
def test_laravel_detection_api_only(self):
|
|
210
|
+
mod = _load_apply_module()
|
|
211
|
+
|
|
212
|
+
with tempfile.TemporaryDirectory() as td:
|
|
213
|
+
repo_root = Path(td)
|
|
214
|
+
(repo_root / "composer.json").write_text(
|
|
215
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
216
|
+
encoding="utf-8",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
detection = mod.detect(repo_root)
|
|
220
|
+
self.assertEqual(detection.framework, "Laravel (API)")
|
|
221
|
+
|
|
222
|
+
def test_mcp_json_created_for_laravel(self):
|
|
223
|
+
mod = _load_apply_module()
|
|
224
|
+
skill_root = Path(__file__).resolve().parents[1]
|
|
225
|
+
|
|
226
|
+
with tempfile.TemporaryDirectory() as td:
|
|
227
|
+
repo_root = Path(td)
|
|
228
|
+
(repo_root / "composer.json").write_text(
|
|
229
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
230
|
+
encoding="utf-8",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
buf = io.StringIO()
|
|
234
|
+
with contextlib.redirect_stdout(buf):
|
|
235
|
+
mod.apply(
|
|
236
|
+
repo_root,
|
|
237
|
+
skill_root,
|
|
238
|
+
update_generated=False,
|
|
239
|
+
dry_run=False,
|
|
240
|
+
detection=mod.detect(repo_root),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
mcp_path = repo_root / ".mcp.json"
|
|
244
|
+
self.assertTrue(mcp_path.exists())
|
|
245
|
+
mcp_data = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
246
|
+
self.assertIn("laravel-boost", mcp_data["mcpServers"])
|
|
247
|
+
self.assertEqual(mcp_data["mcpServers"]["laravel-boost"]["command"], "php")
|
|
248
|
+
|
|
249
|
+
def test_mcp_json_not_created_for_nextjs(self):
|
|
250
|
+
mod = _load_apply_module()
|
|
251
|
+
skill_root = Path(__file__).resolve().parents[1]
|
|
252
|
+
|
|
253
|
+
with tempfile.TemporaryDirectory() as td:
|
|
254
|
+
repo_root = Path(td)
|
|
255
|
+
(repo_root / "package.json").write_text(
|
|
256
|
+
json.dumps({
|
|
257
|
+
"name": "demo",
|
|
258
|
+
"dependencies": {"next": "15.0.0", "react": "19.0.0"},
|
|
259
|
+
}),
|
|
260
|
+
encoding="utf-8",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
buf = io.StringIO()
|
|
264
|
+
with contextlib.redirect_stdout(buf):
|
|
265
|
+
mod.apply(
|
|
266
|
+
repo_root,
|
|
267
|
+
skill_root,
|
|
268
|
+
update_generated=False,
|
|
269
|
+
dry_run=False,
|
|
270
|
+
detection=mod.detect(repo_root),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
mcp_path = repo_root / ".mcp.json"
|
|
274
|
+
if mcp_path.exists():
|
|
275
|
+
mcp_data = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
276
|
+
self.assertNotIn("laravel-boost", mcp_data.get("mcpServers", {}))
|
|
277
|
+
|
|
278
|
+
def test_mcp_json_removed_when_stack_changes(self):
|
|
279
|
+
mod = _load_apply_module()
|
|
280
|
+
skill_root = Path(__file__).resolve().parents[1]
|
|
281
|
+
|
|
282
|
+
with tempfile.TemporaryDirectory() as td:
|
|
283
|
+
repo_root = Path(td)
|
|
284
|
+
|
|
285
|
+
# Start with Laravel — creates .mcp.json with laravel-boost
|
|
286
|
+
(repo_root / "composer.json").write_text(
|
|
287
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
288
|
+
encoding="utf-8",
|
|
289
|
+
)
|
|
290
|
+
laravel_detection = mod.detect(repo_root)
|
|
291
|
+
|
|
292
|
+
buf = io.StringIO()
|
|
293
|
+
with contextlib.redirect_stdout(buf):
|
|
294
|
+
mod.apply(
|
|
295
|
+
repo_root,
|
|
296
|
+
skill_root,
|
|
297
|
+
update_generated=False,
|
|
298
|
+
dry_run=False,
|
|
299
|
+
detection=laravel_detection,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
mcp_path = repo_root / ".mcp.json"
|
|
303
|
+
self.assertTrue(mcp_path.exists())
|
|
304
|
+
mcp_data = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
305
|
+
self.assertIn("laravel-boost", mcp_data["mcpServers"])
|
|
306
|
+
|
|
307
|
+
# Switch to Next.js — laravel-boost should be removed
|
|
308
|
+
(repo_root / "composer.json").unlink()
|
|
309
|
+
(repo_root / "package.json").write_text(
|
|
310
|
+
json.dumps({
|
|
311
|
+
"name": "demo",
|
|
312
|
+
"dependencies": {"next": "15.0.0", "react": "19.0.0"},
|
|
313
|
+
}),
|
|
314
|
+
encoding="utf-8",
|
|
315
|
+
)
|
|
316
|
+
nextjs_detection = mod.detect(repo_root)
|
|
317
|
+
|
|
318
|
+
buf = io.StringIO()
|
|
319
|
+
with contextlib.redirect_stdout(buf):
|
|
320
|
+
mod.apply(
|
|
321
|
+
repo_root,
|
|
322
|
+
skill_root,
|
|
323
|
+
update_generated=False,
|
|
324
|
+
dry_run=False,
|
|
325
|
+
detection=nextjs_detection,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
mcp_data = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
329
|
+
self.assertNotIn("laravel-boost", mcp_data.get("mcpServers", {}))
|
|
330
|
+
|
|
331
|
+
def test_mcp_json_sail_detection(self):
|
|
332
|
+
mod = _load_apply_module()
|
|
333
|
+
skill_root = Path(__file__).resolve().parents[1]
|
|
334
|
+
|
|
335
|
+
with tempfile.TemporaryDirectory() as td:
|
|
336
|
+
repo_root = Path(td)
|
|
337
|
+
(repo_root / "composer.json").write_text(
|
|
338
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
339
|
+
encoding="utf-8",
|
|
340
|
+
)
|
|
341
|
+
# Simulate Sail being present
|
|
342
|
+
(repo_root / "vendor" / "bin").mkdir(parents=True)
|
|
343
|
+
(repo_root / "vendor" / "bin" / "sail").write_text("#!/bin/sh\n", encoding="utf-8")
|
|
344
|
+
|
|
345
|
+
buf = io.StringIO()
|
|
346
|
+
with contextlib.redirect_stdout(buf):
|
|
347
|
+
mod.apply(
|
|
348
|
+
repo_root,
|
|
349
|
+
skill_root,
|
|
350
|
+
update_generated=False,
|
|
351
|
+
dry_run=False,
|
|
352
|
+
detection=mod.detect(repo_root),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
mcp_path = repo_root / ".mcp.json"
|
|
356
|
+
mcp_data = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
357
|
+
self.assertEqual(
|
|
358
|
+
mcp_data["mcpServers"]["laravel-boost"]["command"],
|
|
359
|
+
"vendor/bin/sail",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def test_mcp_json_preserves_user_entries(self):
|
|
363
|
+
mod = _load_apply_module()
|
|
364
|
+
skill_root = Path(__file__).resolve().parents[1]
|
|
365
|
+
|
|
366
|
+
with tempfile.TemporaryDirectory() as td:
|
|
367
|
+
repo_root = Path(td)
|
|
368
|
+
(repo_root / "composer.json").write_text(
|
|
369
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
370
|
+
encoding="utf-8",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Pre-existing user MCP entry
|
|
374
|
+
(repo_root / ".mcp.json").write_text(
|
|
375
|
+
json.dumps({
|
|
376
|
+
"mcpServers": {
|
|
377
|
+
"my-custom-server": {"command": "node", "args": ["server.js"]},
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
encoding="utf-8",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
buf = io.StringIO()
|
|
384
|
+
with contextlib.redirect_stdout(buf):
|
|
385
|
+
mod.apply(
|
|
386
|
+
repo_root,
|
|
387
|
+
skill_root,
|
|
388
|
+
update_generated=False,
|
|
389
|
+
dry_run=False,
|
|
390
|
+
detection=mod.detect(repo_root),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
mcp_data = json.loads((repo_root / ".mcp.json").read_text(encoding="utf-8"))
|
|
394
|
+
self.assertIn("my-custom-server", mcp_data["mcpServers"])
|
|
395
|
+
self.assertIn("laravel-boost", mcp_data["mcpServers"])
|
|
396
|
+
|
|
397
|
+
def test_laravel_rules_filter(self):
|
|
398
|
+
mod = _load_apply_module()
|
|
399
|
+
|
|
400
|
+
with tempfile.TemporaryDirectory() as td:
|
|
401
|
+
repo_root = Path(td)
|
|
402
|
+
(repo_root / "composer.json").write_text(
|
|
403
|
+
json.dumps({"require": {"laravel/framework": "^12.0"}}),
|
|
404
|
+
encoding="utf-8",
|
|
405
|
+
)
|
|
406
|
+
detection = mod.detect(repo_root)
|
|
407
|
+
rule_filter = mod._rules_filter(detection)
|
|
408
|
+
self.assertTrue(rule_filter("laravel.md.template"))
|
|
409
|
+
self.assertTrue(rule_filter("tests.md.template"))
|
|
410
|
+
|
|
411
|
+
def test_nextjs_rules_filter_excludes_laravel(self):
|
|
412
|
+
mod = _load_apply_module()
|
|
413
|
+
|
|
414
|
+
with tempfile.TemporaryDirectory() as td:
|
|
415
|
+
repo_root = Path(td)
|
|
416
|
+
(repo_root / "package.json").write_text(
|
|
417
|
+
json.dumps({
|
|
418
|
+
"dependencies": {"next": "15.0.0", "react": "19.0.0"},
|
|
419
|
+
}),
|
|
420
|
+
encoding="utf-8",
|
|
421
|
+
)
|
|
422
|
+
detection = mod.detect(repo_root)
|
|
423
|
+
rule_filter = mod._rules_filter(detection)
|
|
424
|
+
self.assertFalse(rule_filter("laravel.md.template"))
|
|
425
|
+
self.assertTrue(rule_filter("react.md.template"))
|
|
426
|
+
|
|
427
|
+
|
|
161
428
|
if __name__ == "__main__":
|
|
162
429
|
unittest.main()
|
|
@@ -89,6 +89,10 @@ Rule changes:
|
|
|
89
89
|
+ migrations.md (database paths)
|
|
90
90
|
No removals.
|
|
91
91
|
|
|
92
|
+
Project MCP changes:
|
|
93
|
+
- laravel-boost (stack is no longer laravel)
|
|
94
|
+
No additions.
|
|
95
|
+
|
|
92
96
|
Apply changes? (y/n)
|
|
93
97
|
```
|
|
94
98
|
|
|
@@ -114,6 +118,13 @@ If the user confirms:
|
|
|
114
118
|
- Remove stale rules: delete from `.claude/rules/` in the project if they no longer match
|
|
115
119
|
- Never remove user-customized rules (same detection as agents)
|
|
116
120
|
|
|
121
|
+
**Project-level MCP sync** (sole owner of `.mcp.json` managed entries — Step 1.7 only handles global MCP):
|
|
122
|
+
- Read the MCP Server → Stack Mapping from `skill-profiles.md`
|
|
123
|
+
- **Add:** MCP entries to `.mcp.json` when stack matches and entry is missing
|
|
124
|
+
- **Remove:** MCP entries from `.mcp.json` when stack no longer matches (e.g., `laravel-boost` removed if stack changed from Laravel to Next.js). Only remove entries whose key matches the mapping table — never touch other entries.
|
|
125
|
+
- **Update:** If entry exists but command is stale (e.g., Sail added/removed since last setup — `vendor/bin/sail` exists but entry uses `php`, or vice versa), update the command to match current state
|
|
126
|
+
- For Laravel Boost Sail detection: use `vendor/bin/sail` command variant if `vendor/bin/sail` exists in the project
|
|
127
|
+
|
|
117
128
|
**Config update:**
|
|
118
129
|
- Update `.shipkit/config.json` with new `stack.detected`, `stack.detected_at`, `stack.capabilities`
|
|
119
130
|
|
|
@@ -245,9 +256,11 @@ After hooks deployment, check and configure LSP tooling:
|
|
|
245
256
|
|
|
246
257
|
**Idempotency:** Never overwrite existing hook files — the user may have customized them. Only deploy hooks that don't exist yet. For settings.json, merge additively.
|
|
247
258
|
|
|
248
|
-
### Step 1.7: MCP Servers & Plugin Check
|
|
259
|
+
### Step 1.7: Global MCP Servers & Plugin Check
|
|
260
|
+
|
|
261
|
+
After LSP check, verify the three recommended **global** tools are configured.
|
|
249
262
|
|
|
250
|
-
|
|
263
|
+
> **Note:** Project-level MCP (`.mcp.json`) is managed exclusively by Step 0.5 during stack sync. This step only handles global MCP/plugins.
|
|
251
264
|
|
|
252
265
|
1. **Sequential Thinking MCP** — grep `~/.mcp.json` for `sequential-thinking`
|
|
253
266
|
2. **Context7 plugin** — grep `~/.claude/settings.json` for `context7@claude-plugins-official` in `enabledPlugins`
|
|
@@ -255,7 +268,7 @@ After LSP check, verify the three recommended tools are configured:
|
|
|
255
268
|
|
|
256
269
|
**Report status and prompt:**
|
|
257
270
|
|
|
258
|
-
> "MCP/Plugins: [X/3] configured
|
|
271
|
+
> "Global MCP/Plugins: [X/3] configured
|
|
259
272
|
> sequential-thinking: [✓ configured / ✗ missing]
|
|
260
273
|
> context7: [✓ configured / ✗ missing]
|
|
261
274
|
> ccstatusline: [✓ configured / ✗ missing]
|