@elyracode/laravel 0.7.12

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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.7.12] - 2026-05-24
4
+
5
+ ### Added
6
+ - `laravel_models` tool: map all Eloquent models with relationships, casts, scopes, and traits
7
+ - `laravel_routes` tool: route listing with middleware, controllers, and request classes
8
+ - `laravel_analyze` tool: project architecture analysis (stack, patterns, conventions)
9
+ - `elyra-laravel` skill: Laravel conventions and best practices
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @elyracode/laravel
2
+
3
+ Deep Laravel project understanding for Elyra. Gives the agent x-ray vision into your models, routes, and architecture so it generates code that matches your project's conventions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ elyra install npm:@elyracode/laravel
9
+ ```
10
+
11
+ ## Available Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `laravel_models` | Map all Eloquent models with relationships, casts, scopes, traits, and fillable fields |
16
+ | `laravel_routes` | List routes with middleware, controllers, form requests, and resources |
17
+ | `laravel_analyze` | Analyze project architecture: stack, patterns, auth, queue, conventions |
18
+
19
+ ## Why
20
+
21
+ Other coding agents read Laravel files one at a time and guess at conventions. With these tools, the agent sees your entire data model and architecture before writing a single line — fewer wrong guesses, fewer wasted tokens, code that matches your project.
22
+
23
+ ## Included Skill
24
+
25
+ The `elyra-laravel` skill provides deep knowledge of Laravel conventions, Eloquent patterns, common architectures, and best practices.
@@ -0,0 +1,530 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { basename, join, relative } from "node:path";
4
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ // ── Helpers ─────────────────────────────────────────────────────────────────
8
+
9
+ function isLaravelProject(cwd: string): boolean {
10
+ const composerPath = join(cwd, "composer.json");
11
+ if (!existsSync(composerPath)) return false;
12
+ try {
13
+ const composer = JSON.parse(readFileSync(composerPath, "utf-8")) as Record<string, unknown>;
14
+ const require = composer.require as Record<string, string> | undefined;
15
+ return Boolean(require?.["laravel/framework"]);
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function readPhpFile(filePath: string): string {
22
+ try {
23
+ return readFileSync(filePath, "utf-8");
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
28
+
29
+ function findPhpFiles(dir: string, maxDepth = 5): string[] {
30
+ const results: string[] = [];
31
+ if (!existsSync(dir)) return results;
32
+
33
+ function walk(current: string, depth: number): void {
34
+ if (depth > maxDepth) return;
35
+ let entries: string[];
36
+ try {
37
+ entries = readdirSync(current);
38
+ } catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ if (entry.startsWith(".")) continue;
43
+ const full = join(current, entry);
44
+ try {
45
+ const stat = statSync(full);
46
+ if (stat.isDirectory()) {
47
+ walk(full, depth + 1);
48
+ } else if (entry.endsWith(".php")) {
49
+ results.push(full);
50
+ }
51
+ } catch {
52
+ // skip
53
+ }
54
+ }
55
+ }
56
+ walk(dir, 0);
57
+ return results;
58
+ }
59
+
60
+ function artisan(cwd: string, args: string, timeoutMs = 15_000): string {
61
+ try {
62
+ return execSync(`php artisan ${args}`, {
63
+ cwd,
64
+ encoding: "utf-8",
65
+ timeout: timeoutMs,
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ }).trim();
68
+ } catch {
69
+ return "";
70
+ }
71
+ }
72
+
73
+ // ── Model Parser ────────────────────────────────────────────────────────────
74
+
75
+ interface ModelInfo {
76
+ name: string;
77
+ path: string;
78
+ relationships: Array<{ type: string; related: string; method: string }>;
79
+ fillable: string[];
80
+ casts: string[];
81
+ scopes: string[];
82
+ traits: string[];
83
+ implements: string[];
84
+ useSoftDeletes: boolean;
85
+ }
86
+
87
+ function parseModel(filePath: string, cwd: string): ModelInfo | null {
88
+ const content = readPhpFile(filePath);
89
+ if (!content.includes("extends Model") && !content.includes("extends Authenticatable")) return null;
90
+
91
+ const nameMatch = content.match(/class\s+(\w+)\s+extends/);
92
+ if (!nameMatch) return null;
93
+
94
+ const name = nameMatch[1];
95
+ const relPath = relative(cwd, filePath);
96
+
97
+ // Relationships
98
+ const relationships: ModelInfo["relationships"] = [];
99
+ const relRegex =
100
+ /(?:public\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{[^}]*\$this->(hasOne|hasMany|belongsTo|belongsToMany|hasManyThrough|hasOneThrough|morphOne|morphMany|morphToMany|morphTo|morphedByMany)\s*\(\s*(?:\\?[\w\\]*\\)?(\w+)::class/g;
101
+ let relMatch: RegExpExecArray | null = null;
102
+ while ((relMatch = relRegex.exec(content)) !== null) {
103
+ relationships.push({
104
+ method: relMatch[1],
105
+ type: relMatch[2],
106
+ related: relMatch[3],
107
+ });
108
+ }
109
+
110
+ // Fillable
111
+ const fillable: string[] = [];
112
+ const fillableMatch = content.match(/\$fillable\s*=\s*\[([\s\S]*?)\]/);
113
+ if (fillableMatch) {
114
+ const items = fillableMatch[1].match(/'([^']+)'/g);
115
+ if (items) {
116
+ for (const item of items) fillable.push(item.replace(/'/g, ""));
117
+ }
118
+ }
119
+
120
+ // Casts
121
+ const casts: string[] = [];
122
+ const castsMatch = content.match(/(?:protected\s+function\s+casts\s*\(\)[\s\S]*?return\s*\[([\s\S]*?)\]|\$casts\s*=\s*\[([\s\S]*?)\])/);
123
+ if (castsMatch) {
124
+ const castBlock = castsMatch[1] ?? castsMatch[2] ?? "";
125
+ const castItems = castBlock.match(/'(\w+)'\s*=>\s*'?([^',\]]+)/g);
126
+ if (castItems) {
127
+ for (const item of castItems) {
128
+ const parts = item.match(/'(\w+)'\s*=>\s*'?([^',\]]+)/);
129
+ if (parts) casts.push(`${parts[1]} (${parts[2].trim().replace(/'/g, "")})`);
130
+ }
131
+ }
132
+ }
133
+
134
+ // Scopes
135
+ const scopes: string[] = [];
136
+ const scopeRegex = /function\s+scope(\w+)\s*\(/g;
137
+ let scopeMatch: RegExpExecArray | null = null;
138
+ while ((scopeMatch = scopeRegex.exec(content)) !== null) {
139
+ scopes.push(scopeMatch[1].charAt(0).toLowerCase() + scopeMatch[1].slice(1));
140
+ }
141
+
142
+ // Traits
143
+ const traits: string[] = [];
144
+ const useRegex = /use\s+([\w\\]+(?:\s*,\s*[\w\\]+)*)\s*;/g;
145
+ let useMatch: RegExpExecArray | null = null;
146
+ while ((useMatch = useRegex.exec(content)) !== null) {
147
+ // Only traits inside the class body (after 'class X extends')
148
+ if (content.indexOf(useMatch[0]) > content.indexOf("extends")) {
149
+ const traitNames = useMatch[1].split(",").map((t) => {
150
+ const parts = t.trim().split("\\");
151
+ return parts[parts.length - 1];
152
+ });
153
+ traits.push(...traitNames);
154
+ }
155
+ }
156
+
157
+ const useSoftDeletes = traits.includes("SoftDeletes");
158
+
159
+ // Implements
160
+ const implMatch = content.match(/implements\s+([\w\\,\s]+)/);
161
+ const implements_: string[] = [];
162
+ if (implMatch) {
163
+ implements_.push(
164
+ ...implMatch[1].split(",").map((i) => {
165
+ const parts = i.trim().split("\\");
166
+ return parts[parts.length - 1];
167
+ }),
168
+ );
169
+ }
170
+
171
+ return {
172
+ name,
173
+ path: relPath,
174
+ relationships,
175
+ fillable,
176
+ casts,
177
+ scopes,
178
+ traits: traits.filter((t) => t !== ""),
179
+ implements: implements_,
180
+ useSoftDeletes,
181
+ };
182
+ }
183
+
184
+ function formatModels(models: ModelInfo[]): string {
185
+ if (models.length === 0) return "No Eloquent models found.";
186
+
187
+ const lines: string[] = [`${models.length} Eloquent models found:\n`];
188
+
189
+ for (const model of models) {
190
+ lines.push(`${model.name} (${model.path})`);
191
+
192
+ if (model.relationships.length > 0) {
193
+ for (const rel of model.relationships) {
194
+ lines.push(` ${rel.type}: ${rel.related} (via ${rel.method}())`);
195
+ }
196
+ }
197
+ if (model.fillable.length > 0) {
198
+ lines.push(` fillable: ${model.fillable.join(", ")}`);
199
+ }
200
+ if (model.casts.length > 0) {
201
+ lines.push(` casts: ${model.casts.join(", ")}`);
202
+ }
203
+ if (model.scopes.length > 0) {
204
+ lines.push(` scopes: ${model.scopes.join(", ")}`);
205
+ }
206
+ if (model.traits.length > 0) {
207
+ lines.push(` traits: ${model.traits.join(", ")}`);
208
+ }
209
+ if (model.implements.length > 0) {
210
+ lines.push(` implements: ${model.implements.join(", ")}`);
211
+ }
212
+ lines.push("");
213
+ }
214
+
215
+ return lines.join("\n");
216
+ }
217
+
218
+ // ── Route Parser ────────────────────────────────────────────────────────────
219
+
220
+ function parseRoutes(cwd: string, prefix?: string): string {
221
+ let args = "route:list --json";
222
+ if (prefix) args += ` --path=${prefix}`;
223
+
224
+ const output = artisan(cwd, args);
225
+ if (!output) return "Could not fetch routes. Is php artisan available?";
226
+
227
+ try {
228
+ const routes = JSON.parse(output) as Array<{
229
+ method: string;
230
+ uri: string;
231
+ name: string | null;
232
+ action: string;
233
+ middleware: string[] | string;
234
+ }>;
235
+
236
+ if (routes.length === 0) return prefix ? `No routes matching prefix "${prefix}".` : "No routes defined.";
237
+
238
+ const lines: string[] = [`${routes.length} routes:\n`];
239
+
240
+ for (const route of routes) {
241
+ const methods = route.method.toUpperCase();
242
+ const mw = Array.isArray(route.middleware)
243
+ ? route.middleware.join(", ")
244
+ : route.middleware || "";
245
+
246
+ lines.push(`${methods} /${route.uri}`);
247
+ if (route.name) lines.push(` name: ${route.name}`);
248
+ lines.push(` action: ${route.action}`);
249
+ if (mw) lines.push(` middleware: ${mw}`);
250
+ lines.push("");
251
+ }
252
+
253
+ return lines.join("\n");
254
+ } catch {
255
+ return "Could not parse route list output.";
256
+ }
257
+ }
258
+
259
+ // ── Project Analyzer ────────────────────────────────────────────────────────
260
+
261
+ function analyzeProject(cwd: string, models: ModelInfo[]): string {
262
+ const lines: string[] = [];
263
+
264
+ // Laravel version
265
+ const composerLock = join(cwd, "composer.lock");
266
+ let laravelVersion = "unknown";
267
+ if (existsSync(composerLock)) {
268
+ try {
269
+ const lock = JSON.parse(readFileSync(composerLock, "utf-8")) as {
270
+ packages: Array<{ name: string; version: string }>;
271
+ };
272
+ const laravel = lock.packages.find((p) => p.name === "laravel/framework");
273
+ if (laravel) laravelVersion = laravel.version;
274
+ } catch {
275
+ // skip
276
+ }
277
+ }
278
+
279
+ // Detect stack
280
+ const composer = JSON.parse(readFileSync(join(cwd, "composer.json"), "utf-8")) as Record<string, unknown>;
281
+ const require = { ...(composer.require as Record<string, string> ?? {}), ...(composer["require-dev"] as Record<string, string> ?? {}) };
282
+ const packageJson = existsSync(join(cwd, "package.json"))
283
+ ? (JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8")) as Record<string, unknown>)
284
+ : null;
285
+ const npmDeps = packageJson
286
+ ? { ...(packageJson.dependencies as Record<string, string> ?? {}), ...(packageJson.devDependencies as Record<string, string> ?? {}) }
287
+ : {};
288
+
289
+ const stack: string[] = [`Laravel ${laravelVersion}`];
290
+ if (require["livewire/livewire"]) stack.push("Livewire");
291
+ if (require["inertiajs/inertia-laravel"]) stack.push("Inertia");
292
+ if (require["laravel/sanctum"]) stack.push("Sanctum");
293
+ if (require["laravel/passport"]) stack.push("Passport");
294
+ if (require["filament/filament"]) stack.push("Filament");
295
+ if (npmDeps.vue) stack.push("Vue");
296
+ if (npmDeps.react) stack.push("React");
297
+ if (npmDeps.svelte) stack.push("Svelte");
298
+ if (npmDeps.tailwindcss || npmDeps["@tailwindcss/vite"]) stack.push("Tailwind");
299
+ if (npmDeps.alpinejs) stack.push("Alpine.js");
300
+
301
+ lines.push(`Stack: ${stack.join(" + ")}`);
302
+
303
+ // Auth
304
+ if (require["laravel/sanctum"]) lines.push("Auth: Sanctum");
305
+ else if (require["laravel/passport"]) lines.push("Auth: Passport");
306
+ else if (require["laravel/fortify"]) lines.push("Auth: Fortify");
307
+ else if (require["laravel/breeze"]) lines.push("Auth: Breeze");
308
+ else if (require["laravel/jetstream"]) lines.push("Auth: Jetstream");
309
+
310
+ // Queue/Cache
311
+ const envPath = join(cwd, ".env");
312
+ if (existsSync(envPath)) {
313
+ const env = readFileSync(envPath, "utf-8");
314
+ const queueMatch = env.match(/QUEUE_CONNECTION=(\w+)/);
315
+ const cacheMatch = env.match(/CACHE_STORE=(\w+)/);
316
+ const dbMatch = env.match(/DB_CONNECTION=(\w+)/);
317
+ if (queueMatch) lines.push(`Queue: ${queueMatch[1]}`);
318
+ if (cacheMatch) lines.push(`Cache: ${cacheMatch[1]}`);
319
+ if (dbMatch) lines.push(`Database: ${dbMatch[1]}`);
320
+ }
321
+
322
+ // Patterns detection
323
+ const patterns: string[] = [];
324
+ if (existsSync(join(cwd, "app/Actions"))) patterns.push("Action classes (app/Actions/)");
325
+ if (existsSync(join(cwd, "app/Services"))) patterns.push("Service classes (app/Services/)");
326
+ if (existsSync(join(cwd, "app/Repositories"))) patterns.push("Repository pattern (app/Repositories/)");
327
+ if (existsSync(join(cwd, "app/DTOs")) || existsSync(join(cwd, "app/DataTransferObjects"))) patterns.push("DTOs");
328
+ if (existsSync(join(cwd, "app/Enums"))) patterns.push("Enums (app/Enums/)");
329
+ if (existsSync(join(cwd, "app/Observers"))) patterns.push("Observers (app/Observers/)");
330
+ if (existsSync(join(cwd, "app/Events"))) patterns.push("Events (app/Events/)");
331
+ if (existsSync(join(cwd, "app/Listeners"))) patterns.push("Listeners (app/Listeners/)");
332
+ if (existsSync(join(cwd, "app/Jobs"))) patterns.push("Jobs (app/Jobs/)");
333
+
334
+ if (patterns.length > 0) {
335
+ lines.push(`Patterns: ${patterns.join(", ")}`);
336
+ }
337
+
338
+ // Controller style
339
+ const controllerDir = join(cwd, "app/Http/Controllers");
340
+ if (existsSync(controllerDir)) {
341
+ const controllers = findPhpFiles(controllerDir, 2);
342
+ let invokable = 0;
343
+ let resource = 0;
344
+ for (const c of controllers.slice(0, 20)) {
345
+ const content = readPhpFile(c);
346
+ if (content.includes("__invoke")) invokable++;
347
+ if (content.includes("function index") && content.includes("function store")) resource++;
348
+ }
349
+ if (invokable > resource && invokable > 0) {
350
+ lines.push("Controllers: primarily invokable (single-action)");
351
+ } else if (resource > 0) {
352
+ lines.push("Controllers: primarily resource-style");
353
+ }
354
+ }
355
+
356
+ // Models summary
357
+ lines.push(`Models: ${models.length}`);
358
+
359
+ // Migrations
360
+ const migrationDir = join(cwd, "database/migrations");
361
+ if (existsSync(migrationDir)) {
362
+ try {
363
+ const migrations = readdirSync(migrationDir).filter((f) => f.endsWith(".php"));
364
+ lines.push(`Migrations: ${migrations.length}`);
365
+ } catch {
366
+ // skip
367
+ }
368
+ }
369
+
370
+ // Factories
371
+ const factoryDir = join(cwd, "database/factories");
372
+ if (existsSync(factoryDir)) {
373
+ try {
374
+ const factories = readdirSync(factoryDir).filter((f) => f.endsWith(".php"));
375
+ const modelsWithoutFactory = models.filter(
376
+ (m) => !factories.some((f) => f.includes(m.name)),
377
+ );
378
+ lines.push(`Factories: ${factories.length}`);
379
+ if (modelsWithoutFactory.length > 0) {
380
+ lines.push(`Missing factories: ${modelsWithoutFactory.map((m) => m.name).join(", ")}`);
381
+ }
382
+ } catch {
383
+ // skip
384
+ }
385
+ }
386
+
387
+ // Tests
388
+ const testDir = join(cwd, "tests");
389
+ if (existsSync(testDir)) {
390
+ const testFiles = findPhpFiles(testDir);
391
+ const usesPest = testFiles.some((f) => readPhpFile(f).includes("it(") || readPhpFile(f).includes("test("));
392
+ lines.push(`Tests: ${testFiles.length} files (${usesPest ? "Pest" : "PHPUnit"})`);
393
+ }
394
+
395
+ // Pending migrations
396
+ const pendingOutput = artisan(cwd, "migrate:status --no-interaction 2>&1");
397
+ if (pendingOutput) {
398
+ const pending = (pendingOutput.match(/Pending/gi) ?? []).length;
399
+ if (pending > 0) {
400
+ lines.push(`Pending migrations: ${pending}`);
401
+ }
402
+ }
403
+
404
+ return lines.join("\n");
405
+ }
406
+
407
+ // ── Extension ───────────────────────────────────────────────────────────────
408
+
409
+ export default function (elyra: ExtensionAPI): void {
410
+ let cwd = "";
411
+ let toolsRegistered = false;
412
+
413
+ elyra.on("session_start", async (_event, ctx) => {
414
+ cwd = ctx.cwd;
415
+
416
+ // Only register tools when running inside a Laravel project
417
+ if (!toolsRegistered && isLaravelProject(getCwd())) {
418
+ toolsRegistered = true;
419
+ registerTools(elyra, () => cwd);
420
+ }
421
+ });
422
+ }
423
+
424
+ function registerTools(elyra: ExtensionAPI, getCwd: () => string): void {
425
+
426
+ // ── laravel_models ───────────────────────────────────────────────────
427
+
428
+ const modelsSchema = Type.Object({
429
+ filter: Type.Optional(
430
+ Type.String({ description: "Filter models by name (case-insensitive substring match)" }),
431
+ ),
432
+ });
433
+
434
+ elyra.registerTool({
435
+ name: "laravel_models",
436
+ label: "Laravel Models",
437
+ description:
438
+ "Map all Eloquent models in the project with their relationships, fillable fields, casts, scopes, and traits. Returns a complete data model overview. Use this before generating code that involves database models.",
439
+ parameters: modelsSchema,
440
+ promptSnippet: "Map all Eloquent models with relationships and structure",
441
+ async execute(_toolCallId, params) {
442
+ if (!isLaravelProject(getCwd())) {
443
+ return { content: [{ type: "text", text: "Not a Laravel project (no laravel/framework in composer.json)" }], isError: true };
444
+ }
445
+
446
+ const modelDirs = [
447
+ join(getCwd(), "app/Models"),
448
+ join(getCwd(), "app"), // Laravel < 8 style
449
+ ];
450
+
451
+ const allModels: ModelInfo[] = [];
452
+ const seen = new Set<string>();
453
+
454
+ for (const dir of modelDirs) {
455
+ const files = findPhpFiles(dir, 3);
456
+ for (const file of files) {
457
+ const model = parseModel(file, getCwd());
458
+ if (model && !seen.has(model.name)) {
459
+ seen.add(model.name);
460
+ if (!params.filter || model.name.toLowerCase().includes(params.filter.toLowerCase())) {
461
+ allModels.push(model);
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ allModels.sort((a, b) => a.name.localeCompare(b.name));
468
+ return { content: [{ type: "text", text: formatModels(allModels) }] };
469
+ },
470
+ });
471
+
472
+ // ── laravel_routes ───────────────────────────────────────────────────
473
+
474
+ const routesSchema = Type.Object({
475
+ prefix: Type.Optional(
476
+ Type.String({ description: "Filter routes by URI prefix (e.g. 'api', 'auth', 'admin')" }),
477
+ ),
478
+ });
479
+
480
+ elyra.registerTool({
481
+ name: "laravel_routes",
482
+ label: "Laravel Routes",
483
+ description:
484
+ "List all registered routes with their HTTP methods, middleware, controller actions, and names. Requires php artisan to be available. Use this to understand the existing API surface before adding new routes.",
485
+ parameters: routesSchema,
486
+ promptSnippet: "List Laravel routes with middleware and controllers",
487
+ async execute(_toolCallId, params) {
488
+ if (!isLaravelProject(getCwd())) {
489
+ return { content: [{ type: "text", text: "Not a Laravel project" }], isError: true };
490
+ }
491
+ const result = parseRoutes(getCwd(), params.prefix);
492
+ return { content: [{ type: "text", text: result }] };
493
+ },
494
+ });
495
+
496
+ // ── laravel_analyze ──────────────────────────────────────────────────
497
+
498
+ const analyzeSchema = Type.Object({});
499
+
500
+ elyra.registerTool({
501
+ name: "laravel_analyze",
502
+ label: "Laravel Analyze",
503
+ description:
504
+ "Analyze the Laravel project architecture: framework version, stack (Livewire/Inertia/Vue/React), authentication method, patterns used (Actions/Services/Repositories), controller style, model count, test framework, and missing factories. Run this first when working on a Laravel project to understand conventions.",
505
+ parameters: analyzeSchema,
506
+ promptSnippet: "Analyze Laravel project architecture and conventions",
507
+ async execute() {
508
+ if (!isLaravelProject(getCwd())) {
509
+ return { content: [{ type: "text", text: "Not a Laravel project" }], isError: true };
510
+ }
511
+
512
+ // Parse models for the analysis
513
+ const modelDirs = [join(getCwd(), "app/Models"), join(getCwd(), "app")];
514
+ const allModels: ModelInfo[] = [];
515
+ const seen = new Set<string>();
516
+ for (const dir of modelDirs) {
517
+ for (const file of findPhpFiles(dir, 3)) {
518
+ const model = parseModel(file, getCwd());
519
+ if (model && !seen.has(model.name)) {
520
+ seen.add(model.name);
521
+ allModels.push(model);
522
+ }
523
+ }
524
+ }
525
+
526
+ const result = analyzeProject(getCwd(), allModels);
527
+ return { content: [{ type: "text", text: result }] };
528
+ },
529
+ });
530
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@elyracode/laravel",
3
+ "version": "0.7.12",
4
+ "description": "Deep Laravel project understanding for Elyra -- model relationships, route mapping, architecture analysis",
5
+ "type": "module",
6
+ "keywords": [
7
+ "elyra-package",
8
+ "laravel",
9
+ "eloquent",
10
+ "artisan",
11
+ "php"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Knut W. Horne",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/kwhorne/elyra.git",
18
+ "directory": "packages/laravel"
19
+ },
20
+ "elyra": {
21
+ "extensions": [
22
+ "./extensions/index.ts"
23
+ ],
24
+ "skills": [
25
+ "./skills"
26
+ ]
27
+ },
28
+ "peerDependencies": {
29
+ "@elyracode/coding-agent": "*",
30
+ "typebox": "*"
31
+ },
32
+ "scripts": {
33
+ "clean": "echo 'nothing to clean'",
34
+ "build": "echo 'nothing to build'",
35
+ "check": "echo 'nothing to check'"
36
+ }
37
+ }
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: elyra-laravel
3
+ description: Laravel project understanding. Use when the project has a composer.json with laravel/framework, or when the user asks about Eloquent models, routes, migrations, controllers, or any Laravel-specific topic.
4
+ ---
5
+
6
+ # Laravel Development
7
+
8
+ ## When to Use
9
+
10
+ Use Laravel tools when:
11
+ - The project has `composer.json` with `laravel/framework`
12
+ - The user asks about models, relationships, migrations, or database structure
13
+ - The user wants to add a feature that touches multiple layers (model, controller, routes, tests)
14
+ - You need to understand the existing architecture before generating code
15
+
16
+ ## Available Tools
17
+
18
+ | Tool | Use when |
19
+ |------|----------|
20
+ | `laravel_models` | Understanding data model, relationships, and model structure before generating code |
21
+ | `laravel_routes` | Understanding existing routes, middleware, and controllers before adding new endpoints |
22
+ | `laravel_analyze` | Understanding project conventions and patterns before writing any code |
23
+
24
+ ## Key Principle
25
+
26
+ Run `laravel_analyze` or `laravel_models` BEFORE generating Laravel code. Match the project's existing patterns — if it uses Actions, don't create Services. If controllers are invokable, don't create resource controllers.
27
+
28
+ ## Common Laravel Patterns
29
+
30
+ | Pattern | Files | When used |
31
+ |---------|-------|-----------|
32
+ | Action classes | `app/Actions/` | Single-purpose business logic |
33
+ | Service classes | `app/Services/` | Complex business logic |
34
+ | Repository pattern | `app/Repositories/` | Database abstraction |
35
+ | Form Requests | `app/Http/Requests/` | Input validation |
36
+ | API Resources | `app/Http/Resources/` | Response transformation |
37
+ | Policies | `app/Policies/` | Authorization |
38
+ | Observers | `app/Observers/` | Model event hooks |
39
+ | Enums | `app/Enums/` | Type-safe constants (PHP 8.1+) |
40
+
41
+ ## Eloquent Relationship Methods
42
+
43
+ | Method | Meaning |
44
+ |--------|---------|
45
+ | `hasOne` | One-to-one (parent side) |
46
+ | `belongsTo` | One-to-one/many inverse (child side) |
47
+ | `hasMany` | One-to-many (parent side) |
48
+ | `belongsToMany` | Many-to-many (either side, uses pivot table) |
49
+ | `hasOneThrough` | One-to-one through intermediate model |
50
+ | `hasManyThrough` | One-to-many through intermediate model |
51
+ | `morphOne` | Polymorphic one-to-one |
52
+ | `morphMany` | Polymorphic one-to-many |
53
+ | `morphToMany` | Polymorphic many-to-many |
54
+ | `morphTo` | Polymorphic inverse |
55
+
56
+ ## Migration Best Practices
57
+
58
+ - Always create a migration with a new model
59
+ - Use `$table->foreignId('user_id')->constrained()->cascadeOnDelete()`
60
+ - Use `$table->timestamps()` on every table
61
+ - Name pivot tables alphabetically: `post_tag` not `tag_post`
62
+ - Add indexes on frequently queried columns
63
+
64
+ ## Testing Conventions
65
+
66
+ - Use Pest for new Laravel 11+ projects
67
+ - One test file per feature or model
68
+ - Use factories for test data, never manual inserts
69
+ - Name test methods descriptively: `it('creates a post with valid data')`
70
+
71
+ ## Feature Generation Checklist
72
+
73
+ When adding a new feature, generate in this order:
74
+ 1. Migration
75
+ 2. Model (with relationships, casts, fillable)
76
+ 3. Factory
77
+ 4. Form Request (validation)
78
+ 5. Controller or Action
79
+ 6. API Resource (if API)
80
+ 7. Routes
81
+ 8. Policy (authorization)
82
+ 9. Tests