@kennethsolomon/shipkit 3.19.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.
Files changed (32) hide show
  1. package/README.md +36 -4
  2. package/package.json +1 -1
  3. package/skills/sk:brainstorming/SKILL.md +19 -128
  4. package/skills/sk:debug/SKILL.md +44 -111
  5. package/skills/sk:e2e/SKILL.md +45 -97
  6. package/skills/sk:features/SKILL.md +44 -99
  7. package/skills/sk:frontend-design/SKILL.md +16 -32
  8. package/skills/sk:laravel-init/SKILL.md +8 -7
  9. package/skills/sk:laravel-new/SKILL.md +1 -0
  10. package/skills/sk:lint/SKILL.md +42 -62
  11. package/skills/sk:mvp/SKILL.md +81 -134
  12. package/skills/sk:perf/SKILL.md +24 -43
  13. package/skills/sk:review/SKILL.md +57 -93
  14. package/skills/sk:security-check/SKILL.md +37 -43
  15. package/skills/sk:seo-audit/SKILL.md +75 -96
  16. package/skills/sk:setup-claude/SKILL.md +154 -0
  17. package/skills/sk:setup-claude/references/skill-profiles.md +223 -0
  18. package/skills/sk:setup-claude/scripts/__pycache__/apply_setup_claude.cpython-314.pyc +0 -0
  19. package/skills/sk:setup-claude/scripts/apply_setup_claude.py +110 -10
  20. package/skills/sk:setup-claude/templates/.claude/rules/laravel.md.template +14 -0
  21. package/skills/sk:setup-claude/templates/CLAUDE.md.template +102 -247
  22. package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +1 -1
  23. package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +1 -1
  24. package/skills/sk:setup-claude/templates/commands/finish-feature.md.template +1 -1
  25. package/skills/sk:setup-claude/templates/commands/security-check.md.template +1 -1
  26. package/skills/sk:setup-claude/templates/commands/write-plan.md.template +1 -1
  27. package/skills/sk:setup-claude/tests/__pycache__/test_apply_setup_claude.cpython-314.pyc +0 -0
  28. package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +267 -0
  29. package/skills/sk:setup-optimizer/SKILL.md +101 -17
  30. package/skills/sk:skill-creator/SKILL.md +115 -226
  31. package/skills/sk:website/SKILL.md +81 -149
  32. package/skills/sk:write-tests/SKILL.md +44 -110
@@ -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()
@@ -55,6 +55,89 @@ Before making any changes, runs a diagnostic pass on the existing CLAUDE.md:
55
55
 
56
56
  Reports findings before proceeding. If issues are found, they inform subsequent steps.
57
57
 
58
+ ### Step 0.5: Re-detect Stack + Sync Skills/Agents/Rules
59
+
60
+ After diagnosis, re-detect the project stack and sync installed skills, agents, and rules.
61
+
62
+ **Reference:** Read `~/.claude/skills/sk:setup-claude/references/skill-profiles.md` for the categorization matrix.
63
+
64
+ #### 1. Re-detect stack
65
+
66
+ Run the same detection logic as `sk:setup-claude` Phase 0.5:
67
+ - Scan for stack indicators (composer.json, package.json, go.mod, etc.)
68
+ - Sub-detect database capability (Prisma, Drizzle, Laravel migrations, etc.)
69
+ - Compare new detection against `.shipkit/config.json` current values
70
+
71
+ #### 2. Diff and display changes
72
+
73
+ If the detected stack or capabilities changed, display a diff:
74
+
75
+ ```
76
+ Stack re-detection:
77
+ Stack: nextjs (unchanged)
78
+ Capabilities: web → web, database (prisma/schema.prisma detected)
79
+
80
+ Skill changes:
81
+ + sk:schema-migrate (database capability detected)
82
+ No removals.
83
+
84
+ Agent changes:
85
+ + database-architect (database capability detected)
86
+ No removals.
87
+
88
+ Rule changes:
89
+ + migrations.md (database paths)
90
+ No removals.
91
+
92
+ Project MCP changes:
93
+ - laravel-boost (stack is no longer laravel)
94
+ No additions.
95
+
96
+ Apply changes? (y/n)
97
+ ```
98
+
99
+ If no changes detected, report `Stack: [stack] — no changes detected` and skip to Step 1.
100
+
101
+ #### 3. Sync on confirmation
102
+
103
+ If the user confirms:
104
+
105
+ **Skills sync:**
106
+ - Add newly relevant skills: copy from `~/.claude/skills/` to `.claude/skills/` in the project
107
+ - Remove stale skills: delete from `.claude/skills/` in the project if they no longer match the detected stack
108
+ - Never touch skills in `config.skills.extra` (user manually added)
109
+ - Never touch skills in `config.skills.disabled` (user manually excluded)
110
+
111
+ **Agent sync:**
112
+ - Add newly relevant agents: copy from `~/.claude/agents/` to `.claude/agents/` in the project
113
+ - Remove stale agents: delete from `.claude/agents/` in the project if they no longer match
114
+ - Never remove user-customized agents (detect via content that differs from the template — check if file hash differs from template hash, or if file contains `<!-- EDITED -->` marker)
115
+
116
+ **Rule sync:**
117
+ - Add newly relevant rules: copy from `~/.claude/rules/` to `.claude/rules/` in the project
118
+ - Remove stale rules: delete from `.claude/rules/` in the project if they no longer match
119
+ - Never remove user-customized rules (same detection as agents)
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
+
128
+ **Config update:**
129
+ - Update `.shipkit/config.json` with new `stack.detected`, `stack.detected_at`, `stack.capabilities`
130
+
131
+ **CLAUDE.md commands table:**
132
+ - Regenerate the Commands table to list only currently installed skills
133
+
134
+ #### 4. Upgrade path handling
135
+
136
+ - If project has no `stack` field in config → treat as auto-detect (backwards compatible)
137
+ - If capabilities expanded (e.g., added database) → suggest new skills
138
+ - If capabilities reduced (e.g., removed a dependency) → suggest removing irrelevant skills
139
+ - Display: `Capabilities changed: [old] → [new]. [N] skills affected. Apply? (y/n)`
140
+
58
141
  ### Step 1: Update Workflow
59
142
 
60
143
  If the workflow section is outdated or missing, replace it with the latest version:
@@ -173,9 +256,11 @@ After hooks deployment, check and configure LSP tooling:
173
256
 
174
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.
175
258
 
176
- ### 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.
177
262
 
178
- After LSP check, verify the three recommended tools are configured:
263
+ > **Note:** Project-level MCP (`.mcp.json`) is managed exclusively by Step 0.5 during stack sync. This step only handles global MCP/plugins.
179
264
 
180
265
  1. **Sequential Thinking MCP** — grep `~/.mcp.json` for `sequential-thinking`
181
266
  2. **Context7 plugin** — grep `~/.claude/settings.json` for `context7@claude-plugins-official` in `enabledPlugins`
@@ -183,7 +268,7 @@ After LSP check, verify the three recommended tools are configured:
183
268
 
184
269
  **Report status and prompt:**
185
270
 
186
- > "MCP/Plugins: [X/3] configured
271
+ > "Global MCP/Plugins: [X/3] configured
187
272
  > sequential-thinking: [✓ configured / ✗ missing]
188
273
  > context7: [✓ configured / ✗ missing]
189
274
  > ccstatusline: [✓ configured / ✗ missing]
@@ -200,28 +285,27 @@ After LSP check, verify the three recommended tools are configured:
200
285
 
201
286
  ### Step 1.8: Agents & Rules Check
202
287
 
203
- After MCP check, verify the project has formal agent definitions and path-scoped rules:
288
+ After MCP check, verify the project has the correct agents and rules for its detected stack.
289
+
290
+ **Reference:** Read `~/.claude/skills/sk:setup-claude/references/skill-profiles.md` for agent→stack and rule→stack mappings.
204
291
 
205
292
  **Agents check:**
206
293
 
207
294
  1. Check if `.claude/agents/` directory exists
208
- 2. Check for the 13 core agents:
209
- - **Implementation:** `backend-dev.md`, `frontend-dev.md`, `mobile-dev.md`
210
- - **Quality:** `qa-engineer.md`, `code-reviewer.md`, `security-reviewer.md`, `performance-optimizer.md`
211
- - **Design:** `architect.md`, `database-architect.md`
212
- - **Operations:** `devops-engineer.md`
213
- - **Maintenance:** `debugger.md`, `refactor-specialist.md`, `tech-writer.md`
214
- 3. For each existing agent, check if it has `memory:` and `model:` in frontmatter (older agents may be missing these)
295
+ 2. Read detected stack from `.shipkit/config.json` (or re-detect if not present)
296
+ 3. Using the agent→stack mapping from `skill-profiles.md`, determine which agents this project should have:
297
+ - Universal agents (all projects): architect, qa-engineer, debugger, code-reviewer, security-reviewer, performance-optimizer, refactor-specialist, tech-writer, devops-engineer
298
+ - Stack-specific: backend-dev (backend stacks), frontend-dev (web stacks), mobile-dev (mobile stacks), database-architect (database capability)
299
+ 4. For each expected agent, check if it exists in `.claude/agents/`
300
+ 5. For each existing agent, check if it has `memory:` and `model:` in frontmatter (older agents may be missing these)
215
301
 
216
302
  **Rules check:**
217
303
 
218
304
  1. Check if `.claude/rules/` directory exists
219
- 2. Detect project stack from `CLAUDE.md`, `package.json`, `composer.json`
220
- 3. Check for relevant rule files based on detected stack:
221
- - Laravel/PHP detected check for `laravel.md`, `api.md`, `migrations.md`
222
- - React/Next.js detected check for `react.md`, `tests.md`, `api.md`
223
- - Vue/Nuxt detected → check for `vue.md`, `tests.md`, `api.md`
224
- - Any stack → check for `tests.md`
305
+ 2. Using the rule→stack mapping from `skill-profiles.md`, determine which rules this project should have:
306
+ - Universal rules (all projects): tests.md, api.md
307
+ - Stack-specific: laravel.md (Laravel), react.md (React/Next.js), vue.md (Vue/Nuxt), migrations.md (database capability)
308
+ 3. Check for each expected rule file
225
309
 
226
310
  **Report status and prompt:**
227
311