autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Laravel Skill
|
|
2
|
+
|
|
3
|
+
## Route Definition
|
|
4
|
+
\`\`\`php
|
|
5
|
+
<?php
|
|
6
|
+
// routes/api.php
|
|
7
|
+
|
|
8
|
+
use App\\Http\\Controllers\\Api\\V1\\UserController;
|
|
9
|
+
|
|
10
|
+
Route::prefix('v1')->group(function () {
|
|
11
|
+
// Public routes
|
|
12
|
+
Route::post('auth/login', [AuthController::class, 'login']);
|
|
13
|
+
Route::post('auth/register', [AuthController::class, 'register']);
|
|
14
|
+
|
|
15
|
+
// Protected routes
|
|
16
|
+
Route::middleware('auth:sanctum')->group(function () {
|
|
17
|
+
Route::apiResource('users', UserController::class);
|
|
18
|
+
Route::get('users/{user}/posts', [UserController::class, 'posts']);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
\`\`\`
|
|
22
|
+
|
|
23
|
+
## Controller
|
|
24
|
+
\`\`\`php
|
|
25
|
+
<?php
|
|
26
|
+
|
|
27
|
+
namespace App\\Http\\Controllers\\Api\\V1;
|
|
28
|
+
|
|
29
|
+
use App\\Http\\Controllers\\Controller;
|
|
30
|
+
use App\\Http\\Requests\\CreateUserRequest;
|
|
31
|
+
use App\\Http\\Requests\\UpdateUserRequest;
|
|
32
|
+
use App\\Http\\Resources\\UserResource;
|
|
33
|
+
use App\\Http\\Resources\\UserCollection;
|
|
34
|
+
use App\\Models\\User;
|
|
35
|
+
use Illuminate\\Http\\JsonResponse;
|
|
36
|
+
use Illuminate\\Http\\Response;
|
|
37
|
+
|
|
38
|
+
final class UserController extends Controller
|
|
39
|
+
{
|
|
40
|
+
public function index(): UserCollection
|
|
41
|
+
{
|
|
42
|
+
$users = User::query()
|
|
43
|
+
->where('is_active', true)
|
|
44
|
+
->latest()
|
|
45
|
+
->paginate();
|
|
46
|
+
|
|
47
|
+
return new UserCollection($users);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public function show(User $user): UserResource
|
|
51
|
+
{
|
|
52
|
+
return new UserResource($user->load(['profile', 'roles']));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public function store(CreateUserRequest $request): JsonResponse
|
|
56
|
+
{
|
|
57
|
+
$user = User::create([
|
|
58
|
+
...$request->validated(),
|
|
59
|
+
'password' => bcrypt($request->password),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
return (new UserResource($user))
|
|
63
|
+
->response()
|
|
64
|
+
->setStatusCode(Response::HTTP_CREATED);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public function update(UpdateUserRequest $request, User $user): UserResource
|
|
68
|
+
{
|
|
69
|
+
$user->update($request->validated());
|
|
70
|
+
|
|
71
|
+
return new UserResource($user);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function destroy(User $user): Response
|
|
75
|
+
{
|
|
76
|
+
$user->delete();
|
|
77
|
+
|
|
78
|
+
return response()->noContent();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
## Form Request Validation
|
|
84
|
+
\`\`\`php
|
|
85
|
+
<?php
|
|
86
|
+
|
|
87
|
+
namespace App\\Http\\Requests;
|
|
88
|
+
|
|
89
|
+
use Illuminate\\Foundation\\Http\\FormRequest;
|
|
90
|
+
use Illuminate\\Validation\\Rules\\Password;
|
|
91
|
+
|
|
92
|
+
final class CreateUserRequest extends FormRequest
|
|
93
|
+
{
|
|
94
|
+
public function authorize(): bool
|
|
95
|
+
{
|
|
96
|
+
return true; // Or check permissions
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public function rules(): array
|
|
100
|
+
{
|
|
101
|
+
return [
|
|
102
|
+
'email' => ['required', 'email', 'max:255', 'unique:users'],
|
|
103
|
+
'name' => ['required', 'string', 'min:2', 'max:100'],
|
|
104
|
+
'password' => ['required', 'confirmed', Password::defaults()],
|
|
105
|
+
'role' => ['sometimes', 'string', 'in:user,admin'],
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public function messages(): array
|
|
110
|
+
{
|
|
111
|
+
return [
|
|
112
|
+
'email.unique' => 'This email is already registered.',
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected function prepareForValidation(): void
|
|
117
|
+
{
|
|
118
|
+
$this->merge([
|
|
119
|
+
'email' => strtolower($this->email),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## API Resources
|
|
126
|
+
\`\`\`php
|
|
127
|
+
<?php
|
|
128
|
+
|
|
129
|
+
namespace App\\Http\\Resources;
|
|
130
|
+
|
|
131
|
+
use Illuminate\\Http\\Request;
|
|
132
|
+
use Illuminate\\Http\\Resources\\Json\\JsonResource;
|
|
133
|
+
|
|
134
|
+
final class UserResource extends JsonResource
|
|
135
|
+
{
|
|
136
|
+
public function toArray(Request $request): array
|
|
137
|
+
{
|
|
138
|
+
return [
|
|
139
|
+
'id' => $this->id,
|
|
140
|
+
'email' => $this->email,
|
|
141
|
+
'name' => $this->name,
|
|
142
|
+
'is_active' => $this->is_active,
|
|
143
|
+
'created_at' => $this->created_at->toISOString(),
|
|
144
|
+
|
|
145
|
+
// Conditional relationships
|
|
146
|
+
'profile' => new ProfileResource($this->whenLoaded('profile')),
|
|
147
|
+
'roles' => RoleResource::collection($this->whenLoaded('roles')),
|
|
148
|
+
'posts_count' => $this->when(
|
|
149
|
+
$this->posts_count !== null,
|
|
150
|
+
$this->posts_count
|
|
151
|
+
),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Collection with pagination metadata
|
|
157
|
+
final class UserCollection extends ResourceCollection
|
|
158
|
+
{
|
|
159
|
+
public function toArray(Request $request): array
|
|
160
|
+
{
|
|
161
|
+
return [
|
|
162
|
+
'data' => $this->collection,
|
|
163
|
+
'meta' => [
|
|
164
|
+
'current_page' => $this->currentPage(),
|
|
165
|
+
'per_page' => $this->perPage(),
|
|
166
|
+
'total' => $this->total(),
|
|
167
|
+
'last_page' => $this->lastPage(),
|
|
168
|
+
],
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
## Eloquent Model
|
|
175
|
+
\`\`\`php
|
|
176
|
+
<?php
|
|
177
|
+
|
|
178
|
+
namespace App\\Models;
|
|
179
|
+
|
|
180
|
+
use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
|
|
181
|
+
use Illuminate\\Database\\Eloquent\\Model;
|
|
182
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
|
|
183
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
|
|
184
|
+
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
|
|
185
|
+
use Illuminate\\Database\\Eloquent\\SoftDeletes;
|
|
186
|
+
use Illuminate\\Database\\Eloquent\\Builder;
|
|
187
|
+
|
|
188
|
+
final class User extends Model
|
|
189
|
+
{
|
|
190
|
+
use HasFactory, SoftDeletes;
|
|
191
|
+
|
|
192
|
+
protected $fillable = [
|
|
193
|
+
'email',
|
|
194
|
+
'name',
|
|
195
|
+
'password',
|
|
196
|
+
'is_active',
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
protected $hidden = [
|
|
200
|
+
'password',
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
protected $casts = [
|
|
204
|
+
'is_active' => 'boolean',
|
|
205
|
+
'email_verified_at' => 'datetime',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// Relationships
|
|
209
|
+
public function profile(): HasOne
|
|
210
|
+
{
|
|
211
|
+
return $this->hasOne(Profile::class);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public function posts(): HasMany
|
|
215
|
+
{
|
|
216
|
+
return $this->hasMany(Post::class, 'author_id');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public function roles(): BelongsToMany
|
|
220
|
+
{
|
|
221
|
+
return $this->belongsToMany(Role::class)->withTimestamps();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Scopes
|
|
225
|
+
public function scopeActive(Builder $query): Builder
|
|
226
|
+
{
|
|
227
|
+
return $query->where('is_active', true);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public function scopeWithRole(Builder $query, string $role): Builder
|
|
231
|
+
{
|
|
232
|
+
return $query->whereHas('roles', fn($q) => $q->where('name', $role));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Accessors
|
|
236
|
+
protected function email(): Attribute
|
|
237
|
+
{
|
|
238
|
+
return Attribute::make(
|
|
239
|
+
get: fn(string $value) => strtolower($value),
|
|
240
|
+
set: fn(string $value) => strtolower($value),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Query methods
|
|
245
|
+
public static function findByEmail(string $email): ?self
|
|
246
|
+
{
|
|
247
|
+
return self::where('email', strtolower($email))->first();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
\`\`\`
|
|
251
|
+
|
|
252
|
+
## Service Pattern
|
|
253
|
+
\`\`\`php
|
|
254
|
+
<?php
|
|
255
|
+
|
|
256
|
+
namespace App\\Services;
|
|
257
|
+
|
|
258
|
+
use App\\Models\\User;
|
|
259
|
+
use App\\DTOs\\CreateUserData;
|
|
260
|
+
use Illuminate\\Support\\Facades\\DB;
|
|
261
|
+
use Illuminate\\Support\\Facades\\Hash;
|
|
262
|
+
|
|
263
|
+
final class UserService
|
|
264
|
+
{
|
|
265
|
+
public function __construct(
|
|
266
|
+
private readonly NotificationService $notifications,
|
|
267
|
+
) {}
|
|
268
|
+
|
|
269
|
+
public function create(CreateUserData $data): User
|
|
270
|
+
{
|
|
271
|
+
return DB::transaction(function () use ($data) {
|
|
272
|
+
$user = User::create([
|
|
273
|
+
'email' => $data->email,
|
|
274
|
+
'name' => $data->name,
|
|
275
|
+
'password' => Hash::make($data->password),
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
if ($data->role) {
|
|
279
|
+
$user->roles()->attach(Role::findByName($data->role));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
$this->notifications->sendWelcome($user);
|
|
283
|
+
|
|
284
|
+
return $user;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public function updateProfile(User $user, array $data): User
|
|
289
|
+
{
|
|
290
|
+
$user->update($data);
|
|
291
|
+
|
|
292
|
+
if (isset($data['avatar'])) {
|
|
293
|
+
$user->profile()->updateOrCreate(
|
|
294
|
+
['user_id' => $user->id],
|
|
295
|
+
['avatar' => $data['avatar']]
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return $user->fresh(['profile']);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## Migrations
|
|
305
|
+
\`\`\`php
|
|
306
|
+
<?php
|
|
307
|
+
|
|
308
|
+
use Illuminate\\Database\\Migrations\\Migration;
|
|
309
|
+
use Illuminate\\Database\\Schema\\Blueprint;
|
|
310
|
+
use Illuminate\\Support\\Facades\\Schema;
|
|
311
|
+
|
|
312
|
+
return new class extends Migration
|
|
313
|
+
{
|
|
314
|
+
public function up(): void
|
|
315
|
+
{
|
|
316
|
+
Schema::create('users', function (Blueprint $table) {
|
|
317
|
+
$table->uuid('id')->primary();
|
|
318
|
+
$table->string('email')->unique();
|
|
319
|
+
$table->string('name', 100);
|
|
320
|
+
$table->string('password');
|
|
321
|
+
$table->boolean('is_active')->default(true);
|
|
322
|
+
$table->timestamp('email_verified_at')->nullable();
|
|
323
|
+
$table->timestamps();
|
|
324
|
+
$table->softDeletes();
|
|
325
|
+
|
|
326
|
+
$table->index(['is_active', 'created_at']);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
public function down(): void
|
|
331
|
+
{
|
|
332
|
+
Schema::dropIfExists('users');
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
\`\`\`
|
|
336
|
+
|
|
337
|
+
## Testing
|
|
338
|
+
\`\`\`php
|
|
339
|
+
<?php
|
|
340
|
+
|
|
341
|
+
namespace Tests\\Feature;
|
|
342
|
+
|
|
343
|
+
use App\\Models\\User;
|
|
344
|
+
use Illuminate\\Foundation\\Testing\\RefreshDatabase;
|
|
345
|
+
use Tests\\TestCase;
|
|
346
|
+
|
|
347
|
+
final class UserControllerTest extends TestCase
|
|
348
|
+
{
|
|
349
|
+
use RefreshDatabase;
|
|
350
|
+
|
|
351
|
+
public function test_can_list_users(): void
|
|
352
|
+
{
|
|
353
|
+
User::factory()->count(3)->create();
|
|
354
|
+
|
|
355
|
+
$response = $this->getJson('/api/v1/users');
|
|
356
|
+
|
|
357
|
+
$response->assertOk()
|
|
358
|
+
->assertJsonCount(3, 'data')
|
|
359
|
+
->assertJsonStructure([
|
|
360
|
+
'data' => [['id', 'email', 'name']],
|
|
361
|
+
'meta' => ['current_page', 'total'],
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
public function test_can_create_user(): void
|
|
366
|
+
{
|
|
367
|
+
$data = [
|
|
368
|
+
'email' => 'test@example.com',
|
|
369
|
+
'name' => 'Test User',
|
|
370
|
+
'password' => 'password123',
|
|
371
|
+
'password_confirmation' => 'password123',
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
$response = $this->postJson('/api/v1/users', $data);
|
|
375
|
+
|
|
376
|
+
$response->assertCreated()
|
|
377
|
+
->assertJsonPath('data.email', 'test@example.com');
|
|
378
|
+
|
|
379
|
+
$this->assertDatabaseHas('users', [
|
|
380
|
+
'email' => 'test@example.com',
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
public function test_cannot_create_user_with_duplicate_email(): void
|
|
385
|
+
{
|
|
386
|
+
User::factory()->create(['email' => 'test@example.com']);
|
|
387
|
+
|
|
388
|
+
$response = $this->postJson('/api/v1/users', [
|
|
389
|
+
'email' => 'test@example.com',
|
|
390
|
+
'name' => 'Test',
|
|
391
|
+
'password' => 'password123',
|
|
392
|
+
'password_confirmation' => 'password123',
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
$response->assertUnprocessable()
|
|
396
|
+
->assertJsonValidationErrors(['email']);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
\`\`\`
|
|
400
|
+
|
|
401
|
+
## ✅ DO
|
|
402
|
+
- Use Form Requests for validation
|
|
403
|
+
- Use API Resources for response transformation
|
|
404
|
+
- Use Eloquent scopes for reusable queries
|
|
405
|
+
- Use Service classes for complex business logic
|
|
406
|
+
- Use DB::transaction for multi-model operations
|
|
407
|
+
- Use \`$casts\` for type casting
|
|
408
|
+
|
|
409
|
+
## ❌ DON'T
|
|
410
|
+
- Don't put business logic in controllers
|
|
411
|
+
- Don't return Eloquent models directly (use Resources)
|
|
412
|
+
- Don't use \`DB::raw()\` without parameterized queries
|
|
413
|
+
- Don't eager load everything (use \`whenLoaded\`)
|
|
414
|
+
- Don't forget \`$fillable\` or \`$guarded\`
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# MCP Browser Skill
|
|
2
|
+
|
|
3
|
+
## Server Configuration (Puppeteer)
|
|
4
|
+
\`\`\`json
|
|
5
|
+
// claude_desktop_config.json or .claude/settings.json
|
|
6
|
+
{
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"puppeteer": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
\`\`\`
|
|
15
|
+
|
|
16
|
+
## Server Configuration (Playwright)
|
|
17
|
+
\`\`\`json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"playwright": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@playwright/mcp-server"],
|
|
23
|
+
"env": {
|
|
24
|
+
"BROWSER": "chromium"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
## Available Tools
|
|
32
|
+
|
|
33
|
+
### Navigation
|
|
34
|
+
\`\`\`
|
|
35
|
+
navigate
|
|
36
|
+
- url: string (URL to navigate to)
|
|
37
|
+
- Returns: page title, URL after navigation
|
|
38
|
+
|
|
39
|
+
go_back
|
|
40
|
+
- Returns: previous page info
|
|
41
|
+
|
|
42
|
+
go_forward
|
|
43
|
+
- Returns: next page info
|
|
44
|
+
|
|
45
|
+
reload
|
|
46
|
+
- Returns: page info after reload
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### Screenshot & Content
|
|
50
|
+
\`\`\`
|
|
51
|
+
screenshot
|
|
52
|
+
- name?: string (optional filename)
|
|
53
|
+
- selector?: string (capture specific element)
|
|
54
|
+
- fullPage?: boolean (capture entire scrollable page)
|
|
55
|
+
- Returns: base64 encoded image
|
|
56
|
+
|
|
57
|
+
get_page_content
|
|
58
|
+
- Returns: page HTML content
|
|
59
|
+
|
|
60
|
+
get_page_text
|
|
61
|
+
- Returns: visible text content
|
|
62
|
+
|
|
63
|
+
get_page_title
|
|
64
|
+
- Returns: page title
|
|
65
|
+
|
|
66
|
+
get_current_url
|
|
67
|
+
- Returns: current URL
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
### Element Interaction
|
|
71
|
+
\`\`\`
|
|
72
|
+
click
|
|
73
|
+
- selector: string (CSS selector or XPath)
|
|
74
|
+
- button?: 'left' | 'right' | 'middle'
|
|
75
|
+
- clickCount?: number (for double-click: 2)
|
|
76
|
+
- Returns: success status
|
|
77
|
+
|
|
78
|
+
fill
|
|
79
|
+
- selector: string (input selector)
|
|
80
|
+
- value: string (text to fill)
|
|
81
|
+
- Returns: success status
|
|
82
|
+
|
|
83
|
+
select
|
|
84
|
+
- selector: string (select element)
|
|
85
|
+
- value: string (option value)
|
|
86
|
+
- Returns: selected value
|
|
87
|
+
|
|
88
|
+
check
|
|
89
|
+
- selector: string (checkbox/radio)
|
|
90
|
+
- Returns: success status
|
|
91
|
+
|
|
92
|
+
uncheck
|
|
93
|
+
- selector: string
|
|
94
|
+
- Returns: success status
|
|
95
|
+
|
|
96
|
+
hover
|
|
97
|
+
- selector: string
|
|
98
|
+
- Returns: success status
|
|
99
|
+
|
|
100
|
+
focus
|
|
101
|
+
- selector: string
|
|
102
|
+
- Returns: success status
|
|
103
|
+
|
|
104
|
+
press
|
|
105
|
+
- key: string (e.g., 'Enter', 'Tab', 'Escape')
|
|
106
|
+
- Returns: success status
|
|
107
|
+
|
|
108
|
+
type
|
|
109
|
+
- text: string (type character by character)
|
|
110
|
+
- delay?: number (ms between keystrokes)
|
|
111
|
+
- Returns: success status
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
### Element Queries
|
|
115
|
+
\`\`\`
|
|
116
|
+
query_selector
|
|
117
|
+
- selector: string
|
|
118
|
+
- Returns: element info or null
|
|
119
|
+
|
|
120
|
+
query_selector_all
|
|
121
|
+
- selector: string
|
|
122
|
+
- Returns: array of element info
|
|
123
|
+
|
|
124
|
+
get_element_text
|
|
125
|
+
- selector: string
|
|
126
|
+
- Returns: text content
|
|
127
|
+
|
|
128
|
+
get_element_attribute
|
|
129
|
+
- selector: string
|
|
130
|
+
- attribute: string
|
|
131
|
+
- Returns: attribute value
|
|
132
|
+
|
|
133
|
+
is_visible
|
|
134
|
+
- selector: string
|
|
135
|
+
- Returns: boolean
|
|
136
|
+
|
|
137
|
+
wait_for_selector
|
|
138
|
+
- selector: string
|
|
139
|
+
- timeout?: number (ms)
|
|
140
|
+
- state?: 'attached' | 'detached' | 'visible' | 'hidden'
|
|
141
|
+
- Returns: success status
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
### Page Control
|
|
145
|
+
\`\`\`
|
|
146
|
+
scroll
|
|
147
|
+
- direction: 'up' | 'down' | 'left' | 'right'
|
|
148
|
+
- amount?: number (pixels)
|
|
149
|
+
|
|
150
|
+
scroll_to_element
|
|
151
|
+
- selector: string
|
|
152
|
+
|
|
153
|
+
evaluate
|
|
154
|
+
- script: string (JavaScript to execute)
|
|
155
|
+
- Returns: script result
|
|
156
|
+
|
|
157
|
+
wait_for_navigation
|
|
158
|
+
- timeout?: number (ms)
|
|
159
|
+
- Returns: new page info
|
|
160
|
+
|
|
161
|
+
set_viewport
|
|
162
|
+
- width: number
|
|
163
|
+
- height: number
|
|
164
|
+
- deviceScaleFactor?: number
|
|
165
|
+
- isMobile?: boolean
|
|
166
|
+
\`\`\`
|
|
167
|
+
|
|
168
|
+
## Selector Strategies
|
|
169
|
+
|
|
170
|
+
### CSS Selectors
|
|
171
|
+
\`\`\`css
|
|
172
|
+
/* By ID */
|
|
173
|
+
#submit-button
|
|
174
|
+
|
|
175
|
+
/* By class */
|
|
176
|
+
.nav-item
|
|
177
|
+
.btn.btn-primary
|
|
178
|
+
|
|
179
|
+
/* By attribute */
|
|
180
|
+
[data-testid="login-form"]
|
|
181
|
+
input[type="email"]
|
|
182
|
+
a[href^="https://"]
|
|
183
|
+
|
|
184
|
+
/* By hierarchy */
|
|
185
|
+
form.login input[type="password"]
|
|
186
|
+
.sidebar > .menu-item:first-child
|
|
187
|
+
|
|
188
|
+
/* By state (CSS pseudo-classes) */
|
|
189
|
+
button:not(:disabled)
|
|
190
|
+
input:focus
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
### XPath Selectors
|
|
194
|
+
\`\`\`xpath
|
|
195
|
+
// By text content
|
|
196
|
+
//button[text()="Submit"]
|
|
197
|
+
//a[contains(text(), "Learn more")]
|
|
198
|
+
|
|
199
|
+
// By attribute
|
|
200
|
+
//input[@placeholder="Enter email"]
|
|
201
|
+
//*[@data-testid="user-profile"]
|
|
202
|
+
|
|
203
|
+
// By position
|
|
204
|
+
//ul/li[1]
|
|
205
|
+
(//button[@type="submit"])[2]
|
|
206
|
+
|
|
207
|
+
// By parent/sibling
|
|
208
|
+
//label[text()="Email"]/following-sibling::input
|
|
209
|
+
//td[text()="John"]/parent::tr
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
### Playwright-specific Selectors
|
|
213
|
+
\`\`\`
|
|
214
|
+
// Text selector
|
|
215
|
+
text=Sign In
|
|
216
|
+
text="Exact Match"
|
|
217
|
+
|
|
218
|
+
// Role selector (accessibility)
|
|
219
|
+
role=button[name="Submit"]
|
|
220
|
+
role=link[name=/learn more/i]
|
|
221
|
+
|
|
222
|
+
// Locator chaining
|
|
223
|
+
.form >> input[type="email"]
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
## Common Workflows
|
|
227
|
+
|
|
228
|
+
### Form Submission
|
|
229
|
+
\`\`\`
|
|
230
|
+
1. navigate to form URL
|
|
231
|
+
2. wait_for_selector: form element
|
|
232
|
+
3. fill: email field
|
|
233
|
+
4. fill: password field
|
|
234
|
+
5. click: submit button
|
|
235
|
+
6. wait_for_navigation
|
|
236
|
+
7. screenshot: confirmation page
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
### Data Extraction
|
|
240
|
+
\`\`\`
|
|
241
|
+
1. navigate to page
|
|
242
|
+
2. wait_for_selector: data container
|
|
243
|
+
3. query_selector_all: data items
|
|
244
|
+
4. Loop: get_element_text for each
|
|
245
|
+
5. Return structured data
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
### Screenshot Testing
|
|
249
|
+
\`\`\`
|
|
250
|
+
1. navigate to page
|
|
251
|
+
2. set_viewport: specific dimensions
|
|
252
|
+
3. wait_for_selector: key content
|
|
253
|
+
4. screenshot: fullPage true
|
|
254
|
+
5. Compare with baseline
|
|
255
|
+
\`\`\`
|
|
256
|
+
|
|
257
|
+
### Authentication Flow
|
|
258
|
+
\`\`\`
|
|
259
|
+
1. navigate: login page
|
|
260
|
+
2. fill: credentials
|
|
261
|
+
3. click: sign in
|
|
262
|
+
4. wait_for_selector: dashboard element
|
|
263
|
+
5. Cookies persist for session
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
## Wait Strategies
|
|
267
|
+
\`\`\`javascript
|
|
268
|
+
// Wait for element to appear
|
|
269
|
+
wait_for_selector with state: 'visible'
|
|
270
|
+
|
|
271
|
+
// Wait for element to disappear (loading spinner)
|
|
272
|
+
wait_for_selector with state: 'hidden'
|
|
273
|
+
|
|
274
|
+
// Wait for navigation after click
|
|
275
|
+
click then wait_for_navigation
|
|
276
|
+
|
|
277
|
+
// Wait for network idle (all requests complete)
|
|
278
|
+
// Some servers support: wait_for_load_state('networkidle')
|
|
279
|
+
|
|
280
|
+
// Fixed delay (avoid when possible)
|
|
281
|
+
// Use wait_for_selector instead
|
|
282
|
+
\`\`\`
|
|
283
|
+
|
|
284
|
+
## Handling Dynamic Content
|
|
285
|
+
\`\`\`
|
|
286
|
+
// Infinite scroll
|
|
287
|
+
1. scroll: down
|
|
288
|
+
2. wait_for_selector: new content
|
|
289
|
+
3. Repeat until done
|
|
290
|
+
|
|
291
|
+
// Modal/popup
|
|
292
|
+
1. click: trigger button
|
|
293
|
+
2. wait_for_selector: modal container
|
|
294
|
+
3. Interact with modal content
|
|
295
|
+
4. click: close button
|
|
296
|
+
5. wait_for_selector with state: 'hidden'
|
|
297
|
+
|
|
298
|
+
// AJAX content
|
|
299
|
+
1. click: trigger
|
|
300
|
+
2. wait_for_selector: loading indicator (hidden)
|
|
301
|
+
3. wait_for_selector: new content (visible)
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## ❌ DON'T
|
|
305
|
+
- Use fixed delays instead of wait_for_selector
|
|
306
|
+
- Click on invisible/disabled elements
|
|
307
|
+
- Ignore CAPTCHA or bot protection
|
|
308
|
+
- Scrape sites that prohibit it in robots.txt
|
|
309
|
+
- Store credentials in automation scripts
|
|
310
|
+
- Use automation for malicious purposes
|
|
311
|
+
|
|
312
|
+
## ✅ DO
|
|
313
|
+
- Use data-testid attributes for reliable selectors
|
|
314
|
+
- Wait for elements before interacting
|
|
315
|
+
- Handle navigation and loading states
|
|
316
|
+
- Use appropriate viewport for responsive sites
|
|
317
|
+
- Take screenshots for debugging failures
|
|
318
|
+
- Respect rate limits and robots.txt
|
|
319
|
+
- Handle popups and cookie banners
|
|
320
|
+
- Use role selectors for accessibility
|